Skip to content

Commit f684831

Browse files
authored
feat: add allow list to API keys (coder#19972)
Add API key allow list to the SDK This PR adds an allow list to API keys in the SDK. The allow list is a list of targets that the API key is allowed to access. If the allow list is empty, a default allow list with a single entry that allows access to all resources is created. The changes include: - Adding a default allow list when generating an API key if none is provided - Adding allow list to the API key response in the SDK - Converting database allow list entries to SDK format in the API response - Adding tests to verify the default allow list behavior Fixes coder#19854
1 parent f947a34 commit f684831

File tree

14 files changed

+162
-47
lines changed

14 files changed

+162
-47
lines changed

coderd/apidoc/docs.go

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apikey_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ func TestTokenCRUD(t *testing.T) {
5151
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*24*6))
5252
require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*24*8))
5353
require.Equal(t, codersdk.APIKeyScopeAll, keys[0].Scope)
54+
require.Len(t, keys[0].AllowList, 1)
55+
require.Equal(t, "*:*", keys[0].AllowList[0].String())
5456

5557
// no update
5658

@@ -86,6 +88,8 @@ func TestTokenScoped(t *testing.T) {
8688
require.EqualValues(t, len(keys), 1)
8789
require.Contains(t, res.Key, keys[0].ID)
8890
require.Equal(t, keys[0].Scope, codersdk.APIKeyScopeApplicationConnect)
91+
require.Len(t, keys[0].AllowList, 1)
92+
require.Equal(t, "*:*", keys[0].AllowList[0].String())
8993
}
9094

9195
// Ensure backward-compat: when a token is created using the legacy singular
@@ -132,6 +136,8 @@ func TestTokenLegacySingularScopeCompat(t *testing.T) {
132136
require.Len(t, keys, 1)
133137
require.Equal(t, tc.scope, keys[0].Scope)
134138
require.ElementsMatch(t, keys[0].Scopes, tc.scopes)
139+
require.Len(t, keys[0].AllowList, 1)
140+
require.Equal(t, "*:*", keys[0].AllowList[0].String())
135141
})
136142
}
137143
}

coderd/database/check_constraint.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/db2sdk/db2sdk.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ func ListLazy[F any, T any](convert func(F) T) func(list []F) []T {
5151
}
5252
}
5353

54+
func APIAllowListTarget(entry rbac.AllowListElement) codersdk.APIAllowListTarget {
55+
return codersdk.APIAllowListTarget{
56+
Type: codersdk.RBACResource(entry.Type),
57+
ID: entry.ID,
58+
}
59+
}
60+
5461
type ExternalAuthMeta struct {
5562
Authenticated bool
5663
ValidateError string

coderd/database/dump.sql

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- Drop all CHECK constraints added in the up migration
2+
ALTER TABLE api_keys
3+
DROP CONSTRAINT api_keys_allow_list_not_empty;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-- Defensively update any API keys with empty allow_list to have default '*:*'
2+
-- This ensures all existing keys have at least one entry before adding the constraint
3+
UPDATE api_keys
4+
SET allow_list = ARRAY['*:*']
5+
WHERE allow_list = ARRAY[]::text[] OR array_length(allow_list, 1) IS NULL;
6+
7+
-- Add CHECK constraint to ensure allow_list array is never empty
8+
ALTER TABLE api_keys
9+
ADD CONSTRAINT api_keys_allow_list_not_empty
10+
CHECK (array_length(allow_list, 1) > 0);

coderd/users.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,5 +1608,6 @@ func convertAPIKey(k database.APIKey) codersdk.APIKey {
16081608
Scopes: scopes,
16091609
LifetimeSeconds: k.LifetimeSeconds,
16101610
TokenName: k.TokenName,
1611+
AllowList: db2sdk.List(k.AllowList, db2sdk.APIAllowListTarget),
16111612
}
16121613
}

codersdk/apikey.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,18 @@ import (
1212

1313
// APIKey: do not ever return the HashedSecret
1414
type APIKey struct {
15-
ID string `json:"id" validate:"required"`
16-
UserID uuid.UUID `json:"user_id" validate:"required" format:"uuid"`
17-
LastUsed time.Time `json:"last_used" validate:"required" format:"date-time"`
18-
ExpiresAt time.Time `json:"expires_at" validate:"required" format:"date-time"`
19-
CreatedAt time.Time `json:"created_at" validate:"required" format:"date-time"`
20-
UpdatedAt time.Time `json:"updated_at" validate:"required" format:"date-time"`
21-
LoginType LoginType `json:"login_type" validate:"required" enums:"password,github,oidc,token"`
22-
Scope APIKeyScope `json:"scope" enums:"all,application_connect"` // Deprecated: use Scopes instead.
23-
Scopes []APIKeyScope `json:"scopes"`
24-
TokenName string `json:"token_name" validate:"required"`
25-
LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"`
15+
ID string `json:"id" validate:"required"`
16+
UserID uuid.UUID `json:"user_id" validate:"required" format:"uuid"`
17+
LastUsed time.Time `json:"last_used" validate:"required" format:"date-time"`
18+
ExpiresAt time.Time `json:"expires_at" validate:"required" format:"date-time"`
19+
CreatedAt time.Time `json:"created_at" validate:"required" format:"date-time"`
20+
UpdatedAt time.Time `json:"updated_at" validate:"required" format:"date-time"`
21+
LoginType LoginType `json:"login_type" validate:"required" enums:"password,github,oidc,token"`
22+
Scope APIKeyScope `json:"scope" enums:"all,application_connect"` // Deprecated: use Scopes instead.
23+
Scopes []APIKeyScope `json:"scopes"`
24+
TokenName string `json:"token_name" validate:"required"`
25+
LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"`
26+
AllowList []APIAllowListTarget `json:"allow_list"`
2627
}
2728

2829
// LoginType is the type of login used to create the API key.

0 commit comments

Comments
 (0)