mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
chore: support signed token query param for web terminal (#7197)
* chore: add endpoint to get token for web terminal * chore: support signed token query param for web terminal
This commit is contained in:
@ -1701,10 +1701,6 @@ func (q *querier) GetWorkspaceProxyByName(ctx context.Context, name string) (dat
|
||||
return fetch(q.log, q.auth, q.db.GetWorkspaceProxyByName)(ctx, name)
|
||||
}
|
||||
|
||||
func (q *querier) GetWorkspaceProxyByHostname(ctx context.Context, hostname string) (database.WorkspaceProxy, error) {
|
||||
return fetch(q.log, q.auth, q.db.GetWorkspaceProxyByHostname)(ctx, hostname)
|
||||
}
|
||||
|
||||
func (q *querier) InsertWorkspaceProxy(ctx context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||
return insert(q.log, q.auth, rbac.ResourceWorkspaceProxy, q.db.InsertWorkspaceProxy)(ctx, arg)
|
||||
}
|
||||
|
@ -438,3 +438,10 @@ func (q *querier) InsertParameterSchema(ctx context.Context, arg database.Insert
|
||||
}
|
||||
return q.db.InsertParameterSchema(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetWorkspaceProxyByHostname(ctx context.Context, params database.GetWorkspaceProxyByHostnameParams) (database.WorkspaceProxy, error) {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
|
||||
return database.WorkspaceProxy{}, err
|
||||
}
|
||||
return q.db.GetWorkspaceProxyByHostname(ctx, params)
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import (
|
||||
"github.com/coder/coder/coderd/util/slice"
|
||||
)
|
||||
|
||||
var validProxyByHostnameRegex = regexp.MustCompile(`^[a-zA-Z0-9.-]+$`)
|
||||
var validProxyByHostnameRegex = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
||||
|
||||
// FakeDatabase is helpful for knowing if the underlying db is an in memory fake
|
||||
// database. This is only in the databasefake package, so will only be used
|
||||
@ -5142,34 +5142,36 @@ func (q *fakeQuerier) GetWorkspaceProxyByName(_ context.Context, name string) (d
|
||||
return database.WorkspaceProxy{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceProxyByHostname(_ context.Context, hostname string) (database.WorkspaceProxy, error) {
|
||||
func (q *fakeQuerier) GetWorkspaceProxyByHostname(_ context.Context, params database.GetWorkspaceProxyByHostnameParams) (database.WorkspaceProxy, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
// Return zero rows if this is called with a non-sanitized hostname. The SQL
|
||||
// version of this query does the same thing.
|
||||
if !validProxyByHostnameRegex.MatchString(hostname) {
|
||||
if !validProxyByHostnameRegex.MatchString(params.Hostname) {
|
||||
return database.WorkspaceProxy{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
// This regex matches the SQL version.
|
||||
accessURLRegex := regexp.MustCompile(`[^:]*://` + regexp.QuoteMeta(hostname) + `([:/]?.)*`)
|
||||
accessURLRegex := regexp.MustCompile(`[^:]*://` + regexp.QuoteMeta(params.Hostname) + `([:/]?.)*`)
|
||||
|
||||
for _, proxy := range q.workspaceProxies {
|
||||
if proxy.Deleted {
|
||||
continue
|
||||
}
|
||||
if accessURLRegex.MatchString(proxy.Url) {
|
||||
if params.AllowAccessUrl && accessURLRegex.MatchString(proxy.Url) {
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// Compile the app hostname regex. This is slow sadly.
|
||||
wildcardRegexp, err := httpapi.CompileHostnamePattern(proxy.WildcardHostname)
|
||||
if err != nil {
|
||||
return database.WorkspaceProxy{}, xerrors.Errorf("compile hostname pattern %q for proxy %q (%s): %w", proxy.WildcardHostname, proxy.Name, proxy.ID.String(), err)
|
||||
}
|
||||
if _, ok := httpapi.ExecuteHostnamePattern(wildcardRegexp, hostname); ok {
|
||||
return proxy, nil
|
||||
if params.AllowWildcardHostname {
|
||||
wildcardRegexp, err := httpapi.CompileHostnamePattern(proxy.WildcardHostname)
|
||||
if err != nil {
|
||||
return database.WorkspaceProxy{}, xerrors.Errorf("compile hostname pattern %q for proxy %q (%s): %w", proxy.WildcardHostname, proxy.Name, proxy.ID.String(), err)
|
||||
}
|
||||
if _, ok := httpapi.ExecuteHostnamePattern(wildcardRegexp, params.Hostname); ok {
|
||||
return proxy, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,44 +160,74 @@ func TestProxyByHostname(t *testing.T) {
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
testHostname string
|
||||
matchProxyName string
|
||||
name string
|
||||
testHostname string
|
||||
allowAccessURL bool
|
||||
allowWildcardHost bool
|
||||
matchProxyName string
|
||||
}{
|
||||
{
|
||||
name: "NoMatch",
|
||||
testHostname: "test.com",
|
||||
matchProxyName: "",
|
||||
name: "NoMatch",
|
||||
testHostname: "test.com",
|
||||
allowAccessURL: true,
|
||||
allowWildcardHost: true,
|
||||
matchProxyName: "",
|
||||
},
|
||||
{
|
||||
name: "MatchAccessURL",
|
||||
testHostname: "one.coder.com",
|
||||
matchProxyName: "one",
|
||||
name: "MatchAccessURL",
|
||||
testHostname: "one.coder.com",
|
||||
allowAccessURL: true,
|
||||
allowWildcardHost: true,
|
||||
matchProxyName: "one",
|
||||
},
|
||||
{
|
||||
name: "MatchWildcard",
|
||||
testHostname: "something.wildcard.one.coder.com",
|
||||
matchProxyName: "one",
|
||||
name: "MatchWildcard",
|
||||
testHostname: "something.wildcard.one.coder.com",
|
||||
allowAccessURL: true,
|
||||
allowWildcardHost: true,
|
||||
matchProxyName: "one",
|
||||
},
|
||||
{
|
||||
name: "MatchSuffix",
|
||||
testHostname: "something--suffix.two.coder.com",
|
||||
matchProxyName: "two",
|
||||
name: "MatchSuffix",
|
||||
testHostname: "something--suffix.two.coder.com",
|
||||
allowAccessURL: true,
|
||||
allowWildcardHost: true,
|
||||
matchProxyName: "two",
|
||||
},
|
||||
{
|
||||
name: "ValidateHostname/1",
|
||||
testHostname: ".*ne.coder.com",
|
||||
matchProxyName: "",
|
||||
name: "ValidateHostname/1",
|
||||
testHostname: ".*ne.coder.com",
|
||||
allowAccessURL: true,
|
||||
allowWildcardHost: true,
|
||||
matchProxyName: "",
|
||||
},
|
||||
{
|
||||
name: "ValidateHostname/2",
|
||||
testHostname: "https://one.coder.com",
|
||||
matchProxyName: "",
|
||||
name: "ValidateHostname/2",
|
||||
testHostname: "https://one.coder.com",
|
||||
allowAccessURL: true,
|
||||
allowWildcardHost: true,
|
||||
matchProxyName: "",
|
||||
},
|
||||
{
|
||||
name: "ValidateHostname/3",
|
||||
testHostname: "one.coder.com:8080/hello",
|
||||
matchProxyName: "",
|
||||
name: "ValidateHostname/3",
|
||||
testHostname: "one.coder.com:8080/hello",
|
||||
allowAccessURL: true,
|
||||
allowWildcardHost: true,
|
||||
matchProxyName: "",
|
||||
},
|
||||
{
|
||||
name: "IgnoreAccessURLMatch",
|
||||
testHostname: "one.coder.com",
|
||||
allowAccessURL: false,
|
||||
allowWildcardHost: true,
|
||||
matchProxyName: "",
|
||||
},
|
||||
{
|
||||
name: "IgnoreWildcardMatch",
|
||||
testHostname: "hi.wildcard.one.coder.com",
|
||||
allowAccessURL: true,
|
||||
allowWildcardHost: false,
|
||||
matchProxyName: "",
|
||||
},
|
||||
}
|
||||
|
||||
@ -206,7 +236,11 @@ func TestProxyByHostname(t *testing.T) {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
proxy, err := db.GetWorkspaceProxyByHostname(context.Background(), c.testHostname)
|
||||
proxy, err := db.GetWorkspaceProxyByHostname(context.Background(), database.GetWorkspaceProxyByHostnameParams{
|
||||
Hostname: c.testHostname,
|
||||
AllowAccessUrl: c.allowAccessURL,
|
||||
AllowWildcardHostname: c.allowWildcardHost,
|
||||
})
|
||||
if c.matchProxyName == "" {
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
require.Empty(t, proxy)
|
||||
|
@ -156,7 +156,7 @@ type sqlcQuerier interface {
|
||||
// The hostname must be sanitized to only contain [a-zA-Z0-9.-] before calling
|
||||
// this query. The scheme, port and path should be stripped.
|
||||
//
|
||||
GetWorkspaceProxyByHostname(ctx context.Context, hostname string) (WorkspaceProxy, error)
|
||||
GetWorkspaceProxyByHostname(ctx context.Context, arg GetWorkspaceProxyByHostnameParams) (WorkspaceProxy, error)
|
||||
GetWorkspaceProxyByID(ctx context.Context, id uuid.UUID) (WorkspaceProxy, error)
|
||||
GetWorkspaceProxyByName(ctx context.Context, name string) (WorkspaceProxy, error)
|
||||
GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error)
|
||||
|
@ -165,44 +165,74 @@ func TestProxyByHostname(t *testing.T) {
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
testHostname string
|
||||
matchProxyName string
|
||||
name string
|
||||
testHostname string
|
||||
allowAccessURL bool
|
||||
allowWildcardHost bool
|
||||
matchProxyName string
|
||||
}{
|
||||
{
|
||||
name: "NoMatch",
|
||||
testHostname: "test.com",
|
||||
matchProxyName: "",
|
||||
name: "NoMatch",
|
||||
testHostname: "test.com",
|
||||
allowAccessURL: true,
|
||||
allowWildcardHost: true,
|
||||
matchProxyName: "",
|
||||
},
|
||||
{
|
||||
name: "MatchAccessURL",
|
||||
testHostname: "one.coder.com",
|
||||
matchProxyName: "one",
|
||||
name: "MatchAccessURL",
|
||||
testHostname: "one.coder.com",
|
||||
allowAccessURL: true,
|
||||
allowWildcardHost: true,
|
||||
matchProxyName: "one",
|
||||
},
|
||||
{
|
||||
name: "MatchWildcard",
|
||||
testHostname: "something.wildcard.one.coder.com",
|
||||
matchProxyName: "one",
|
||||
name: "MatchWildcard",
|
||||
testHostname: "something.wildcard.one.coder.com",
|
||||
allowAccessURL: true,
|
||||
allowWildcardHost: true,
|
||||
matchProxyName: "one",
|
||||
},
|
||||
{
|
||||
name: "MatchSuffix",
|
||||
testHostname: "something--suffix.two.coder.com",
|
||||
matchProxyName: "two",
|
||||
name: "MatchSuffix",
|
||||
testHostname: "something--suffix.two.coder.com",
|
||||
allowAccessURL: true,
|
||||
allowWildcardHost: true,
|
||||
matchProxyName: "two",
|
||||
},
|
||||
{
|
||||
name: "ValidateHostname/1",
|
||||
testHostname: ".*ne.coder.com",
|
||||
matchProxyName: "",
|
||||
name: "ValidateHostname/1",
|
||||
testHostname: ".*ne.coder.com",
|
||||
allowAccessURL: true,
|
||||
allowWildcardHost: true,
|
||||
matchProxyName: "",
|
||||
},
|
||||
{
|
||||
name: "ValidateHostname/2",
|
||||
testHostname: "https://one.coder.com",
|
||||
matchProxyName: "",
|
||||
name: "ValidateHostname/2",
|
||||
testHostname: "https://one.coder.com",
|
||||
allowAccessURL: true,
|
||||
allowWildcardHost: true,
|
||||
matchProxyName: "",
|
||||
},
|
||||
{
|
||||
name: "ValidateHostname/3",
|
||||
testHostname: "one.coder.com:8080/hello",
|
||||
matchProxyName: "",
|
||||
name: "ValidateHostname/3",
|
||||
testHostname: "one.coder.com:8080/hello",
|
||||
allowAccessURL: true,
|
||||
allowWildcardHost: true,
|
||||
matchProxyName: "",
|
||||
},
|
||||
{
|
||||
name: "IgnoreAccessURLMatch",
|
||||
testHostname: "one.coder.com",
|
||||
allowAccessURL: false,
|
||||
allowWildcardHost: true,
|
||||
matchProxyName: "",
|
||||
},
|
||||
{
|
||||
name: "IgnoreWildcardMatch",
|
||||
testHostname: "hi.wildcard.one.coder.com",
|
||||
allowAccessURL: true,
|
||||
allowWildcardHost: false,
|
||||
matchProxyName: "",
|
||||
},
|
||||
}
|
||||
|
||||
@ -211,7 +241,11 @@ func TestProxyByHostname(t *testing.T) {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
proxy, err := db.GetWorkspaceProxyByHostname(context.Background(), c.testHostname)
|
||||
proxy, err := db.GetWorkspaceProxyByHostname(context.Background(), database.GetWorkspaceProxyByHostnameParams{
|
||||
Hostname: c.testHostname,
|
||||
AllowAccessUrl: c.allowAccessURL,
|
||||
AllowWildcardHostname: c.allowWildcardHost,
|
||||
})
|
||||
if c.matchProxyName == "" {
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
require.Empty(t, proxy)
|
||||
|
@ -2871,27 +2871,39 @@ WHERE
|
||||
--
|
||||
-- Periods don't need to be escaped because they're not special characters
|
||||
-- in SQL matches unlike regular expressions.
|
||||
$1 :: text SIMILAR TO '[a-zA-Z0-9.-]+' AND
|
||||
$1 :: text SIMILAR TO '[a-zA-Z0-9._-]+' AND
|
||||
deleted = false AND
|
||||
|
||||
-- Validate that the hostname matches either the wildcard hostname or the
|
||||
-- access URL (ignoring scheme, port and path).
|
||||
(
|
||||
url SIMILAR TO '[^:]*://' || $1 :: text || '([:/]?%)*' OR
|
||||
$1 :: text LIKE replace(wildcard_hostname, '*', '%')
|
||||
(
|
||||
$2 :: bool = true AND
|
||||
url SIMILAR TO '[^:]*://' || $1 :: text || '([:/]?%)*'
|
||||
) OR
|
||||
(
|
||||
$3 :: bool = true AND
|
||||
$1 :: text LIKE replace(wildcard_hostname, '*', '%')
|
||||
)
|
||||
)
|
||||
LIMIT
|
||||
1
|
||||
`
|
||||
|
||||
type GetWorkspaceProxyByHostnameParams struct {
|
||||
Hostname string `db:"hostname" json:"hostname"`
|
||||
AllowAccessUrl bool `db:"allow_access_url" json:"allow_access_url"`
|
||||
AllowWildcardHostname bool `db:"allow_wildcard_hostname" json:"allow_wildcard_hostname"`
|
||||
}
|
||||
|
||||
// Finds a workspace proxy that has an access URL or app hostname that matches
|
||||
// the provided hostname. This is to check if a hostname matches any workspace
|
||||
// proxy.
|
||||
//
|
||||
// The hostname must be sanitized to only contain [a-zA-Z0-9.-] before calling
|
||||
// this query. The scheme, port and path should be stripped.
|
||||
func (q *sqlQuerier) GetWorkspaceProxyByHostname(ctx context.Context, hostname string) (WorkspaceProxy, error) {
|
||||
row := q.db.QueryRowContext(ctx, getWorkspaceProxyByHostname, hostname)
|
||||
func (q *sqlQuerier) GetWorkspaceProxyByHostname(ctx context.Context, arg GetWorkspaceProxyByHostnameParams) (WorkspaceProxy, error) {
|
||||
row := q.db.QueryRowContext(ctx, getWorkspaceProxyByHostname, arg.Hostname, arg.AllowAccessUrl, arg.AllowWildcardHostname)
|
||||
var i WorkspaceProxy
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
|
@ -77,14 +77,20 @@ WHERE
|
||||
--
|
||||
-- Periods don't need to be escaped because they're not special characters
|
||||
-- in SQL matches unlike regular expressions.
|
||||
@hostname :: text SIMILAR TO '[a-zA-Z0-9.-]+' AND
|
||||
@hostname :: text SIMILAR TO '[a-zA-Z0-9._-]+' AND
|
||||
deleted = false AND
|
||||
|
||||
-- Validate that the hostname matches either the wildcard hostname or the
|
||||
-- access URL (ignoring scheme, port and path).
|
||||
(
|
||||
url SIMILAR TO '[^:]*://' || @hostname :: text || '([:/]?%)*' OR
|
||||
@hostname :: text LIKE replace(wildcard_hostname, '*', '%')
|
||||
(
|
||||
@allow_access_url :: bool = true AND
|
||||
url SIMILAR TO '[^:]*://' || @hostname :: text || '([:/]?%)*'
|
||||
) OR
|
||||
(
|
||||
@allow_wildcard_hostname :: bool = true AND
|
||||
@hostname :: text LIKE replace(wildcard_hostname, '*', '%')
|
||||
)
|
||||
)
|
||||
LIMIT
|
||||
1;
|
||||
|
Reference in New Issue
Block a user