mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: add workspace proxies to the backend (#7032)
Co-authored-by: Dean Sheather <dean@deansheather.com>
This commit is contained in:
co-authored by
Dean Sheather
parent
dc5e16ae22
commit
658246d5f2
@@ -180,6 +180,7 @@ var (
|
||||
rbac.ResourceUser.Type: {rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
|
||||
rbac.ResourceUserData.Type: {rbac.ActionCreate, rbac.ActionUpdate},
|
||||
rbac.ResourceWorkspace.Type: {rbac.ActionUpdate},
|
||||
rbac.ResourceWorkspaceExecution.Type: {rbac.ActionCreate},
|
||||
}),
|
||||
Org: map[string][]rbac.Permission{},
|
||||
User: []rbac.Permission{},
|
||||
|
||||
@@ -1697,6 +1697,10 @@ func (q *querier) GetWorkspaceProxyByID(ctx context.Context, id uuid.UUID) (data
|
||||
return fetch(q.log, q.auth, q.db.GetWorkspaceProxyByID)(ctx, id)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -445,25 +445,25 @@ func (s *MethodTestSuite) TestWorkspaceProxy() {
|
||||
}).Asserts(rbac.ResourceWorkspaceProxy, rbac.ActionCreate)
|
||||
}))
|
||||
s.Run("UpdateWorkspaceProxy", s.Subtest(func(db database.Store, check *expects) {
|
||||
p := dbgen.WorkspaceProxy(s.T(), db, database.WorkspaceProxy{})
|
||||
p, _ := dbgen.WorkspaceProxy(s.T(), db, database.WorkspaceProxy{})
|
||||
check.Args(database.UpdateWorkspaceProxyParams{
|
||||
ID: p.ID,
|
||||
}).Asserts(p, rbac.ActionUpdate)
|
||||
}))
|
||||
s.Run("GetWorkspaceProxyByID", s.Subtest(func(db database.Store, check *expects) {
|
||||
p := dbgen.WorkspaceProxy(s.T(), db, database.WorkspaceProxy{})
|
||||
p, _ := dbgen.WorkspaceProxy(s.T(), db, database.WorkspaceProxy{})
|
||||
check.Args(p.ID).Asserts(p, rbac.ActionRead).Returns(p)
|
||||
}))
|
||||
s.Run("UpdateWorkspaceProxyDeleted", s.Subtest(func(db database.Store, check *expects) {
|
||||
p := dbgen.WorkspaceProxy(s.T(), db, database.WorkspaceProxy{})
|
||||
p, _ := dbgen.WorkspaceProxy(s.T(), db, database.WorkspaceProxy{})
|
||||
check.Args(database.UpdateWorkspaceProxyDeletedParams{
|
||||
ID: p.ID,
|
||||
Deleted: true,
|
||||
}).Asserts(p, rbac.ActionDelete)
|
||||
}))
|
||||
s.Run("GetWorkspaceProxies", s.Subtest(func(db database.Store, check *expects) {
|
||||
p1 := dbgen.WorkspaceProxy(s.T(), db, database.WorkspaceProxy{})
|
||||
p2 := dbgen.WorkspaceProxy(s.T(), db, database.WorkspaceProxy{})
|
||||
p1, _ := dbgen.WorkspaceProxy(s.T(), db, database.WorkspaceProxy{})
|
||||
p2, _ := dbgen.WorkspaceProxy(s.T(), db, database.WorkspaceProxy{})
|
||||
check.Args().Asserts(p1, rbac.ActionRead, p2, rbac.ActionRead).Returns(slice.New(p1, p2))
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,10 +19,13 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/coderd/util/slice"
|
||||
)
|
||||
|
||||
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
|
||||
// by unit tests.
|
||||
@@ -5093,6 +5097,40 @@ func (q *fakeQuerier) GetWorkspaceProxyByID(_ context.Context, id uuid.UUID) (da
|
||||
return database.WorkspaceProxy{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetWorkspaceProxyByHostname(_ context.Context, hostname string) (database.WorkspaceProxy, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
// 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) {
|
||||
return database.WorkspaceProxy{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
// This regex matches the SQL version.
|
||||
accessURLRegex := regexp.MustCompile(`[^:]*://` + regexp.QuoteMeta(hostname) + `([:/]?.)*`)
|
||||
|
||||
for _, proxy := range q.workspaceProxies {
|
||||
if proxy.Deleted {
|
||||
continue
|
||||
}
|
||||
if 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
|
||||
}
|
||||
}
|
||||
|
||||
return database.WorkspaceProxy{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) InsertWorkspaceProxy(_ context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
@@ -5104,14 +5142,16 @@ func (q *fakeQuerier) InsertWorkspaceProxy(_ context.Context, arg database.Inser
|
||||
}
|
||||
|
||||
p := database.WorkspaceProxy{
|
||||
ID: arg.ID,
|
||||
Name: arg.Name,
|
||||
Icon: arg.Icon,
|
||||
Url: arg.Url,
|
||||
WildcardHostname: arg.WildcardHostname,
|
||||
CreatedAt: arg.CreatedAt,
|
||||
UpdatedAt: arg.UpdatedAt,
|
||||
Deleted: false,
|
||||
ID: arg.ID,
|
||||
Name: arg.Name,
|
||||
DisplayName: arg.DisplayName,
|
||||
Icon: arg.Icon,
|
||||
Url: arg.Url,
|
||||
WildcardHostname: arg.WildcardHostname,
|
||||
TokenHashedSecret: arg.TokenHashedSecret,
|
||||
CreatedAt: arg.CreatedAt,
|
||||
UpdatedAt: arg.UpdatedAt,
|
||||
Deleted: false,
|
||||
}
|
||||
q.workspaceProxies = append(q.workspaceProxies, p)
|
||||
return p, nil
|
||||
|
||||
@@ -129,6 +129,96 @@ func TestUserOrder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyByHostname(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := dbfake.New()
|
||||
|
||||
// Insert a bunch of different proxies.
|
||||
proxies := []struct {
|
||||
name string
|
||||
accessURL string
|
||||
wildcardHostname string
|
||||
}{
|
||||
{
|
||||
name: "one",
|
||||
accessURL: "https://one.coder.com",
|
||||
wildcardHostname: "*.wildcard.one.coder.com",
|
||||
},
|
||||
{
|
||||
name: "two",
|
||||
accessURL: "https://two.coder.com",
|
||||
wildcardHostname: "*--suffix.two.coder.com",
|
||||
},
|
||||
}
|
||||
for _, p := range proxies {
|
||||
dbgen.WorkspaceProxy(t, db, database.WorkspaceProxy{
|
||||
Name: p.name,
|
||||
Url: p.accessURL,
|
||||
WildcardHostname: p.wildcardHostname,
|
||||
})
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
testHostname string
|
||||
matchProxyName string
|
||||
}{
|
||||
{
|
||||
name: "NoMatch",
|
||||
testHostname: "test.com",
|
||||
matchProxyName: "",
|
||||
},
|
||||
{
|
||||
name: "MatchAccessURL",
|
||||
testHostname: "one.coder.com",
|
||||
matchProxyName: "one",
|
||||
},
|
||||
{
|
||||
name: "MatchWildcard",
|
||||
testHostname: "something.wildcard.one.coder.com",
|
||||
matchProxyName: "one",
|
||||
},
|
||||
{
|
||||
name: "MatchSuffix",
|
||||
testHostname: "something--suffix.two.coder.com",
|
||||
matchProxyName: "two",
|
||||
},
|
||||
{
|
||||
name: "ValidateHostname/1",
|
||||
testHostname: ".*ne.coder.com",
|
||||
matchProxyName: "",
|
||||
},
|
||||
{
|
||||
name: "ValidateHostname/2",
|
||||
testHostname: "https://one.coder.com",
|
||||
matchProxyName: "",
|
||||
},
|
||||
{
|
||||
name: "ValidateHostname/3",
|
||||
testHostname: "one.coder.com:8080/hello",
|
||||
matchProxyName: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
c := c
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
proxy, err := db.GetWorkspaceProxyByHostname(context.Background(), c.testHostname)
|
||||
if c.matchProxyName == "" {
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
require.Empty(t, proxy)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, proxy)
|
||||
require.Equal(t, c.matchProxyName, proxy.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func methods(rt reflect.Type) map[string]bool {
|
||||
methods := make(map[string]bool)
|
||||
for i := 0; i < rt.NumMethod(); i++ {
|
||||
|
||||
@@ -338,19 +338,24 @@ func WorkspaceResourceMetadatums(t testing.TB, db database.Store, seed database.
|
||||
return meta
|
||||
}
|
||||
|
||||
func WorkspaceProxy(t testing.TB, db database.Store, orig database.WorkspaceProxy) database.WorkspaceProxy {
|
||||
func WorkspaceProxy(t testing.TB, db database.Store, orig database.WorkspaceProxy) (database.WorkspaceProxy, string) {
|
||||
secret, err := cryptorand.HexString(64)
|
||||
require.NoError(t, err, "generate secret")
|
||||
hashedSecret := sha256.Sum256([]byte(secret))
|
||||
|
||||
resource, err := db.InsertWorkspaceProxy(context.Background(), database.InsertWorkspaceProxyParams{
|
||||
ID: takeFirst(orig.ID, uuid.New()),
|
||||
Name: takeFirst(orig.Name, namesgenerator.GetRandomName(1)),
|
||||
DisplayName: takeFirst(orig.DisplayName, namesgenerator.GetRandomName(1)),
|
||||
Icon: takeFirst(orig.Icon, namesgenerator.GetRandomName(1)),
|
||||
Url: takeFirst(orig.Url, fmt.Sprintf("https://%s.com", namesgenerator.GetRandomName(1))),
|
||||
WildcardHostname: takeFirst(orig.WildcardHostname, fmt.Sprintf(".%s.com", namesgenerator.GetRandomName(1))),
|
||||
CreatedAt: takeFirst(orig.CreatedAt, database.Now()),
|
||||
UpdatedAt: takeFirst(orig.UpdatedAt, database.Now()),
|
||||
ID: takeFirst(orig.ID, uuid.New()),
|
||||
Name: takeFirst(orig.Name, namesgenerator.GetRandomName(1)),
|
||||
DisplayName: takeFirst(orig.DisplayName, namesgenerator.GetRandomName(1)),
|
||||
Icon: takeFirst(orig.Icon, namesgenerator.GetRandomName(1)),
|
||||
Url: takeFirst(orig.Url, fmt.Sprintf("https://%s.com", namesgenerator.GetRandomName(1))),
|
||||
WildcardHostname: takeFirst(orig.WildcardHostname, fmt.Sprintf("*.%s.com", namesgenerator.GetRandomName(1))),
|
||||
TokenHashedSecret: hashedSecret[:],
|
||||
CreatedAt: takeFirst(orig.CreatedAt, database.Now()),
|
||||
UpdatedAt: takeFirst(orig.UpdatedAt, database.Now()),
|
||||
})
|
||||
require.NoError(t, err, "insert app")
|
||||
return resource
|
||||
require.NoError(t, err, "insert proxy")
|
||||
return resource, secret
|
||||
}
|
||||
|
||||
func File(t testing.TB, db database.Store, orig database.File) database.File {
|
||||
|
||||
@@ -78,7 +78,8 @@ func TestGenerator(t *testing.T) {
|
||||
t.Run("WorkspaceProxy", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := dbfake.New()
|
||||
exp := dbgen.WorkspaceProxy(t, db, database.WorkspaceProxy{})
|
||||
exp, secret := dbgen.WorkspaceProxy(t, db, database.WorkspaceProxy{})
|
||||
require.Len(t, secret, 64)
|
||||
require.Equal(t, exp, must(db.GetWorkspaceProxyByID(context.Background(), exp.ID)))
|
||||
})
|
||||
|
||||
|
||||
Generated
+8
-1
@@ -647,13 +647,20 @@ CREATE TABLE workspace_proxies (
|
||||
wildcard_hostname text NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
deleted boolean NOT NULL
|
||||
deleted boolean NOT NULL,
|
||||
token_hashed_secret bytea NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN workspace_proxies.icon IS 'Expects an emoji character. (/emojis/1f1fa-1f1f8.png)';
|
||||
|
||||
COMMENT ON COLUMN workspace_proxies.url IS 'Full url including scheme of the proxy api url: https://us.example.com';
|
||||
|
||||
COMMENT ON COLUMN workspace_proxies.wildcard_hostname IS 'Hostname with the wildcard for subdomain based app hosting: *.us.example.com';
|
||||
|
||||
COMMENT ON COLUMN workspace_proxies.deleted IS 'Boolean indicator of a deleted workspace proxy. Proxies are soft-deleted.';
|
||||
|
||||
COMMENT ON COLUMN workspace_proxies.token_hashed_secret IS 'Hashed secret is used to authenticate the workspace proxy using a session token.';
|
||||
|
||||
CREATE TABLE workspace_resource_metadata (
|
||||
workspace_resource_id uuid NOT NULL,
|
||||
key character varying(1024) NOT NULL,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE workspace_proxies
|
||||
DROP COLUMN token_hashed_secret;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,22 @@
|
||||
BEGIN;
|
||||
|
||||
-- It's difficult to generate tokens for existing proxies, so we'll just delete
|
||||
-- them if they exist.
|
||||
--
|
||||
-- No one is using this feature yet as of writing this migration, so this is
|
||||
-- fine.
|
||||
DELETE FROM workspace_proxies;
|
||||
|
||||
ALTER TABLE workspace_proxies
|
||||
ADD COLUMN token_hashed_secret bytea NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN workspace_proxies.token_hashed_secret
|
||||
IS 'Hashed secret is used to authenticate the workspace proxy using a session token.';
|
||||
|
||||
COMMENT ON COLUMN workspace_proxies.deleted
|
||||
IS 'Boolean indicator of a deleted workspace proxy. Proxies are soft-deleted.';
|
||||
|
||||
COMMENT ON COLUMN workspace_proxies.icon
|
||||
IS 'Expects an emoji character. (/emojis/1f1fa-1f1f8.png)';
|
||||
|
||||
COMMIT;
|
||||
@@ -1,14 +0,0 @@
|
||||
INSERT INTO workspace_proxies
|
||||
(id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted)
|
||||
VALUES
|
||||
(
|
||||
'cf8ede8c-ff47-441f-a738-d92e4e34a657',
|
||||
'us',
|
||||
'United States',
|
||||
'/emojis/us.png',
|
||||
'https://us.coder.com',
|
||||
'*.us.coder.com',
|
||||
'2023-03-30 12:00:00.000+02',
|
||||
'2023-03-30 12:00:00.000+02',
|
||||
false
|
||||
);
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
INSERT INTO workspace_proxies
|
||||
(id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret)
|
||||
VALUES
|
||||
(
|
||||
'cf8ede8c-ff47-441f-a738-d92e4e34a657',
|
||||
'us',
|
||||
'United States',
|
||||
'/emojis/us.png',
|
||||
'https://us.coder.com',
|
||||
'*.us.coder.com',
|
||||
'2023-03-30 12:00:00.000+02',
|
||||
'2023-03-30 12:00:00.000+02',
|
||||
false,
|
||||
'abc123'::bytea
|
||||
);
|
||||
@@ -1674,14 +1674,18 @@ type WorkspaceProxy struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
// Expects an emoji character. (/emojis/1f1fa-1f1f8.png)
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
// Full url including scheme of the proxy api url: https://us.example.com
|
||||
Url string `db:"url" json:"url"`
|
||||
// Hostname with the wildcard for subdomain based app hosting: *.us.example.com
|
||||
WildcardHostname string `db:"wildcard_hostname" json:"wildcard_hostname"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
// Boolean indicator of a deleted workspace proxy. Proxies are soft-deleted.
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
// Hashed secret is used to authenticate the workspace proxy using a session token.
|
||||
TokenHashedSecret []byte `db:"token_hashed_secret" json:"token_hashed_secret"`
|
||||
}
|
||||
|
||||
type WorkspaceResource struct {
|
||||
|
||||
@@ -149,6 +149,14 @@ type sqlcQuerier interface {
|
||||
GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error)
|
||||
GetWorkspaceByWorkspaceAppID(ctx context.Context, workspaceAppID uuid.UUID) (Workspace, error)
|
||||
GetWorkspaceProxies(ctx context.Context) ([]WorkspaceProxy, error)
|
||||
// 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.
|
||||
//
|
||||
GetWorkspaceProxyByHostname(ctx context.Context, hostname string) (WorkspaceProxy, error)
|
||||
GetWorkspaceProxyByID(ctx context.Context, id uuid.UUID) (WorkspaceProxy, error)
|
||||
GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error)
|
||||
GetWorkspaceResourceMetadataByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResourceMetadatum, error)
|
||||
|
||||
@@ -4,6 +4,7 @@ package database_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -127,3 +128,98 @@ func TestInsertWorkspaceAgentStartupLogs(t *testing.T) {
|
||||
})
|
||||
require.True(t, database.IsStartupLogsLimitError(err))
|
||||
}
|
||||
|
||||
func TestProxyByHostname(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
sqlDB := testSQLDB(t)
|
||||
err := migrations.Up(sqlDB)
|
||||
require.NoError(t, err)
|
||||
db := database.New(sqlDB)
|
||||
|
||||
// Insert a bunch of different proxies.
|
||||
proxies := []struct {
|
||||
name string
|
||||
accessURL string
|
||||
wildcardHostname string
|
||||
}{
|
||||
{
|
||||
name: "one",
|
||||
accessURL: "https://one.coder.com",
|
||||
wildcardHostname: "*.wildcard.one.coder.com",
|
||||
},
|
||||
{
|
||||
name: "two",
|
||||
accessURL: "https://two.coder.com",
|
||||
wildcardHostname: "*--suffix.two.coder.com",
|
||||
},
|
||||
}
|
||||
for _, p := range proxies {
|
||||
dbgen.WorkspaceProxy(t, db, database.WorkspaceProxy{
|
||||
Name: p.name,
|
||||
Url: p.accessURL,
|
||||
WildcardHostname: p.wildcardHostname,
|
||||
})
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
testHostname string
|
||||
matchProxyName string
|
||||
}{
|
||||
{
|
||||
name: "NoMatch",
|
||||
testHostname: "test.com",
|
||||
matchProxyName: "",
|
||||
},
|
||||
{
|
||||
name: "MatchAccessURL",
|
||||
testHostname: "one.coder.com",
|
||||
matchProxyName: "one",
|
||||
},
|
||||
{
|
||||
name: "MatchWildcard",
|
||||
testHostname: "something.wildcard.one.coder.com",
|
||||
matchProxyName: "one",
|
||||
},
|
||||
{
|
||||
name: "MatchSuffix",
|
||||
testHostname: "something--suffix.two.coder.com",
|
||||
matchProxyName: "two",
|
||||
},
|
||||
{
|
||||
name: "ValidateHostname/1",
|
||||
testHostname: ".*ne.coder.com",
|
||||
matchProxyName: "",
|
||||
},
|
||||
{
|
||||
name: "ValidateHostname/2",
|
||||
testHostname: "https://one.coder.com",
|
||||
matchProxyName: "",
|
||||
},
|
||||
{
|
||||
name: "ValidateHostname/3",
|
||||
testHostname: "one.coder.com:8080/hello",
|
||||
matchProxyName: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
c := c
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
proxy, err := db.GetWorkspaceProxyByHostname(context.Background(), c.testHostname)
|
||||
if c.matchProxyName == "" {
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
require.Empty(t, proxy)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, proxy)
|
||||
require.Equal(t, c.matchProxyName, proxy.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2817,7 +2817,7 @@ func (q *sqlQuerier) UpdateProvisionerJobWithCompleteByID(ctx context.Context, a
|
||||
|
||||
const getWorkspaceProxies = `-- name: GetWorkspaceProxies :many
|
||||
SELECT
|
||||
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted
|
||||
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret
|
||||
FROM
|
||||
workspace_proxies
|
||||
WHERE
|
||||
@@ -2843,6 +2843,7 @@ func (q *sqlQuerier) GetWorkspaceProxies(ctx context.Context) ([]WorkspaceProxy,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Deleted,
|
||||
&i.TokenHashedSecret,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2857,9 +2858,59 @@ func (q *sqlQuerier) GetWorkspaceProxies(ctx context.Context) ([]WorkspaceProxy,
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspaceProxyByHostname = `-- name: GetWorkspaceProxyByHostname :one
|
||||
SELECT
|
||||
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret
|
||||
FROM
|
||||
workspace_proxies
|
||||
WHERE
|
||||
-- Validate that the @hostname has been sanitized and is not empty. This
|
||||
-- doesn't prevent SQL injection (already prevented by using prepared
|
||||
-- queries), but it does prevent carefully crafted hostnames from matching
|
||||
-- when they shouldn't.
|
||||
--
|
||||
-- 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
|
||||
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, '*', '%')
|
||||
)
|
||||
LIMIT
|
||||
1
|
||||
`
|
||||
|
||||
// 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)
|
||||
var i WorkspaceProxy
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Icon,
|
||||
&i.Url,
|
||||
&i.WildcardHostname,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Deleted,
|
||||
&i.TokenHashedSecret,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getWorkspaceProxyByID = `-- name: GetWorkspaceProxyByID :one
|
||||
SELECT
|
||||
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted
|
||||
id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret
|
||||
FROM
|
||||
workspace_proxies
|
||||
WHERE
|
||||
@@ -2881,6 +2932,7 @@ func (q *sqlQuerier) GetWorkspaceProxyByID(ctx context.Context, id uuid.UUID) (W
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Deleted,
|
||||
&i.TokenHashedSecret,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -2894,23 +2946,25 @@ INSERT INTO
|
||||
icon,
|
||||
url,
|
||||
wildcard_hostname,
|
||||
token_hashed_secret,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, false) RETURNING id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, false) RETURNING id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret
|
||||
`
|
||||
|
||||
type InsertWorkspaceProxyParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
Url string `db:"url" json:"url"`
|
||||
WildcardHostname string `db:"wildcard_hostname" json:"wildcard_hostname"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
Url string `db:"url" json:"url"`
|
||||
WildcardHostname string `db:"wildcard_hostname" json:"wildcard_hostname"`
|
||||
TokenHashedSecret []byte `db:"token_hashed_secret" json:"token_hashed_secret"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error) {
|
||||
@@ -2921,6 +2975,7 @@ func (q *sqlQuerier) InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspa
|
||||
arg.Icon,
|
||||
arg.Url,
|
||||
arg.WildcardHostname,
|
||||
arg.TokenHashedSecret,
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
)
|
||||
@@ -2935,6 +2990,7 @@ func (q *sqlQuerier) InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspa
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Deleted,
|
||||
&i.TokenHashedSecret,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -2951,7 +3007,7 @@ SET
|
||||
updated_at = Now()
|
||||
WHERE
|
||||
id = $6
|
||||
RETURNING id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted
|
||||
RETURNING id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret
|
||||
`
|
||||
|
||||
type UpdateWorkspaceProxyParams struct {
|
||||
@@ -2983,6 +3039,7 @@ func (q *sqlQuerier) UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspa
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Deleted,
|
||||
&i.TokenHashedSecret,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ INSERT INTO
|
||||
icon,
|
||||
url,
|
||||
wildcard_hostname,
|
||||
token_hashed_secret,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, false) RETURNING *;
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, false) RETURNING *;
|
||||
|
||||
-- name: UpdateWorkspaceProxy :one
|
||||
UPDATE
|
||||
@@ -48,6 +49,38 @@ WHERE
|
||||
LIMIT
|
||||
1;
|
||||
|
||||
-- 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.
|
||||
--
|
||||
-- name: GetWorkspaceProxyByHostname :one
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
workspace_proxies
|
||||
WHERE
|
||||
-- Validate that the @hostname has been sanitized and is not empty. This
|
||||
-- doesn't prevent SQL injection (already prevented by using prepared
|
||||
-- queries), but it does prevent carefully crafted hostnames from matching
|
||||
-- when they shouldn't.
|
||||
--
|
||||
-- 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
|
||||
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, '*', '%')
|
||||
)
|
||||
LIMIT
|
||||
1;
|
||||
|
||||
-- name: GetWorkspaceProxies :many
|
||||
SELECT
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user