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

Commit1bdd2ab

Browse files
authored
feat: use JWT ticket to avoid DB queries on apps (#6148)
Issue a JWT ticket on the first request with a short expiry thatcontains details about which workspace/agent/app combo the ticket isvalid for.
1 parentf8494d2 commit1bdd2ab

37 files changed

+2736
-896
lines changed

‎cli/server.go

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"crypto/tls"
1111
"crypto/x509"
1212
"database/sql"
13+
"encoding/hex"
1314
"errors"
1415
"fmt"
1516
"io"
@@ -587,19 +588,62 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
587588
deferoptions.Pubsub.Close()
588589
}
589590

590-
deploymentID,err:=options.Database.GetDeploymentID(ctx)
591-
iferrors.Is(err,sql.ErrNoRows) {
592-
err=nil
593-
}
594-
iferr!=nil {
595-
returnxerrors.Errorf("get deployment id: %w",err)
596-
}
597-
ifdeploymentID=="" {
598-
deploymentID=uuid.NewString()
599-
err=options.Database.InsertDeploymentID(ctx,deploymentID)
591+
vardeploymentIDstring
592+
err=options.Database.InTx(func(tx database.Store)error {
593+
// This will block until the lock is acquired, and will be
594+
// automatically released when the transaction ends.
595+
err:=tx.AcquireLock(ctx,database.LockIDDeploymentSetup)
596+
iferr!=nil {
597+
returnxerrors.Errorf("acquire lock: %w",err)
598+
}
599+
600+
deploymentID,err=tx.GetDeploymentID(ctx)
601+
iferr!=nil&&!xerrors.Is(err,sql.ErrNoRows) {
602+
returnxerrors.Errorf("get deployment id: %w",err)
603+
}
604+
ifdeploymentID=="" {
605+
deploymentID=uuid.NewString()
606+
err=tx.InsertDeploymentID(ctx,deploymentID)
607+
iferr!=nil {
608+
returnxerrors.Errorf("set deployment id: %w",err)
609+
}
610+
}
611+
612+
// Read the app signing key from the DB. We store it hex
613+
// encoded since the config table uses strings for the value and
614+
// we don't want to deal with automatic encoding issues.
615+
appSigningKeyStr,err:=tx.GetAppSigningKey(ctx)
616+
iferr!=nil&&!xerrors.Is(err,sql.ErrNoRows) {
617+
returnxerrors.Errorf("get app signing key: %w",err)
618+
}
619+
ifappSigningKeyStr=="" {
620+
// Generate 64 byte secure random string.
621+
b:=make([]byte,64)
622+
_,err:=rand.Read(b)
623+
iferr!=nil {
624+
returnxerrors.Errorf("generate fresh app signing key: %w",err)
625+
}
626+
627+
appSigningKeyStr=hex.EncodeToString(b)
628+
err=tx.InsertAppSigningKey(ctx,appSigningKeyStr)
629+
iferr!=nil {
630+
returnxerrors.Errorf("insert freshly generated app signing key to database: %w",err)
631+
}
632+
}
633+
634+
appSigningKey,err:=hex.DecodeString(appSigningKeyStr)
600635
iferr!=nil {
601-
returnxerrors.Errorf("set deployment id: %w",err)
636+
returnxerrors.Errorf("decode app signing key from database as hex: %w",err)
637+
}
638+
iflen(appSigningKey)!=64 {
639+
returnxerrors.Errorf("app signing key must be 64 bytes, key in database is %d bytes",len(appSigningKey))
602640
}
641+
642+
options.AppSigningKey=appSigningKey
643+
returnnil
644+
},nil)
645+
iferr!=nil {
646+
returnerr
603647
}
604648

605649
// Disable telemetry if the in-memory database is used unless explicitly defined!

‎coderd/coderd.go

Lines changed: 32 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import (
5656
"github.com/coder/coder/coderd/tracing"
5757
"github.com/coder/coder/coderd/updatecheck"
5858
"github.com/coder/coder/coderd/util/slice"
59+
"github.com/coder/coder/coderd/workspaceapps"
5960
"github.com/coder/coder/coderd/wsconncache"
6061
"github.com/coder/coder/codersdk"
6162
"github.com/coder/coder/provisionerd/proto"
@@ -120,6 +121,9 @@ type Options struct {
120121
SwaggerEndpointbool
121122
SetUserGroupsfunc(ctx context.Context,tx database.Store,userID uuid.UUID,groupNames []string)error
122123
TemplateScheduleStore schedule.TemplateScheduleStore
124+
// AppSigningKey denotes the symmetric key to use for signing app tickets.
125+
// The key must be 64 bytes long.
126+
AppSigningKey []byte
123127

124128
// APIRateLimit is the minutely throughput rate limit per user or ip.
125129
// Setting a rate limit <0 will disable the rate limiter across the entire
@@ -214,6 +218,9 @@ func New(options *Options) *API {
214218
ifoptions.TemplateScheduleStore==nil {
215219
options.TemplateScheduleStore=schedule.NewAGPLTemplateScheduleStore()
216220
}
221+
iflen(options.AppSigningKey)!=64 {
222+
panic("coderd: AppSigningKey must be 64 bytes long")
223+
}
217224

218225
siteCacheDir:=options.CacheDir
219226
ifsiteCacheDir!="" {
@@ -236,6 +243,11 @@ func New(options *Options) *API {
236243
// static files since it only affects browsers.
237244
staticHandler=httpmw.HSTS(staticHandler,options.StrictTransportSecurityCfg)
238245

246+
oauthConfigs:=&httpmw.OAuth2Configs{
247+
Github:options.GithubOAuth2Config,
248+
OIDC:options.OIDCConfig,
249+
}
250+
239251
r:=chi.NewRouter()
240252
ctx,cancel:=context.WithCancel(context.Background())
241253
api:=&API{
@@ -250,6 +262,15 @@ func New(options *Options) *API {
250262
Authorizer:options.Authorizer,
251263
Logger:options.Logger,
252264
},
265+
WorkspaceAppsProvider:workspaceapps.New(
266+
options.Logger.Named("workspaceapps"),
267+
options.AccessURL,
268+
options.Authorizer,
269+
options.Database,
270+
options.DeploymentConfig,
271+
oauthConfigs,
272+
options.AppSigningKey,
273+
),
253274
metricsCache:metricsCache,
254275
Auditor: atomic.Pointer[audit.Auditor]{},
255276
TemplateScheduleStore: atomic.Pointer[schedule.TemplateScheduleStore]{},
@@ -266,20 +287,16 @@ func New(options *Options) *API {
266287
api.TemplateScheduleStore.Store(&options.TemplateScheduleStore)
267288
api.workspaceAgentCache=wsconncache.New(api.dialWorkspaceAgentTailnet,0)
268289
api.TailnetCoordinator.Store(&options.TailnetCoordinator)
269-
oauthConfigs:=&httpmw.OAuth2Configs{
270-
Github:options.GithubOAuth2Config,
271-
OIDC:options.OIDCConfig,
272-
}
273290

274-
apiKeyMiddleware:=httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
291+
apiKeyMiddleware:=httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
275292
DB:options.Database,
276293
OAuth2Configs:oauthConfigs,
277294
RedirectToLogin:false,
278295
DisableSessionExpiryRefresh:options.DeploymentConfig.DisableSessionExpiryRefresh.Value,
279296
Optional:false,
280297
})
281298
// Same as above but it redirects to the login page.
282-
apiKeyMiddlewareRedirect:=httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
299+
apiKeyMiddlewareRedirect:=httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
283300
DB:options.Database,
284301
OAuth2Configs:oauthConfigs,
285302
RedirectToLogin:true,
@@ -305,23 +322,9 @@ func New(options *Options) *API {
305322
httpmw.Prometheus(options.PrometheusRegistry),
306323
// handleSubdomainApplications checks if the first subdomain is a valid
307324
// app URL. If it is, it will serve that application.
308-
api.handleSubdomainApplications(
309-
apiRateLimiter,
310-
// Middleware to impose on the served application.
311-
httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
312-
DB:options.Database,
313-
OAuth2Configs:oauthConfigs,
314-
// The code handles the the case where the user is not
315-
// authenticated automatically.
316-
RedirectToLogin:false,
317-
DisableSessionExpiryRefresh:options.DeploymentConfig.DisableSessionExpiryRefresh.Value,
318-
Optional:true,
319-
}),
320-
httpmw.AsAuthzSystem(
321-
httpmw.ExtractUserParam(api.Database,false),
322-
httpmw.ExtractWorkspaceAndAgentParam(api.Database),
323-
),
324-
),
325+
//
326+
// Workspace apps do their own auth.
327+
api.handleSubdomainApplications(apiRateLimiter),
325328
// Build-Version is helpful for debugging.
326329
func(next http.Handler) http.Handler {
327330
returnhttp.HandlerFunc(func(w http.ResponseWriter,r*http.Request) {
@@ -345,26 +348,8 @@ func New(options *Options) *API {
345348
r.Get("/healthz",func(w http.ResponseWriter,r*http.Request) {_,_=w.Write([]byte("OK")) })
346349

347350
apps:=func(r chi.Router) {
348-
r.Use(
349-
apiRateLimiter,
350-
httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
351-
DB:options.Database,
352-
OAuth2Configs:oauthConfigs,
353-
// Optional is true to allow for public apps. If an
354-
// authorization check fails and the user is not authenticated,
355-
// they will be redirected to the login page by the app handler.
356-
RedirectToLogin:false,
357-
DisableSessionExpiryRefresh:options.DeploymentConfig.DisableSessionExpiryRefresh.Value,
358-
Optional:true,
359-
}),
360-
httpmw.AsAuthzSystem(
361-
// Redirect to the login page if the user tries to open an app with
362-
// "me" as the username and they are not logged in.
363-
httpmw.ExtractUserParam(api.Database,true),
364-
// Extracts the <workspace.agent> from the url
365-
httpmw.ExtractWorkspaceAndAgentParam(api.Database),
366-
),
367-
)
351+
// Workspace apps do their own auth.
352+
r.Use(apiRateLimiter)
368353
r.HandleFunc("/*",api.workspaceAppsProxyPath)
369354
}
370355
// %40 is the encoded character of the @ symbol. VS Code Web does
@@ -742,9 +727,10 @@ type API struct {
742727
WebsocketWaitGroup sync.WaitGroup
743728
derpCloseFuncfunc()
744729

745-
metricsCache*metricscache.Cache
746-
workspaceAgentCache*wsconncache.Cache
747-
updateChecker*updatecheck.Checker
730+
metricsCache*metricscache.Cache
731+
workspaceAgentCache*wsconncache.Cache
732+
updateChecker*updatecheck.Checker
733+
WorkspaceAppsProvider*workspaceapps.Provider
748734

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

‎coderd/coderdtest/coderdtest.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"crypto/x509"
1212
"crypto/x509/pkix"
1313
"encoding/base64"
14+
"encoding/hex"
1415
"encoding/json"
1516
"encoding/pem"
1617
"errors"
@@ -82,6 +83,10 @@ import (
8283
"github.com/coder/coder/testutil"
8384
)
8485

86+
// AppSigningKey is a 64-byte key used to sign JWTs for workspace app tickets in
87+
// tests.
88+
varAppSigningKey=must(hex.DecodeString("64656164626565666465616462656566646561646265656664656164626565666465616462656566646561646265656664656164626565666465616462656566"))
89+
8590
typeOptionsstruct {
8691
// AccessURL denotes a custom access URL. By default we use the httptest
8792
// server's URL. Setting this may result in unexpected behavior (especially
@@ -330,6 +335,7 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can
330335
DeploymentConfig:options.DeploymentConfig,
331336
UpdateCheckOptions:options.UpdateCheckOptions,
332337
SwaggerEndpoint:options.SwaggerEndpoint,
338+
AppSigningKey:AppSigningKey,
333339
}
334340
}
335341

‎coderd/database/dbauthz/querier.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ func (q *querier) Ping(ctx context.Context) (time.Duration, error) {
1919
returnq.db.Ping(ctx)
2020
}
2121

22+
func (q*querier)AcquireLock(ctx context.Context,idint64)error {
23+
returnq.db.AcquireLock(ctx,id)
24+
}
25+
26+
func (q*querier)TryAcquireLock(ctx context.Context,idint64) (bool,error) {
27+
returnq.db.TryAcquireLock(ctx,id)
28+
}
29+
2230
// InTx runs the given function in a transaction.
2331
func (q*querier)InTx(functionfunc(querier database.Store)error,txOpts*sql.TxOptions)error {
2432
returnq.db.InTx(func(tx database.Store)error {
@@ -317,6 +325,16 @@ func (q *querier) GetLogoURL(ctx context.Context) (string, error) {
317325
returnq.db.GetLogoURL(ctx)
318326
}
319327

328+
func (q*querier)GetAppSigningKey(ctx context.Context) (string,error) {
329+
// No authz checks
330+
returnq.db.GetAppSigningKey(ctx)
331+
}
332+
333+
func (q*querier)InsertAppSigningKey(ctx context.Context,datastring)error {
334+
// No authz checks as this is done during startup
335+
returnq.db.InsertAppSigningKey(ctx,data)
336+
}
337+
320338
func (q*querier)GetServiceBanner(ctx context.Context) (string,error) {
321339
// No authz checks
322340
returnq.db.GetServiceBanner(ctx)

‎coderd/database/dbfake/databasefake.go

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ func New() database.Store {
6464
workspaceApps:make([]database.WorkspaceApp,0),
6565
workspaces:make([]database.Workspace,0),
6666
licenses:make([]database.License,0),
67+
locks:map[int64]struct{}{},
6768
},
6869
}
6970
}
@@ -89,6 +90,11 @@ type fakeQuerier struct {
8990
*data
9091
}
9192

93+
typefakeTxstruct {
94+
*fakeQuerier
95+
locksmap[int64]struct{}
96+
}
97+
9298
typedatastruct {
9399
// Legacy tables
94100
apiKeys []database.APIKey
@@ -124,11 +130,15 @@ type data struct {
124130
workspaceResources []database.WorkspaceResource
125131
workspaces []database.Workspace
126132

133+
// Locks is a map of lock names. Any keys within the map are currently
134+
// locked.
135+
locksmap[int64]struct{}
127136
deploymentIDstring
128137
derpMeshKeystring
129138
lastUpdateCheck []byte
130139
serviceBanner []byte
131140
logoURLstring
141+
appSigningKeystring
132142
lastLicenseIDint32
133143
}
134144

@@ -196,11 +206,50 @@ func (*fakeQuerier) Ping(_ context.Context) (time.Duration, error) {
196206
return0,nil
197207
}
198208

209+
func (*fakeQuerier)AcquireLock(_ context.Context,_int64)error {
210+
returnxerrors.New("AcquireLock must only be called within a transaction")
211+
}
212+
213+
func (*fakeQuerier)TryAcquireLock(_ context.Context,_int64) (bool,error) {
214+
returnfalse,xerrors.New("TryAcquireLock must only be called within a transaction")
215+
}
216+
217+
func (tx*fakeTx)AcquireLock(_ context.Context,idint64)error {
218+
if_,ok:=tx.fakeQuerier.locks[id];ok {
219+
returnxerrors.Errorf("cannot acquire lock %d: already held",id)
220+
}
221+
tx.fakeQuerier.locks[id]=struct{}{}
222+
tx.locks[id]=struct{}{}
223+
returnnil
224+
}
225+
226+
func (tx*fakeTx)TryAcquireLock(_ context.Context,idint64) (bool,error) {
227+
if_,ok:=tx.fakeQuerier.locks[id];ok {
228+
returnfalse,nil
229+
}
230+
tx.fakeQuerier.locks[id]=struct{}{}
231+
tx.locks[id]=struct{}{}
232+
returntrue,nil
233+
}
234+
235+
func (tx*fakeTx)releaseLocks() {
236+
forid:=rangetx.locks {
237+
delete(tx.fakeQuerier.locks,id)
238+
}
239+
tx.locks=map[int64]struct{}{}
240+
}
241+
199242
// InTx doesn't rollback data properly for in-memory yet.
200243
func (q*fakeQuerier)InTx(fnfunc(database.Store)error,_*sql.TxOptions)error {
201244
q.mutex.Lock()
202245
deferq.mutex.Unlock()
203-
returnfn(&fakeQuerier{mutex:inTxMutex{},data:q.data})
246+
tx:=&fakeTx{
247+
fakeQuerier:&fakeQuerier{mutex:inTxMutex{},data:q.data},
248+
locks:map[int64]struct{}{},
249+
}
250+
defertx.releaseLocks()
251+
252+
returnfn(tx)
204253
}
205254

206255
func (q*fakeQuerier)AcquireProvisionerJob(_ context.Context,arg database.AcquireProvisionerJobParams) (database.ProvisionerJob,error) {
@@ -4004,6 +4053,21 @@ func (q *fakeQuerier) GetLogoURL(_ context.Context) (string, error) {
40044053
returnq.logoURL,nil
40054054
}
40064055

4056+
func (q*fakeQuerier)GetAppSigningKey(_ context.Context) (string,error) {
4057+
q.mutex.RLock()
4058+
deferq.mutex.RUnlock()
4059+
4060+
returnq.appSigningKey,nil
4061+
}
4062+
4063+
func (q*fakeQuerier)InsertAppSigningKey(_ context.Context,datastring)error {
4064+
q.mutex.Lock()
4065+
deferq.mutex.Unlock()
4066+
4067+
q.appSigningKey=data
4068+
returnnil
4069+
}
4070+
40074071
func (q*fakeQuerier)InsertLicense(
40084072
_ context.Context,arg database.InsertLicenseParams,
40094073
) (database.License,error) {

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp