feat: add allow list to API keys (#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 #19854
This commit is contained in:
Thomas Kosiewski
2025-10-24 12:33:56 +01:00
committed by GitHub
parent f947a34103
commit f684831f56
14 changed files with 162 additions and 47 deletions
+6
View File
@@ -11894,6 +11894,12 @@ const docTemplate = `{
"user_id"
],
"properties": {
"allow_list": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.APIAllowListTarget"
}
},
"created_at": {
"type": "string",
"format": "date-time"
+6
View File
@@ -10590,6 +10590,12 @@
"user_id"
],
"properties": {
"allow_list": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.APIAllowListTarget"
}
},
"created_at": {
"type": "string",
"format": "date-time"
+6
View File
@@ -51,6 +51,8 @@ func TestTokenCRUD(t *testing.T) {
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*24*6))
require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*24*8))
require.Equal(t, codersdk.APIKeyScopeAll, keys[0].Scope)
require.Len(t, keys[0].AllowList, 1)
require.Equal(t, "*:*", keys[0].AllowList[0].String())
// no update
@@ -86,6 +88,8 @@ func TestTokenScoped(t *testing.T) {
require.EqualValues(t, len(keys), 1)
require.Contains(t, res.Key, keys[0].ID)
require.Equal(t, keys[0].Scope, codersdk.APIKeyScopeApplicationConnect)
require.Len(t, keys[0].AllowList, 1)
require.Equal(t, "*:*", keys[0].AllowList[0].String())
}
// Ensure backward-compat: when a token is created using the legacy singular
@@ -132,6 +136,8 @@ func TestTokenLegacySingularScopeCompat(t *testing.T) {
require.Len(t, keys, 1)
require.Equal(t, tc.scope, keys[0].Scope)
require.ElementsMatch(t, keys[0].Scopes, tc.scopes)
require.Len(t, keys[0].AllowList, 1)
require.Equal(t, "*:*", keys[0].AllowList[0].String())
})
}
}
+1
View File
@@ -6,6 +6,7 @@ type CheckConstraint string
// CheckConstraint enums.
const (
CheckAPIKeysAllowListNotEmpty CheckConstraint = "api_keys_allow_list_not_empty" // api_keys
CheckOneTimePasscodeSet CheckConstraint = "one_time_passcode_set" // users
CheckUsersUsernameMinLength CheckConstraint = "users_username_min_length" // users
CheckMaxProvisionerLogsLength CheckConstraint = "max_provisioner_logs_length" // provisioner_jobs
+7
View File
@@ -51,6 +51,13 @@ func ListLazy[F any, T any](convert func(F) T) func(list []F) []T {
}
}
func APIAllowListTarget(entry rbac.AllowListElement) codersdk.APIAllowListTarget {
return codersdk.APIAllowListTarget{
Type: codersdk.RBACResource(entry.Type),
ID: entry.ID,
}
}
type ExternalAuthMeta struct {
Authenticated bool
ValidateError string
+2 -1
View File
@@ -1126,7 +1126,8 @@ CREATE TABLE api_keys (
ip_address inet DEFAULT '0.0.0.0'::inet NOT NULL,
token_name text DEFAULT ''::text NOT NULL,
scopes api_key_scope[] NOT NULL,
allow_list text[] NOT NULL
allow_list text[] NOT NULL,
CONSTRAINT api_keys_allow_list_not_empty CHECK ((array_length(allow_list, 1) > 0))
);
COMMENT ON COLUMN api_keys.hashed_secret IS 'hashed_secret contains a SHA256 hash of the key secret. This is considered a secret and MUST NOT be returned from the API as it is used for API key encryption in app proxying code.';
@@ -0,0 +1,3 @@
-- Drop all CHECK constraints added in the up migration
ALTER TABLE api_keys
DROP CONSTRAINT api_keys_allow_list_not_empty;
@@ -0,0 +1,10 @@
-- Defensively update any API keys with empty allow_list to have default '*:*'
-- This ensures all existing keys have at least one entry before adding the constraint
UPDATE api_keys
SET allow_list = ARRAY['*:*']
WHERE allow_list = ARRAY[]::text[] OR array_length(allow_list, 1) IS NULL;
-- Add CHECK constraint to ensure allow_list array is never empty
ALTER TABLE api_keys
ADD CONSTRAINT api_keys_allow_list_not_empty
CHECK (array_length(allow_list, 1) > 0);
+1
View File
@@ -1608,5 +1608,6 @@ func convertAPIKey(k database.APIKey) codersdk.APIKey {
Scopes: scopes,
LifetimeSeconds: k.LifetimeSeconds,
TokenName: k.TokenName,
AllowList: db2sdk.List(k.AllowList, db2sdk.APIAllowListTarget),
}
}