Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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)
Comment thread
BobbyHo marked this conversation as resolved.
Outdated
8 changes: 8 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,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 @@ -2307,6 +2311,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 @@ -6013,6 +6019,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 @@ -6024,6 +6031,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 @@ -6057,6 +6073,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