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:
Dean Sheather
2023-04-20 16:59:45 -07:00
committed by GitHub
parent ac3c530283
commit 68667323f3
25 changed files with 886 additions and 164 deletions

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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;