Skip to content

Commit eb66cc9

Browse files
deansheatherEmyrk
andauthored
chore: move app proxying code to workspaceapps pkg (#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 <stevenmasley@coder.com>
1 parent 0069831 commit eb66cc9

28 files changed

+1236
-1334
lines changed

cli/server.go

Lines changed: 21 additions & 15 deletions
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

Lines changed: 10 additions & 7 deletions
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

Lines changed: 32 additions & 24 deletions
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

Lines changed: 5 additions & 5 deletions
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

Lines changed: 14 additions & 4 deletions
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

Lines changed: 18 additions & 5 deletions
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

Lines changed: 12 additions & 1 deletion
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy