Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions coderd/database/check_constraint.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions coderd/database/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,13 @@ import (
// for use as a uuid.UUID. Both must agree; tests pin the value to the
// codersdk constant so the two cannot drift.
var PrebuildsSystemUserID = uuid.MustParse(codersdk.PrebuildsSystemUserID)

// OAuth2ScopeUnrestricted is the oauth2_provider_app_codes.scope and
// oauth2_provider_app_tokens.scope value recording a grant that carries no
// restriction. Both columns hold space-separated values from the
// api_key_scope vocabulary, so an unrestricted grant is spelled the same way
// api_keys.scopes spells it. The columns are NOT NULL: writing this constant
// is how a caller states "unrestricted" on purpose, which is what
// distinguishes a deliberate grant from a scope that was never threaded
// through.
const OAuth2ScopeUnrestricted = string(ApiKeyScopeCoderAll)
8 changes: 8 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -2067,6 +2067,10 @@ func (q *querier) DeleteAPIKeyByID(ctx context.Context, id string) error {
return deleteQ(q.log, q.auth, q.db.GetAPIKeyByID, q.db.DeleteAPIKeyByID)(ctx, id)
}

func (q *querier) DeleteAPIKeyByIDReturningRow(ctx context.Context, id string) (database.APIKey, error) {
return fetchAndQuery(q.log, q.auth, policy.ActionDelete, q.db.GetAPIKeyByID, q.db.DeleteAPIKeyByIDReturningRow)(ctx, id)
}

func (q *querier) DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error {
// TODO: This is not 100% correct because it omits apikey IDs.
err := q.authorizeContext(ctx, policy.ActionDelete,
Expand Down Expand Up @@ -2314,6 +2318,10 @@ func (q *querier) DeleteOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.U
return q.db.DeleteOAuth2ProviderAppCodeByID(ctx, id)
}

func (q *querier) DeleteOAuth2ProviderAppCodeByIDReturningRow(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderAppCode, error) {
return fetchAndQuery(q.log, q.auth, policy.ActionDelete, q.db.GetOAuth2ProviderAppCodeByID, q.db.DeleteOAuth2ProviderAppCodeByIDReturningRow)(ctx, id)
}

func (q *querier) DeleteOAuth2ProviderAppCodesByAppAndUserID(ctx context.Context, arg database.DeleteOAuth2ProviderAppCodesByAppAndUserIDParams) error {
if err := q.authorizeContext(ctx, policy.ActionDelete,
rbac.ResourceOauth2AppCodeToken.WithOwner(arg.UserID.String())); err != nil {
Expand Down
17 changes: 17 additions & 0 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,12 @@ func (s *MethodTestSuite) TestAPIKey() {
dbm.EXPECT().DeleteAPIKeyByID(gomock.Any(), key.ID).Return(nil).AnyTimes()
check.Args(key.ID).Asserts(key, policy.ActionDelete).Returns()
}))
s.Run("DeleteAPIKeyByIDReturningRow", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
key := testutil.Fake(s.T(), faker, database.APIKey{})
dbm.EXPECT().GetAPIKeyByID(gomock.Any(), key.ID).Return(key, nil).AnyTimes()
dbm.EXPECT().DeleteAPIKeyByIDReturningRow(gomock.Any(), key.ID).Return(key, nil).AnyTimes()
check.Args(key.ID).Asserts(key, policy.ActionDelete).Returns(key)
}))
s.Run("DeleteExpiredAPIKeys", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
args := database.DeleteExpiredAPIKeysParams{
Before: time.Date(2025, 11, 21, 0, 0, 0, 0, time.UTC),
Expand Down Expand Up @@ -6017,6 +6023,7 @@ func (s *MethodTestSuite) TestOAuth2ProviderAppCodes() {
check.Args(database.InsertOAuth2ProviderAppCodeParams{
AppID: app.ID,
UserID: user.ID,
Scope: database.OAuth2ScopeUnrestricted,
}).Asserts(rbac.ResourceOauth2AppCodeToken.WithOwner(user.ID.String()), policy.ActionCreate)
}))
s.Run("DeleteOAuth2ProviderAppCodeByID", s.Subtest(func(db database.Store, check *expects) {
Expand All @@ -6028,6 +6035,15 @@ func (s *MethodTestSuite) TestOAuth2ProviderAppCodes() {
})
check.Args(code.ID).Asserts(code, policy.ActionDelete)
}))
s.Run("DeleteOAuth2ProviderAppCodeByIDReturningRow", s.Subtest(func(db database.Store, check *expects) {
user := dbgen.User(s.T(), db, database.User{})
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
code := dbgen.OAuth2ProviderAppCode(s.T(), db, database.OAuth2ProviderAppCode{
AppID: app.ID,
UserID: user.ID,
})
check.Args(code.ID).Asserts(code, policy.ActionDelete).Returns(code)
}))
s.Run("DeleteOAuth2ProviderAppCodesByAppAndUserID", s.Subtest(func(db database.Store, check *expects) {
dbtestutil.DisableForeignKeysAndTriggers(s.T(), db)
user := dbgen.User(s.T(), db, database.User{})
Expand Down Expand Up @@ -6061,6 +6077,7 @@ func (s *MethodTestSuite) TestOAuth2ProviderAppTokens() {
AppSecretID: uuid.NullUUID{UUID: secret.ID, Valid: true},
APIKeyID: key.ID,
UserID: user.ID,
Scope: database.OAuth2ScopeUnrestricted,
}).Asserts(rbac.ResourceOauth2AppCodeToken.WithOwner(user.ID.String()), policy.ActionCreate)
}))
s.Run("GetOAuth2ProviderAppTokenByPrefix", s.Subtest(func(db database.Store, check *expects) {
Expand Down
2 changes: 2 additions & 0 deletions coderd/database/dbgen/dbgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -1784,6 +1784,7 @@ func OAuth2ProviderAppCode(t testing.TB, db database.Store, seed database.OAuth2
CodeChallengeMethod: seed.CodeChallengeMethod,
StateHash: seed.StateHash,
RedirectUri: seed.RedirectUri,
Scope: takeFirst(seed.Scope, database.OAuth2ScopeUnrestricted),
})
require.NoError(t, err, "insert oauth2 app code")
return code
Expand All @@ -1805,6 +1806,7 @@ func OAuth2ProviderAppToken(t testing.TB, db database.Store, seed database.OAuth
APIKeyID: takeFirst(seed.APIKeyID, uuid.New().String()),
UserID: takeFirst(seed.UserID, uuid.New()),
Audience: seed.Audience,
Scope: takeFirst(seed.Scope, database.OAuth2ScopeUnrestricted),
})
require.NoError(t, err, "insert oauth2 app token")
return token
Expand Down
16 changes: 16 additions & 0 deletions coderd/database/dbmetrics/querymetrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions coderd/database/dbmock/dbmock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE oauth2_provider_app_codes DROP COLUMN scope;

ALTER TABLE oauth2_provider_app_tokens DROP COLUMN scope;
30 changes: 30 additions & 0 deletions coderd/database/migrations/000569_oauth2_scope_columns.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- The scope negotiated at /oauth2/authorize travels with the grant itself:
Comment thread
BobbyHo marked this conversation as resolved.
-- recorded on the code when it is issued, then carried onto the token it is
-- exchanged for, so a refresh can be narrowed against what was actually
-- granted rather than against the app's current allowlist.
--
-- Existing rows are unrestricted in fact rather than by omission, since
-- apikey.Generate mints every OAuth2 access key with the coder:all scope.
-- The backfill writes that down. Both columns are then NOT NULL with no
-- default, so a grant's authority is always stated explicitly and a caller
-- that omits the column fails instead of silently issuing full access.

ALTER TABLE oauth2_provider_app_codes ADD COLUMN scope text;
Comment thread
BobbyHo marked this conversation as resolved.
Comment thread
BobbyHo marked this conversation as resolved.

ALTER TABLE oauth2_provider_app_tokens ADD COLUMN scope text;
Comment thread
BobbyHo marked this conversation as resolved.

UPDATE oauth2_provider_app_codes SET scope = 'coder:all' WHERE scope IS NULL;

UPDATE oauth2_provider_app_tokens SET scope = 'coder:all' WHERE scope IS NULL;

ALTER TABLE oauth2_provider_app_codes
ALTER COLUMN scope SET NOT NULL,
ADD CONSTRAINT oauth2_provider_app_codes_scope_not_empty CHECK (scope <> '');

ALTER TABLE oauth2_provider_app_tokens
ALTER COLUMN scope SET NOT NULL,
ADD CONSTRAINT oauth2_provider_app_tokens_scope_not_empty CHECK (scope <> '');

COMMENT ON COLUMN oauth2_provider_app_codes.scope IS 'Space-separated scope negotiated at authorization time, drawn from the api_key_scope vocabulary. Always set; coder:all records an unrestricted grant.';

COMMENT ON COLUMN oauth2_provider_app_tokens.scope IS 'Space-separated scope granted to this token, drawn from the api_key_scope vocabulary. Always set; coder:all records an unrestricted grant. Later phases will narrow this on refresh and never widen it.';
4 changes: 4 additions & 0 deletions coderd/database/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading