Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit eb66cc9

Browse files
deansheatherEmyrk
andauthored
chore: move app proxying code to workspaceapps pkg (coder#6998)
* chore: move app proxying code to workspaceapps pkg Moves path-app, subdomain-app and reconnecting PTY proxying to the new workspaceapps.WorkspaceAppServer struct. This is in preparation for external workspace proxies. Updates app logout flow to avoid redirecting to coder-logout.${app_host} on logout. Instead, all subdomain app tokens owned by the logging-out user will be deleted every time you logout for simplicity sake. Tests will remain in their original package, pending being moved to an apptest package (or similar). Co-authored-by: Steven Masley <[email protected]>
1 parent 0069831 commit eb66cc9

28 files changed

+1236
-1334
lines changed

cli/server.go

+21-15
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import (
7878
"github.com/coder/coder/coderd/tracing"
7979
"github.com/coder/coder/coderd/updatecheck"
8080
"github.com/coder/coder/coderd/util/slice"
81+
"github.com/coder/coder/coderd/workspaceapps"
8182
"github.com/coder/coder/codersdk"
8283
"github.com/coder/coder/cryptorand"
8384
"github.com/coder/coder/provisioner/echo"
@@ -781,37 +782,42 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
781782
}
782783
}
783784

784-
// Read the app signing key from the DB. We store it hex
785-
// encoded since the config table uses strings for the value and
786-
// we don't want to deal with automatic encoding issues.
787-
appSigningKeyStr, err := tx.GetAppSigningKey(ctx)
785+
// Read the app signing key from the DB. We store it hex encoded
786+
// since the config table uses strings for the value and we
787+
// don't want to deal with automatic encoding issues.
788+
appSecurityKeyStr, err := tx.GetAppSecurityKey(ctx)
788789
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
789790
return xerrors.Errorf("get app signing key: %w", err)
790791
}
791-
if appSigningKeyStr == "" {
792-
// Generate 64 byte secure random string.
793-
b := make([]byte, 64)
792+
// If the string in the DB is an invalid hex string or the
793+
// length is not equal to the current key length, generate a new
794+
// one.
795+
//
796+
// If the key is regenerated, old signed tokens and encrypted
797+
// strings will become invalid. New signed app tokens will be
798+
// generated automatically on failure. Any workspace app token
799+
// smuggling operations in progress may fail, although with a
800+
// helpful error.
801+
if decoded, err := hex.DecodeString(appSecurityKeyStr); err != nil || len(decoded) != len(workspaceapps.SecurityKey{}) {
802+
b := make([]byte, len(workspaceapps.SecurityKey{}))
794803
_, err := rand.Read(b)
795804
if err != nil {
796805
return xerrors.Errorf("generate fresh app signing key: %w", err)
797806
}
798807

799-
appSigningKeyStr = hex.EncodeToString(b)
800-
err = tx.InsertAppSigningKey(ctx, appSigningKeyStr)
808+
appSecurityKeyStr = hex.EncodeToString(b)
809+
err = tx.UpsertAppSecurityKey(ctx, appSecurityKeyStr)
801810
if err != nil {
802811
return xerrors.Errorf("insert freshly generated app signing key to database: %w", err)
803812
}
804813
}
805814

806-
appSigningKey, err := hex.DecodeString(appSigningKeyStr)
815+
appSecurityKey, err := workspaceapps.KeyFromString(appSecurityKeyStr)
807816
if err != nil {
808-
return xerrors.Errorf("decode app signing key from database as hex: %w", err)
809-
}
810-
if len(appSigningKey) != 64 {
811-
return xerrors.Errorf("app signing key must be 64 bytes, key in database is %d bytes", len(appSigningKey))
817+
return xerrors.Errorf("decode app signing key from database: %w", err)
812818
}
813819

814-
options.AppSigningKey = appSigningKey
820+
options.AppSecurityKey = appSecurityKey
815821
return nil
816822
}, nil)
817823
if err != nil {

cli/server_test.go

+10-7
Original file line numberDiff line numberDiff line change
@@ -668,8 +668,7 @@ func TestServer(t *testing.T) {
668668
if c.tlsListener {
669669
accessURLParsed, err := url.Parse(c.requestURL)
670670
require.NoError(t, err)
671-
client := codersdk.New(accessURLParsed)
672-
client.HTTPClient = &http.Client{
671+
client := &http.Client{
673672
CheckRedirect: func(req *http.Request, via []*http.Request) error {
674673
return http.ErrUseLastResponse
675674
},
@@ -682,11 +681,15 @@ func TestServer(t *testing.T) {
682681
},
683682
},
684683
}
685-
defer client.HTTPClient.CloseIdleConnections()
686-
_, err = client.HasFirstUser(ctx)
687-
if err != nil {
688-
require.ErrorContains(t, err, "Invalid application URL")
689-
}
684+
defer client.CloseIdleConnections()
685+
686+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, accessURLParsed.String(), nil)
687+
require.NoError(t, err)
688+
resp, err := client.Do(req)
689+
// We don't care much about the response, just that TLS
690+
// worked.
691+
require.NoError(t, err)
692+
defer resp.Body.Close()
690693
}
691694
})
692695
}

coderd/coderd.go

+32-24
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,9 @@ type Options struct {
123123
SwaggerEndpoint bool
124124
SetUserGroups func(ctx context.Context, tx database.Store, userID uuid.UUID, groupNames []string) error
125125
TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
126-
// AppSigningKey denotes the symmetric key to use for signing temporary app
127-
// tokens. The key must be 64 bytes long.
128-
AppSigningKey []byte
126+
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to
127+
// workspace applications. It consists of both a signing and encryption key.
128+
AppSecurityKey workspaceapps.SecurityKey
129129
HealthcheckFunc func(ctx context.Context) (*healthcheck.Report, error)
130130
HealthcheckTimeout time.Duration
131131
HealthcheckRefresh time.Duration
@@ -241,9 +241,6 @@ func New(options *Options) *API {
241241
v := schedule.NewAGPLTemplateScheduleStore()
242242
options.TemplateScheduleStore.Store(&v)
243243
}
244-
if len(options.AppSigningKey) != 64 {
245-
panic("coderd: AppSigningKey must be 64 bytes long")
246-
}
247244
if options.HealthcheckFunc == nil {
248245
options.HealthcheckFunc = func(ctx context.Context) (*healthcheck.Report, error) {
249246
return healthcheck.Run(ctx, &healthcheck.ReportOptions{
@@ -309,7 +306,7 @@ func New(options *Options) *API {
309306
options.DeploymentValues,
310307
oauthConfigs,
311308
options.AgentInactiveDisconnectTimeout,
312-
options.AppSigningKey,
309+
options.AppSecurityKey,
313310
),
314311
metricsCache: metricsCache,
315312
Auditor: atomic.Pointer[audit.Auditor]{},
@@ -334,6 +331,21 @@ func New(options *Options) *API {
334331
api.workspaceAgentCache = wsconncache.New(api.dialWorkspaceAgentTailnet, 0)
335332
api.TailnetCoordinator.Store(&options.TailnetCoordinator)
336333

334+
api.workspaceAppServer = &workspaceapps.Server{
335+
Logger: options.Logger.Named("workspaceapps"),
336+
337+
DashboardURL: api.AccessURL,
338+
AccessURL: api.AccessURL,
339+
Hostname: api.AppHostname,
340+
HostnameRegex: api.AppHostnameRegex,
341+
DeploymentValues: options.DeploymentValues,
342+
RealIPConfig: options.RealIPConfig,
343+
344+
SignedTokenProvider: api.WorkspaceAppsProvider,
345+
WorkspaceConnCache: api.workspaceAgentCache,
346+
AppSecurityKey: options.AppSecurityKey,
347+
}
348+
337349
apiKeyMiddleware := httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
338350
DB: options.Database,
339351
OAuth2Configs: oauthConfigs,
@@ -366,11 +378,12 @@ func New(options *Options) *API {
366378
httpmw.ExtractRealIP(api.RealIPConfig),
367379
httpmw.Logger(api.Logger),
368380
httpmw.Prometheus(options.PrometheusRegistry),
369-
// handleSubdomainApplications checks if the first subdomain is a valid
370-
// app URL. If it is, it will serve that application.
381+
// SubdomainAppMW checks if the first subdomain is a valid app URL. If
382+
// it is, it will serve that application.
371383
//
372-
// Workspace apps do their own auth.
373-
api.handleSubdomainApplications(apiRateLimiter),
384+
// Workspace apps do their own auth and must be BEFORE the auth
385+
// middleware.
386+
api.workspaceAppServer.SubdomainAppMW(apiRateLimiter),
374387
// Build-Version is helpful for debugging.
375388
func(next http.Handler) http.Handler {
376389
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -393,16 +406,12 @@ func New(options *Options) *API {
393406

394407
r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("OK")) })
395408

396-
apps := func(r chi.Router) {
397-
// Workspace apps do their own auth.
409+
// Attach workspace apps routes.
410+
r.Group(func(r chi.Router) {
398411
r.Use(apiRateLimiter)
399-
r.HandleFunc("/*", api.workspaceAppsProxyPath)
400-
}
401-
// %40 is the encoded character of the @ symbol. VS Code Web does
402-
// not handle character encoding properly, so it's safe to assume
403-
// other applications might not as well.
404-
r.Route("/%40{user}/{workspace_and_agent}/apps/{workspaceapp}", apps)
405-
r.Route("/@{user}/{workspace_and_agent}/apps/{workspaceapp}", apps)
412+
api.workspaceAppServer.Attach(r)
413+
})
414+
406415
r.Route("/derp", func(r chi.Router) {
407416
r.Get("/", derpHandler.ServeHTTP)
408417
// This is used when UDP is blocked, and latency must be checked via HTTP(s).
@@ -644,9 +653,6 @@ func New(options *Options) *API {
644653
r.Post("/report-lifecycle", api.workspaceAgentReportLifecycle)
645654
r.Post("/metadata/{key}", api.workspaceAgentPostMetadata)
646655
})
647-
// No middleware on the PTY endpoint since it uses workspace
648-
// application auth and signed app tokens.
649-
r.Get("/{workspaceagent}/pty", api.workspaceAgentPTY)
650656
r.Route("/{workspaceagent}", func(r chi.Router) {
651657
r.Use(
652658
apiKeyMiddleware,
@@ -655,11 +661,12 @@ func New(options *Options) *API {
655661
)
656662
r.Get("/", api.workspaceAgent)
657663
r.Get("/watch-metadata", api.watchWorkspaceAgentMetadata)
658-
r.Get("/pty", api.workspaceAgentPTY)
659664
r.Get("/startup-logs", api.workspaceAgentStartupLogs)
660665
r.Get("/listening-ports", api.workspaceAgentListeningPorts)
661666
r.Get("/connection", api.workspaceAgentConnection)
662667
r.Get("/coordinate", api.workspaceAgentClientCoordinate)
668+
669+
// PTY is part of workspaceAppServer.
663670
})
664671
})
665672
r.Route("/workspaces", func(r chi.Router) {
@@ -792,6 +799,7 @@ type API struct {
792799
workspaceAgentCache *wsconncache.Cache
793800
updateChecker *updatecheck.Checker
794801
WorkspaceAppsProvider workspaceapps.SignedTokenProvider
802+
workspaceAppServer *workspaceapps.Server
795803

796804
// Experiments contains the list of experiments currently enabled.
797805
// This is used to gate features that are not yet ready for production.

coderd/coderdtest/coderdtest.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"crypto/x509"
1212
"crypto/x509/pkix"
1313
"encoding/base64"
14-
"encoding/hex"
1514
"encoding/json"
1615
"encoding/pem"
1716
"errors"
@@ -69,6 +68,7 @@ import (
6968
"github.com/coder/coder/coderd/telemetry"
7069
"github.com/coder/coder/coderd/updatecheck"
7170
"github.com/coder/coder/coderd/util/ptr"
71+
"github.com/coder/coder/coderd/workspaceapps"
7272
"github.com/coder/coder/codersdk"
7373
"github.com/coder/coder/codersdk/agentsdk"
7474
"github.com/coder/coder/cryptorand"
@@ -81,9 +81,9 @@ import (
8181
"github.com/coder/coder/testutil"
8282
)
8383

84-
// AppSigningKey is a 64-byte key used to sign JWTs for workspace app tokens in
85-
// tests.
86-
var AppSigningKey = must(hex.DecodeString("64656164626565666465616462656566646561646265656664656164626565666465616462656566646561646265656664656164626565666465616462656566"))
84+
// AppSecurityKey is a 96-byte key used to sign JWTs and encrypt JWEs for
85+
// workspace app tokens in tests.
86+
var AppSecurityKey = must(workspaceapps.KeyFromString("6465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e2077617320686572"))
8787

8888
type Options struct {
8989
// AccessURL denotes a custom access URL. By default we use the httptest
@@ -346,7 +346,7 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can
346346
DeploymentValues: options.DeploymentValues,
347347
UpdateCheckOptions: options.UpdateCheckOptions,
348348
SwaggerEndpoint: options.SwaggerEndpoint,
349-
AppSigningKey: AppSigningKey,
349+
AppSecurityKey: AppSecurityKey,
350350
SSHConfig: options.ConfigSSH,
351351
HealthcheckFunc: options.HealthcheckFunc,
352352
HealthcheckTimeout: options.HealthcheckTimeout,

coderd/database/dbauthz/querier.go

+14-4
Original file line numberDiff line numberDiff line change
@@ -379,14 +379,14 @@ func (q *querier) GetLogoURL(ctx context.Context) (string, error) {
379379
return q.db.GetLogoURL(ctx)
380380
}
381381

382-
func (q *querier) GetAppSigningKey(ctx context.Context) (string, error) {
382+
func (q *querier) GetAppSecurityKey(ctx context.Context) (string, error) {
383383
// No authz checks
384-
return q.db.GetAppSigningKey(ctx)
384+
return q.db.GetAppSecurityKey(ctx)
385385
}
386386

387-
func (q *querier) InsertAppSigningKey(ctx context.Context, data string) error {
387+
func (q *querier) UpsertAppSecurityKey(ctx context.Context, data string) error {
388388
// No authz checks as this is done during startup
389-
return q.db.InsertAppSigningKey(ctx, data)
389+
return q.db.UpsertAppSecurityKey(ctx, data)
390390
}
391391

392392
func (q *querier) GetServiceBanner(ctx context.Context) (string, error) {
@@ -994,6 +994,16 @@ func (q *querier) GetTemplateUserRoles(ctx context.Context, id uuid.UUID) ([]dat
994994
return q.db.GetTemplateUserRoles(ctx, id)
995995
}
996996

997+
func (q *querier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error {
998+
// TODO: This is not 100% correct because it omits apikey IDs.
999+
err := q.authorizeContext(ctx, rbac.ActionDelete,
1000+
rbac.ResourceAPIKey.WithOwner(userID.String()))
1001+
if err != nil {
1002+
return err
1003+
}
1004+
return q.db.DeleteApplicationConnectAPIKeysByUserID(ctx, userID)
1005+
}
1006+
9971007
func (q *querier) DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error {
9981008
// TODO: This is not 100% correct because it omits apikey IDs.
9991009
err := q.authorizeContext(ctx, rbac.ActionDelete,

coderd/database/dbfake/databasefake.go

+18-5
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ type data struct {
143143
lastUpdateCheck []byte
144144
serviceBanner []byte
145145
logoURL string
146-
appSigningKey string
146+
appSecurityKey string
147147
lastLicenseID int32
148148
}
149149

@@ -679,6 +679,19 @@ func (q *fakeQuerier) DeleteAPIKeyByID(_ context.Context, id string) error {
679679
return sql.ErrNoRows
680680
}
681681

682+
func (q *fakeQuerier) DeleteApplicationConnectAPIKeysByUserID(_ context.Context, userID uuid.UUID) error {
683+
q.mutex.Lock()
684+
defer q.mutex.Unlock()
685+
686+
for i := len(q.apiKeys) - 1; i >= 0; i-- {
687+
if q.apiKeys[i].UserID == userID && q.apiKeys[i].Scope == database.APIKeyScopeApplicationConnect {
688+
q.apiKeys = append(q.apiKeys[:i], q.apiKeys[i+1:]...)
689+
}
690+
}
691+
692+
return nil
693+
}
694+
682695
func (q *fakeQuerier) DeleteAPIKeysByUserID(_ context.Context, userID uuid.UUID) error {
683696
q.mutex.Lock()
684697
defer q.mutex.Unlock()
@@ -4463,18 +4476,18 @@ func (q *fakeQuerier) GetLogoURL(_ context.Context) (string, error) {
44634476
return q.logoURL, nil
44644477
}
44654478

4466-
func (q *fakeQuerier) GetAppSigningKey(_ context.Context) (string, error) {
4479+
func (q *fakeQuerier) GetAppSecurityKey(_ context.Context) (string, error) {
44674480
q.mutex.RLock()
44684481
defer q.mutex.RUnlock()
44694482

4470-
return q.appSigningKey, nil
4483+
return q.appSecurityKey, nil
44714484
}
44724485

4473-
func (q *fakeQuerier) InsertAppSigningKey(_ context.Context, data string) error {
4486+
func (q *fakeQuerier) UpsertAppSecurityKey(_ context.Context, data string) error {
44744487
q.mutex.Lock()
44754488
defer q.mutex.Unlock()
44764489

4477-
q.appSigningKey = data
4490+
q.appSecurityKey = data
44784491
return nil
44794492
}
44804493

coderd/database/dbgen/generator.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,23 @@ func APIKey(t testing.TB, db database.Store, seed database.APIKey) (key database
7777
secret, _ := cryptorand.String(22)
7878
hashed := sha256.Sum256([]byte(secret))
7979

80+
ip := seed.IPAddress
81+
if !ip.Valid {
82+
ip = pqtype.Inet{
83+
IPNet: net.IPNet{
84+
IP: net.IPv4(127, 0, 0, 1),
85+
Mask: net.IPv4Mask(255, 255, 255, 255),
86+
},
87+
Valid: true,
88+
}
89+
}
90+
8091
key, err := db.InsertAPIKey(context.Background(), database.InsertAPIKeyParams{
8192
ID: takeFirst(seed.ID, id),
8293
// 0 defaults to 86400 at the db layer
8394
LifetimeSeconds: takeFirst(seed.LifetimeSeconds, 0),
8495
HashedSecret: takeFirstSlice(seed.HashedSecret, hashed[:]),
85-
IPAddress: pqtype.Inet{},
96+
IPAddress: ip,
8697
UserID: takeFirst(seed.UserID, uuid.New()),
8798
LastUsed: takeFirst(seed.LastUsed, database.Now()),
8899
ExpiresAt: takeFirst(seed.ExpiresAt, database.Now().Add(time.Hour)),

0 commit comments

Comments
 (0)