Skip to content

Commit db8b42e

Browse files
committed
feat: API key scopes database migration
1 parent 241294b commit db8b42e

File tree

24 files changed

+964
-70
lines changed

24 files changed

+964
-70
lines changed

coderd/apikey/apikey.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error)
9292
UpdatedAt: dbtime.Now(),
9393
HashedSecret: hashed[:],
9494
LoginType: params.LoginType,
95-
Scope: scope,
95+
Scopes: database.APIKeyScopes{scope},
96+
AllowList: []string{database.AllowListWildcard().String()},
9697
TokenName: params.TokenName,
9798
}, token, nil
9899
}

coderd/apikey/apikey_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,9 @@ func TestGenerate(t *testing.T) {
159159
}
160160

161161
if tc.params.Scope != "" {
162-
assert.Equal(t, tc.params.Scope, key.Scope)
162+
assert.Equal(t, tc.params.Scope, key.Scopes[0])
163163
} else {
164-
assert.Equal(t, database.APIKeyScopeAll, key.Scope)
164+
assert.Equal(t, database.APIKeyScopeAll, key.Scopes[0])
165165
}
166166

167167
if tc.params.TokenName != "" {

coderd/coderdtest/authorize.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func AssertRBAC(t *testing.T, api *coderd.API, client *codersdk.Client) RBACAsse
6868
ID: key.UserID.String(),
6969
Roles: rbac.RoleIdentifiers(roleNames),
7070
Groups: roles.Groups,
71-
Scope: rbac.ScopeName(key.Scope),
71+
Scope: key.Scopes,
7272
},
7373
Recorder: recorder,
7474
}

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ func (s *MethodTestSuite) TestAPIKey() {
251251
}))
252252
s.Run("InsertAPIKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
253253
u := testutil.Fake(s.T(), faker, database.User{})
254-
arg := database.InsertAPIKeyParams{UserID: u.ID, LoginType: database.LoginTypePassword, Scope: database.APIKeyScopeAll, IPAddress: defaultIPAddress()}
254+
arg := database.InsertAPIKeyParams{UserID: u.ID, LoginType: database.LoginTypePassword, Scopes: database.APIKeyScopes{database.APIKeyScopeAll}, IPAddress: defaultIPAddress()}
255255
ret := testutil.Fake(s.T(), faker, database.APIKey{UserID: u.ID, LoginType: database.LoginTypePassword})
256256
dbm.EXPECT().InsertAPIKey(gomock.Any(), arg).Return(ret, nil).AnyTimes()
257257
check.Args(arg).Asserts(rbac.ResourceApiKey.WithOwner(u.ID.String()), policy.ActionCreate)
@@ -265,7 +265,7 @@ func (s *MethodTestSuite) TestAPIKey() {
265265
check.Args(arg).Asserts(a, policy.ActionUpdate).Returns()
266266
}))
267267
s.Run("DeleteApplicationConnectAPIKeysByUserID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
268-
a := testutil.Fake(s.T(), faker, database.APIKey{Scope: database.APIKeyScopeApplicationConnect})
268+
a := testutil.Fake(s.T(), faker, database.APIKey{Scopes: database.APIKeyScopes{database.APIKeyScopeApplicationConnect}})
269269
dbm.EXPECT().DeleteApplicationConnectAPIKeysByUserID(gomock.Any(), a.UserID).Return(nil).AnyTimes()
270270
check.Args(a.UserID).Asserts(rbac.ResourceApiKey.WithOwner(a.UserID.String()), policy.ActionDelete).Returns()
271271
}))

coderd/database/dbgen/dbgen.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,8 @@ func APIKey(t testing.TB, db database.Store, seed database.APIKey, munge ...func
185185
CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()),
186186
UpdatedAt: takeFirst(seed.UpdatedAt, dbtime.Now()),
187187
LoginType: takeFirst(seed.LoginType, database.LoginTypePassword),
188-
Scope: takeFirst(seed.Scope, database.APIKeyScopeAll),
188+
Scopes: takeFirstSlice([]database.APIKeyScope(seed.Scopes), []database.APIKeyScope{database.APIKeyScopeAll}),
189+
AllowList: takeFirstSlice(seed.AllowList, []string{database.AllowListWildcard().String()}),
189190
TokenName: takeFirst(seed.TokenName),
190191
}
191192
for _, fn := range munge {

coderd/database/dump.sql

Lines changed: 142 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-- Recreate single-scope column and collapse arrays
2+
ALTER TABLE api_keys ADD COLUMN scope api_key_scope DEFAULT 'all'::api_key_scope NOT NULL;
3+
4+
-- Collapse logic: prefer 'all', else 'application_connect', else 'all'
5+
UPDATE api_keys SET scope =
6+
CASE
7+
WHEN 'all'::api_key_scope = ANY(scopes) THEN 'all'::api_key_scope
8+
WHEN 'application_connect'::api_key_scope = ANY(scopes) THEN 'application_connect'::api_key_scope
9+
ELSE 'all'::api_key_scope
10+
END;
11+
12+
-- Drop new columns
13+
ALTER TABLE api_keys DROP COLUMN allow_list;
14+
ALTER TABLE api_keys DROP COLUMN scopes;
15+
16+
-- Note: We intentionally keep the expanded enum values to avoid dependency churn.
17+
-- If strict narrowing is required, create a new type with only ('all','application_connect'),
18+
-- cast column, drop the new type, and rename.
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
-- Extend api_key_scope enum with low-level <resource>:<action> values derived from RBACPermissions
2+
-- Generated via: go run ./scripts/generate_api_key_scope_enum
3+
-- Begin enum extensions
4+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'aibridge_interception:create';
5+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'aibridge_interception:read';
6+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'aibridge_interception:update';
7+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'api_key:create';
8+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'api_key:delete';
9+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'api_key:read';
10+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'api_key:update';
11+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_org_role:assign';
12+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_org_role:create';
13+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_org_role:delete';
14+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_org_role:read';
15+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_org_role:unassign';
16+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_org_role:update';
17+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_role:assign';
18+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_role:read';
19+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_role:unassign';
20+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'audit_log:create';
21+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'audit_log:read';
22+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'connection_log:read';
23+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'connection_log:update';
24+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'crypto_key:create';
25+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'crypto_key:delete';
26+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'crypto_key:read';
27+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'crypto_key:update';
28+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'debug_info:read';
29+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'deployment_config:read';
30+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'deployment_config:update';
31+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'deployment_stats:read';
32+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'file:create';
33+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'file:read';
34+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'group:create';
35+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'group:delete';
36+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'group:read';
37+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'group:update';
38+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'group_member:read';
39+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'idpsync_settings:read';
40+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'idpsync_settings:update';
41+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'inbox_notification:create';
42+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'inbox_notification:read';
43+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'inbox_notification:update';
44+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'license:create';
45+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'license:delete';
46+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'license:read';
47+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'notification_message:create';
48+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'notification_message:delete';
49+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'notification_message:read';
50+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'notification_message:update';
51+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'notification_preference:read';
52+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'notification_preference:update';
53+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'notification_template:read';
54+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'notification_template:update';
55+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app:create';
56+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app:delete';
57+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app:read';
58+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app:update';
59+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app_code_token:create';
60+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app_code_token:delete';
61+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app_code_token:read';
62+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app_secret:create';
63+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app_secret:delete';
64+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app_secret:read';
65+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app_secret:update';
66+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization:create';
67+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization:delete';
68+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization:read';
69+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization:update';
70+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization_member:create';
71+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization_member:delete';
72+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization_member:read';
73+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization_member:update';
74+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'prebuilt_workspace:delete';
75+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'prebuilt_workspace:update';
76+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'provisioner_daemon:create';
77+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'provisioner_daemon:delete';
78+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'provisioner_daemon:read';
79+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'provisioner_daemon:update';
80+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'provisioner_jobs:create';
81+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'provisioner_jobs:read';
82+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'provisioner_jobs:update';
83+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'replicas:read';
84+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'system:create';
85+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'system:delete';
86+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'system:read';
87+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'system:update';
88+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'tailnet_coordinator:create';
89+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'tailnet_coordinator:delete';
90+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'tailnet_coordinator:read';
91+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'tailnet_coordinator:update';
92+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'template:create';
93+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'template:delete';
94+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'template:read';
95+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'template:update';
96+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'template:use';
97+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'template:view_insights';
98+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'usage_event:create';
99+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'usage_event:read';
100+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'usage_event:update';
101+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user:create';
102+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user:delete';
103+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user:read';
104+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user:read_personal';
105+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user:update';
106+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user:update_personal';
107+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user_secret:create';
108+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user_secret:delete';
109+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user_secret:read';
110+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user_secret:update';
111+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'webpush_subscription:create';
112+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'webpush_subscription:delete';
113+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'webpush_subscription:read';
114+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:application_connect';
115+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:create';
116+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:create_agent';
117+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:delete';
118+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:delete_agent';
119+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:read';
120+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:ssh';
121+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:start';
122+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:stop';
123+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:update';
124+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_agent_devcontainers:create';
125+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_agent_resource_monitor:create';
126+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_agent_resource_monitor:read';
127+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_agent_resource_monitor:update';
128+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:application_connect';
129+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:create';
130+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:create_agent';
131+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:delete';
132+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:delete_agent';
133+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:read';
134+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:ssh';
135+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:start';
136+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:stop';
137+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:update';
138+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_proxy:create';
139+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_proxy:delete';
140+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_proxy:read';
141+
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_proxy:update';
142+
-- End enum extensions
143+
144+
-- Add new columns without defaults; backfill; then enforce NOT NULL
145+
ALTER TABLE api_keys ADD COLUMN scopes api_key_scope[];
146+
ALTER TABLE api_keys ADD COLUMN allow_list text[];
147+
148+
-- Backfill existing rows for compatibility
149+
UPDATE api_keys SET scopes = ARRAY[scope::api_key_scope];
150+
UPDATE api_keys SET allow_list = ARRAY['*:*'];
151+
152+
-- Enforce NOT NULL
153+
ALTER TABLE api_keys ALTER COLUMN scopes SET NOT NULL;
154+
ALTER TABLE api_keys ALTER COLUMN allow_list SET NOT NULL;
155+
156+
-- Drop legacy single-scope column
157+
ALTER TABLE api_keys DROP COLUMN scope;

0 commit comments

Comments
 (0)