Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commiteb66cc9

Browse files
chore: move app proxying code to workspaceapps pkg (#6998)
* chore: move app proxying code to workspaceapps pkgMoves path-app, subdomain-app and reconnecting PTY proxying to the newworkspaceapps.WorkspaceAppServer struct. This is in preparation forexternal 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-outuser will be deleted every time you logout for simplicity sake.Tests will remain in their original package, pending being moved to anapptest package (or similar).Co-authored-by: Steven Masley <stevenmasley@coder.com>
1 parent0069831 commiteb66cc9

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-
//encodedsince the config table uses strings for the value and
786-
//wedon'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
iferr!=nil&&!xerrors.Is(err,sql.ErrNoRows) {
789790
returnxerrors.Errorf("get app signing key: %w",err)
790791
}
791-
ifappSigningKeyStr=="" {
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+
ifdecoded,err:=hex.DecodeString(appSecurityKeyStr);err!=nil||len(decoded)!=len(workspaceapps.SecurityKey{}) {
802+
b:=make([]byte,len(workspaceapps.SecurityKey{}))
794803
_,err:=rand.Read(b)
795804
iferr!=nil {
796805
returnxerrors.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
iferr!=nil {
802811
returnxerrors.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
iferr!=nil {
808-
returnxerrors.Errorf("decode app signing key from database as hex: %w",err)
809-
}
810-
iflen(appSigningKey)!=64 {
811-
returnxerrors.Errorf("app signing key must be 64 bytes, key in database is %d bytes",len(appSigningKey))
817+
returnxerrors.Errorf("decode app signing key from database: %w",err)
812818
}
813819

814-
options.AppSigningKey=appSigningKey
820+
options.AppSecurityKey=appSecurityKey
815821
returnnil
816822
},nil)
817823
iferr!=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
ifc.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
returnhttp.ErrUseLastResponse
675674
},
@@ -682,11 +681,15 @@ func TestServer(t *testing.T) {
682681
},
683682
},
684683
}
685-
deferclient.HTTPClient.CloseIdleConnections()
686-
_,err=client.HasFirstUser(ctx)
687-
iferr!=nil {
688-
require.ErrorContains(t,err,"Invalid application URL")
689-
}
684+
deferclient.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+
deferresp.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
SwaggerEndpointbool
124124
SetUserGroupsfunc(ctx context.Context,tx database.Store,userID uuid.UUID,groupNames []string)error
125125
TemplateScheduleStore*atomic.Pointer[schedule.TemplateScheduleStore]
126-
//AppSigningKey denotes thesymmetric key touse for signing temporary app
127-
//tokens. The key must be 64 bytes long.
128-
AppSigningKey []byte
126+
//AppSecurityKey is thecrypto keyusedtosign and encrypt tokens related to
127+
//workspace applications. It consists of both a signing and encryption key.
128+
AppSecurityKeyworkspaceapps.SecurityKey
129129
HealthcheckFuncfunc(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-
iflen(options.AppSigningKey)!=64 {
245-
panic("coderd: AppSigningKey must be 64 bytes long")
246-
}
247244
ifoptions.HealthcheckFunc==nil {
248245
options.HealthcheckFunc=func(ctx context.Context) (*healthcheck.Report,error) {
249246
returnhealthcheck.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. Ifit 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
returnhttp.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 a64-byte key used to sign JWTsfor workspace app tokens in
85-
// tests.
86-
varAppSigningKey=must(hex.DecodeString("64656164626565666465616462656566646561646265656664656164626565666465616462656566646561646265656664656164626565666465616462656566"))
84+
//AppSecurityKey is a96-byte key used to sign JWTsand encrypt JWEs for
85+
//workspace app tokens intests.
86+
varAppSecurityKey=must(workspaceapps.KeyFromString("6465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e2077617320686572"))
8787

8888
typeOptionsstruct {
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
returnq.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-
returnq.db.GetAppSigningKey(ctx)
384+
returnq.db.GetAppSecurityKey(ctx)
385385
}
386386

387-
func (q*querier)InsertAppSigningKey(ctx context.Context,datastring)error {
387+
func (q*querier)UpsertAppSecurityKey(ctx context.Context,datastring)error {
388388
// No authz checks as this is done during startup
389-
returnq.db.InsertAppSigningKey(ctx,data)
389+
returnq.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
returnq.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+
iferr!=nil {
1002+
returnerr
1003+
}
1004+
returnq.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
logoURLstring
146-
appSigningKeystring
146+
appSecurityKeystring
147147
lastLicenseIDint32
148148
}
149149

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

682+
func (q*fakeQuerier)DeleteApplicationConnectAPIKeysByUserID(_ context.Context,userID uuid.UUID)error {
683+
q.mutex.Lock()
684+
deferq.mutex.Unlock()
685+
686+
fori:=len(q.apiKeys)-1;i>=0;i-- {
687+
ifq.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+
returnnil
693+
}
694+
682695
func (q*fakeQuerier)DeleteAPIKeysByUserID(_ context.Context,userID uuid.UUID)error {
683696
q.mutex.Lock()
684697
deferq.mutex.Unlock()
@@ -4463,18 +4476,18 @@ func (q *fakeQuerier) GetLogoURL(_ context.Context) (string, error) {
44634476
returnq.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
deferq.mutex.RUnlock()
44694482

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

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

4477-
q.appSigningKey=data
4490+
q.appSecurityKey=data
44784491
returnnil
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)

[8]ページ先頭

©2009-2025 Movatter.jp