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

Skip to content

Commit 415d8b1

Browse files
committed
Merge branch 'main' into exportstats
2 parents 37ad03f + bb0a996 commit 415d8b1

40 files changed

+2767
-926
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
defer options.Pubsub.Close()
588589
}
589590

590-
deploymentID, err := options.Database.GetDeploymentID(ctx)
591-
if errors.Is(err, sql.ErrNoRows) {
592-
err = nil
593-
}
594-
if err != nil {
595-
return xerrors.Errorf("get deployment id: %w", err)
596-
}
597-
if deploymentID == "" {
598-
deploymentID = uuid.NewString()
599-
err = options.Database.InsertDeploymentID(ctx, deploymentID)
591+
var deploymentID string
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+
if err != nil {
597+
return xerrors.Errorf("acquire lock: %w", err)
598+
}
599+
600+
deploymentID, err = tx.GetDeploymentID(ctx)
601+
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
602+
return xerrors.Errorf("get deployment id: %w", err)
603+
}
604+
if deploymentID == "" {
605+
deploymentID = uuid.NewString()
606+
err = tx.InsertDeploymentID(ctx, deploymentID)
607+
if err != nil {
608+
return xerrors.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+
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
617+
return xerrors.Errorf("get app signing key: %w", err)
618+
}
619+
if appSigningKeyStr == "" {
620+
// Generate 64 byte secure random string.
621+
b := make([]byte, 64)
622+
_, err := rand.Read(b)
623+
if err != nil {
624+
return xerrors.Errorf("generate fresh app signing key: %w", err)
625+
}
626+
627+
appSigningKeyStr = hex.EncodeToString(b)
628+
err = tx.InsertAppSigningKey(ctx, appSigningKeyStr)
629+
if err != nil {
630+
return xerrors.Errorf("insert freshly generated app signing key to database: %w", err)
631+
}
632+
}
633+
634+
appSigningKey, err := hex.DecodeString(appSigningKeyStr)
600635
if err != nil {
601-
return xerrors.Errorf("set deployment id: %w", err)
636+
return xerrors.Errorf("decode app signing key from database as hex: %w", err)
637+
}
638+
if len(appSigningKey) != 64 {
639+
return xerrors.Errorf("app signing key must be 64 bytes, key in database is %d bytes", len(appSigningKey))
602640
}
641+
642+
options.AppSigningKey = appSigningKey
643+
return nil
644+
}, nil)
645+
if err != nil {
646+
return err
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
SwaggerEndpoint bool
121122
SetUserGroups func(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
if options.TemplateScheduleStore == nil {
215219
options.TemplateScheduleStore = schedule.NewAGPLTemplateScheduleStore()
216220
}
221+
if len(options.AppSigningKey) != 64 {
222+
panic("coderd: AppSigningKey must be 64 bytes long")
223+
}
217224

218225
siteCacheDir := options.CacheDir
219226
if siteCacheDir != "" {
@@ -239,6 +246,11 @@ func New(options *Options) *API {
239246
// static files since it only affects browsers.
240247
staticHandler = httpmw.HSTS(staticHandler, options.StrictTransportSecurityCfg)
241248

249+
oauthConfigs := &httpmw.OAuth2Configs{
250+
Github: options.GithubOAuth2Config,
251+
OIDC: options.OIDCConfig,
252+
}
253+
242254
r := chi.NewRouter()
243255
ctx, cancel := context.WithCancel(context.Background())
244256
api := &API{
@@ -253,6 +265,15 @@ func New(options *Options) *API {
253265
Authorizer: options.Authorizer,
254266
Logger: options.Logger,
255267
},
268+
WorkspaceAppsProvider: workspaceapps.New(
269+
options.Logger.Named("workspaceapps"),
270+
options.AccessURL,
271+
options.Authorizer,
272+
options.Database,
273+
options.DeploymentConfig,
274+
oauthConfigs,
275+
options.AppSigningKey,
276+
),
256277
metricsCache: metricsCache,
257278
Auditor: atomic.Pointer[audit.Auditor]{},
258279
TemplateScheduleStore: atomic.Pointer[schedule.TemplateScheduleStore]{},
@@ -269,20 +290,16 @@ func New(options *Options) *API {
269290
api.TemplateScheduleStore.Store(&options.TemplateScheduleStore)
270291
api.workspaceAgentCache = wsconncache.New(api.dialWorkspaceAgentTailnet, 0)
271292
api.TailnetCoordinator.Store(&options.TailnetCoordinator)
272-
oauthConfigs := &httpmw.OAuth2Configs{
273-
Github: options.GithubOAuth2Config,
274-
OIDC: options.OIDCConfig,
275-
}
276293

277-
apiKeyMiddleware := httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
294+
apiKeyMiddleware := httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
278295
DB: options.Database,
279296
OAuth2Configs: oauthConfigs,
280297
RedirectToLogin: false,
281298
DisableSessionExpiryRefresh: options.DeploymentConfig.DisableSessionExpiryRefresh.Value,
282299
Optional: false,
283300
})
284301
// Same as above but it redirects to the login page.
285-
apiKeyMiddlewareRedirect := httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
302+
apiKeyMiddlewareRedirect := httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
286303
DB: options.Database,
287304
OAuth2Configs: oauthConfigs,
288305
RedirectToLogin: true,
@@ -308,23 +325,9 @@ func New(options *Options) *API {
308325
httpmw.Prometheus(options.PrometheusRegistry),
309326
// handleSubdomainApplications checks if the first subdomain is a valid
310327
// app URL. If it is, it will serve that application.
311-
api.handleSubdomainApplications(
312-
apiRateLimiter,
313-
// Middleware to impose on the served application.
314-
httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
315-
DB: options.Database,
316-
OAuth2Configs: oauthConfigs,
317-
// The code handles the the case where the user is not
318-
// authenticated automatically.
319-
RedirectToLogin: false,
320-
DisableSessionExpiryRefresh: options.DeploymentConfig.DisableSessionExpiryRefresh.Value,
321-
Optional: true,
322-
}),
323-
httpmw.AsAuthzSystem(
324-
httpmw.ExtractUserParam(api.Database, false),
325-
httpmw.ExtractWorkspaceAndAgentParam(api.Database),
326-
),
327-
),
328+
//
329+
// Workspace apps do their own auth.
330+
api.handleSubdomainApplications(apiRateLimiter),
328331
// Build-Version is helpful for debugging.
329332
func(next http.Handler) http.Handler {
330333
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -348,26 +351,8 @@ func New(options *Options) *API {
348351
r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("OK")) })
349352

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

749-
metricsCache *metricscache.Cache
750-
workspaceAgentCache *wsconncache.Cache
751-
updateChecker *updatecheck.Checker
734+
metricsCache *metricscache.Cache
735+
workspaceAgentCache *wsconncache.Cache
736+
updateChecker *updatecheck.Checker
737+
WorkspaceAppsProvider *workspaceapps.Provider
752738

753739
// Experiments contains the list of experiments currently enabled.
754740
// 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+
var AppSigningKey = must(hex.DecodeString("64656164626565666465616462656566646561646265656664656164626565666465616462656566646561646265656664656164626565666465616462656566"))
89+
8590
type Options struct {
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
return q.db.Ping(ctx)
2020
}
2121

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

328+
func (q *querier) GetAppSigningKey(ctx context.Context) (string, error) {
329+
// No authz checks
330+
return q.db.GetAppSigningKey(ctx)
331+
}
332+
333+
func (q *querier) InsertAppSigningKey(ctx context.Context, data string) error {
334+
// No authz checks as this is done during startup
335+
return q.db.InsertAppSigningKey(ctx, data)
336+
}
337+
320338
func (q *querier) GetServiceBanner(ctx context.Context) (string, error) {
321339
// No authz checks
322340
return q.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+
type fakeTx struct {
94+
*fakeQuerier
95+
locks map[int64]struct{}
96+
}
97+
9298
type data struct {
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+
locks map[int64]struct{}
127136
deploymentID string
128137
derpMeshKey string
129138
lastUpdateCheck []byte
130139
serviceBanner []byte
131140
logoURL string
141+
appSigningKey string
132142
lastLicenseID int32
133143
}
134144

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

209+
func (*fakeQuerier) AcquireLock(_ context.Context, _ int64) error {
210+
return xerrors.New("AcquireLock must only be called within a transaction")
211+
}
212+
213+
func (*fakeQuerier) TryAcquireLock(_ context.Context, _ int64) (bool, error) {
214+
return false, xerrors.New("TryAcquireLock must only be called within a transaction")
215+
}
216+
217+
func (tx *fakeTx) AcquireLock(_ context.Context, id int64) error {
218+
if _, ok := tx.fakeQuerier.locks[id]; ok {
219+
return xerrors.Errorf("cannot acquire lock %d: already held", id)
220+
}
221+
tx.fakeQuerier.locks[id] = struct{}{}
222+
tx.locks[id] = struct{}{}
223+
return nil
224+
}
225+
226+
func (tx *fakeTx) TryAcquireLock(_ context.Context, id int64) (bool, error) {
227+
if _, ok := tx.fakeQuerier.locks[id]; ok {
228+
return false, nil
229+
}
230+
tx.fakeQuerier.locks[id] = struct{}{}
231+
tx.locks[id] = struct{}{}
232+
return true, nil
233+
}
234+
235+
func (tx *fakeTx) releaseLocks() {
236+
for id := range tx.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(fn func(database.Store) error, _ *sql.TxOptions) error {
201244
q.mutex.Lock()
202245
defer q.mutex.Unlock()
203-
return fn(&fakeQuerier{mutex: inTxMutex{}, data: q.data})
246+
tx := &fakeTx{
247+
fakeQuerier: &fakeQuerier{mutex: inTxMutex{}, data: q.data},
248+
locks: map[int64]struct{}{},
249+
}
250+
defer tx.releaseLocks()
251+
252+
return fn(tx)
204253
}
205254

206255
func (q *fakeQuerier) AcquireProvisionerJob(_ context.Context, arg database.AcquireProvisionerJobParams) (database.ProvisionerJob, error) {
@@ -4099,6 +4148,21 @@ func (q *fakeQuerier) GetLogoURL(_ context.Context) (string, error) {
40994148
return q.logoURL, nil
41004149
}
41014150

4151+
func (q *fakeQuerier) GetAppSigningKey(_ context.Context) (string, error) {
4152+
q.mutex.RLock()
4153+
defer q.mutex.RUnlock()
4154+
4155+
return q.appSigningKey, nil
4156+
}
4157+
4158+
func (q *fakeQuerier) InsertAppSigningKey(_ context.Context, data string) error {
4159+
q.mutex.Lock()
4160+
defer q.mutex.Unlock()
4161+
4162+
q.appSigningKey = data
4163+
return nil
4164+
}
4165+
41024166
func (q *fakeQuerier) InsertLicense(
41034167
_ context.Context, arg database.InsertLicenseParams,
41044168
) (database.License, error) {

0 commit comments

Comments
 (0)