mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: implement API key scopes database migration (#19861)
Added database migration for API key scopes. Fixes #19845
This commit is contained in:
@@ -92,7 +92,8 @@ func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error)
|
||||
UpdatedAt: dbtime.Now(),
|
||||
HashedSecret: hashed[:],
|
||||
LoginType: params.LoginType,
|
||||
Scope: scope,
|
||||
Scopes: database.APIKeyScopes{scope},
|
||||
AllowList: database.AllowList{database.AllowListWildcard()},
|
||||
TokenName: params.TokenName,
|
||||
}, token, nil
|
||||
}
|
||||
|
||||
@@ -159,9 +159,9 @@ func TestGenerate(t *testing.T) {
|
||||
}
|
||||
|
||||
if tc.params.Scope != "" {
|
||||
assert.Equal(t, tc.params.Scope, key.Scope)
|
||||
assert.True(t, key.Scopes.Has(tc.params.Scope))
|
||||
} else {
|
||||
assert.Equal(t, database.APIKeyScopeAll, key.Scope)
|
||||
assert.True(t, key.Scopes.Has(database.APIKeyScopeAll))
|
||||
}
|
||||
|
||||
if tc.params.TokenName != "" {
|
||||
|
||||
@@ -68,7 +68,7 @@ func AssertRBAC(t *testing.T, api *coderd.API, client *codersdk.Client) RBACAsse
|
||||
ID: key.UserID.String(),
|
||||
Roles: rbac.RoleIdentifiers(roleNames),
|
||||
Groups: roles.Groups,
|
||||
Scope: rbac.ScopeName(key.Scope),
|
||||
Scope: key.Scopes,
|
||||
},
|
||||
Recorder: recorder,
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ func (s *MethodTestSuite) TestAPIKey() {
|
||||
}))
|
||||
s.Run("InsertAPIKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
u := testutil.Fake(s.T(), faker, database.User{})
|
||||
arg := database.InsertAPIKeyParams{UserID: u.ID, LoginType: database.LoginTypePassword, Scope: database.APIKeyScopeAll, IPAddress: defaultIPAddress()}
|
||||
arg := database.InsertAPIKeyParams{UserID: u.ID, LoginType: database.LoginTypePassword, Scopes: database.APIKeyScopes{database.APIKeyScopeAll}, IPAddress: defaultIPAddress()}
|
||||
ret := testutil.Fake(s.T(), faker, database.APIKey{UserID: u.ID, LoginType: database.LoginTypePassword})
|
||||
dbm.EXPECT().InsertAPIKey(gomock.Any(), arg).Return(ret, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceApiKey.WithOwner(u.ID.String()), policy.ActionCreate)
|
||||
@@ -265,7 +265,7 @@ func (s *MethodTestSuite) TestAPIKey() {
|
||||
check.Args(arg).Asserts(a, policy.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("DeleteApplicationConnectAPIKeysByUserID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
a := testutil.Fake(s.T(), faker, database.APIKey{Scope: database.APIKeyScopeApplicationConnect})
|
||||
a := testutil.Fake(s.T(), faker, database.APIKey{Scopes: database.APIKeyScopes{database.APIKeyScopeApplicationConnect}})
|
||||
dbm.EXPECT().DeleteApplicationConnectAPIKeysByUserID(gomock.Any(), a.UserID).Return(nil).AnyTimes()
|
||||
check.Args(a.UserID).Asserts(rbac.ResourceApiKey.WithOwner(a.UserID.String()), policy.ActionDelete).Returns()
|
||||
}))
|
||||
|
||||
@@ -185,7 +185,8 @@ func APIKey(t testing.TB, db database.Store, seed database.APIKey, munge ...func
|
||||
CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()),
|
||||
UpdatedAt: takeFirst(seed.UpdatedAt, dbtime.Now()),
|
||||
LoginType: takeFirst(seed.LoginType, database.LoginTypePassword),
|
||||
Scope: takeFirst(seed.Scope, database.APIKeyScopeAll),
|
||||
Scopes: takeFirstSlice([]database.APIKeyScope(seed.Scopes), []database.APIKeyScope{database.APIKeyScopeAll}),
|
||||
AllowList: takeFirstSlice(seed.AllowList, database.AllowList{database.AllowListWildcard()}),
|
||||
TokenName: takeFirst(seed.TokenName),
|
||||
}
|
||||
for _, fn := range munge {
|
||||
|
||||
Generated
+142
-3
@@ -12,7 +12,145 @@ CREATE TYPE agent_key_scope_enum AS ENUM (
|
||||
|
||||
CREATE TYPE api_key_scope AS ENUM (
|
||||
'all',
|
||||
'application_connect'
|
||||
'application_connect',
|
||||
'aibridge_interception:create',
|
||||
'aibridge_interception:read',
|
||||
'aibridge_interception:update',
|
||||
'api_key:create',
|
||||
'api_key:delete',
|
||||
'api_key:read',
|
||||
'api_key:update',
|
||||
'assign_org_role:assign',
|
||||
'assign_org_role:create',
|
||||
'assign_org_role:delete',
|
||||
'assign_org_role:read',
|
||||
'assign_org_role:unassign',
|
||||
'assign_org_role:update',
|
||||
'assign_role:assign',
|
||||
'assign_role:read',
|
||||
'assign_role:unassign',
|
||||
'audit_log:create',
|
||||
'audit_log:read',
|
||||
'connection_log:read',
|
||||
'connection_log:update',
|
||||
'crypto_key:create',
|
||||
'crypto_key:delete',
|
||||
'crypto_key:read',
|
||||
'crypto_key:update',
|
||||
'debug_info:read',
|
||||
'deployment_config:read',
|
||||
'deployment_config:update',
|
||||
'deployment_stats:read',
|
||||
'file:create',
|
||||
'file:read',
|
||||
'group:create',
|
||||
'group:delete',
|
||||
'group:read',
|
||||
'group:update',
|
||||
'group_member:read',
|
||||
'idpsync_settings:read',
|
||||
'idpsync_settings:update',
|
||||
'inbox_notification:create',
|
||||
'inbox_notification:read',
|
||||
'inbox_notification:update',
|
||||
'license:create',
|
||||
'license:delete',
|
||||
'license:read',
|
||||
'notification_message:create',
|
||||
'notification_message:delete',
|
||||
'notification_message:read',
|
||||
'notification_message:update',
|
||||
'notification_preference:read',
|
||||
'notification_preference:update',
|
||||
'notification_template:read',
|
||||
'notification_template:update',
|
||||
'oauth2_app:create',
|
||||
'oauth2_app:delete',
|
||||
'oauth2_app:read',
|
||||
'oauth2_app:update',
|
||||
'oauth2_app_code_token:create',
|
||||
'oauth2_app_code_token:delete',
|
||||
'oauth2_app_code_token:read',
|
||||
'oauth2_app_secret:create',
|
||||
'oauth2_app_secret:delete',
|
||||
'oauth2_app_secret:read',
|
||||
'oauth2_app_secret:update',
|
||||
'organization:create',
|
||||
'organization:delete',
|
||||
'organization:read',
|
||||
'organization:update',
|
||||
'organization_member:create',
|
||||
'organization_member:delete',
|
||||
'organization_member:read',
|
||||
'organization_member:update',
|
||||
'prebuilt_workspace:delete',
|
||||
'prebuilt_workspace:update',
|
||||
'provisioner_daemon:create',
|
||||
'provisioner_daemon:delete',
|
||||
'provisioner_daemon:read',
|
||||
'provisioner_daemon:update',
|
||||
'provisioner_jobs:create',
|
||||
'provisioner_jobs:read',
|
||||
'provisioner_jobs:update',
|
||||
'replicas:read',
|
||||
'system:create',
|
||||
'system:delete',
|
||||
'system:read',
|
||||
'system:update',
|
||||
'tailnet_coordinator:create',
|
||||
'tailnet_coordinator:delete',
|
||||
'tailnet_coordinator:read',
|
||||
'tailnet_coordinator:update',
|
||||
'template:create',
|
||||
'template:delete',
|
||||
'template:read',
|
||||
'template:update',
|
||||
'template:use',
|
||||
'template:view_insights',
|
||||
'usage_event:create',
|
||||
'usage_event:read',
|
||||
'usage_event:update',
|
||||
'user:create',
|
||||
'user:delete',
|
||||
'user:read',
|
||||
'user:read_personal',
|
||||
'user:update',
|
||||
'user:update_personal',
|
||||
'user_secret:create',
|
||||
'user_secret:delete',
|
||||
'user_secret:read',
|
||||
'user_secret:update',
|
||||
'webpush_subscription:create',
|
||||
'webpush_subscription:delete',
|
||||
'webpush_subscription:read',
|
||||
'workspace:application_connect',
|
||||
'workspace:create',
|
||||
'workspace:create_agent',
|
||||
'workspace:delete',
|
||||
'workspace:delete_agent',
|
||||
'workspace:read',
|
||||
'workspace:ssh',
|
||||
'workspace:start',
|
||||
'workspace:stop',
|
||||
'workspace:update',
|
||||
'workspace_agent_devcontainers:create',
|
||||
'workspace_agent_resource_monitor:create',
|
||||
'workspace_agent_resource_monitor:read',
|
||||
'workspace_agent_resource_monitor:update',
|
||||
'workspace_dormant:application_connect',
|
||||
'workspace_dormant:create',
|
||||
'workspace_dormant:create_agent',
|
||||
'workspace_dormant:delete',
|
||||
'workspace_dormant:delete_agent',
|
||||
'workspace_dormant:read',
|
||||
'workspace_dormant:ssh',
|
||||
'workspace_dormant:start',
|
||||
'workspace_dormant:stop',
|
||||
'workspace_dormant:update',
|
||||
'workspace_proxy:create',
|
||||
'workspace_proxy:delete',
|
||||
'workspace_proxy:read',
|
||||
'workspace_proxy:update'
|
||||
);
|
||||
|
||||
CREATE TYPE app_sharing_level AS ENUM (
|
||||
@@ -920,8 +1058,9 @@ CREATE TABLE api_keys (
|
||||
login_type login_type NOT NULL,
|
||||
lifetime_seconds bigint DEFAULT 86400 NOT NULL,
|
||||
ip_address inet DEFAULT '0.0.0.0'::inet NOT NULL,
|
||||
scope api_key_scope DEFAULT 'all'::api_key_scope NOT NULL,
|
||||
token_name text DEFAULT ''::text NOT NULL
|
||||
token_name text DEFAULT ''::text NOT NULL,
|
||||
scopes api_key_scope[] NOT NULL,
|
||||
allow_list text[] NOT NULL
|
||||
);
|
||||
|
||||
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,18 @@
|
||||
-- Recreate single-scope column and collapse arrays
|
||||
ALTER TABLE api_keys ADD COLUMN scope api_key_scope DEFAULT 'all'::api_key_scope NOT NULL;
|
||||
|
||||
-- Collapse logic: prefer 'all', else 'application_connect', else 'all'
|
||||
UPDATE api_keys SET scope =
|
||||
CASE
|
||||
WHEN 'all'::api_key_scope = ANY(scopes) THEN 'all'::api_key_scope
|
||||
WHEN 'application_connect'::api_key_scope = ANY(scopes) THEN 'application_connect'::api_key_scope
|
||||
ELSE 'all'::api_key_scope
|
||||
END;
|
||||
|
||||
-- Drop new columns
|
||||
ALTER TABLE api_keys DROP COLUMN allow_list;
|
||||
ALTER TABLE api_keys DROP COLUMN scopes;
|
||||
|
||||
-- Note: We intentionally keep the expanded enum values to avoid dependency churn.
|
||||
-- If strict narrowing is required, create a new type with only ('all','application_connect'),
|
||||
-- cast column, drop the new type, and rename.
|
||||
@@ -0,0 +1,157 @@
|
||||
-- Extend api_key_scope enum with low-level <resource>:<action> values derived from RBACPermissions
|
||||
-- Generated via: go run ./scripts/generate_api_key_scope_enum
|
||||
-- Begin enum extensions
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'aibridge_interception:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'aibridge_interception:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'aibridge_interception:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'api_key:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'api_key:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'api_key:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'api_key:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_org_role:assign';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_org_role:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_org_role:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_org_role:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_org_role:unassign';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_org_role:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_role:assign';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_role:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'assign_role:unassign';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'audit_log:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'audit_log:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'connection_log:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'connection_log:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'crypto_key:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'crypto_key:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'crypto_key:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'crypto_key:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'debug_info:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'deployment_config:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'deployment_config:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'deployment_stats:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'file:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'file:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'group:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'group:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'group:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'group:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'group_member:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'idpsync_settings:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'idpsync_settings:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'inbox_notification:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'inbox_notification:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'inbox_notification:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'license:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'license:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'license:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'notification_message:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'notification_message:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'notification_message:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'notification_message:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'notification_preference:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'notification_preference:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'notification_template:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'notification_template:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app_code_token:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app_code_token:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app_code_token:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app_secret:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app_secret:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app_secret:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'oauth2_app_secret:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization_member:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization_member:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization_member:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization_member:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'prebuilt_workspace:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'prebuilt_workspace:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'provisioner_daemon:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'provisioner_daemon:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'provisioner_daemon:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'provisioner_daemon:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'provisioner_jobs:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'provisioner_jobs:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'provisioner_jobs:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'replicas:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'system:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'system:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'system:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'system:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'tailnet_coordinator:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'tailnet_coordinator:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'tailnet_coordinator:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'tailnet_coordinator:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'template:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'template:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'template:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'template:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'template:use';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'template:view_insights';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'usage_event:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'usage_event:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'usage_event:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user:read_personal';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user:update_personal';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user_secret:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user_secret:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user_secret:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user_secret:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'webpush_subscription:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'webpush_subscription:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'webpush_subscription:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:application_connect';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:create_agent';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:delete_agent';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:ssh';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:start';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:stop';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_agent_devcontainers:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_agent_resource_monitor:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_agent_resource_monitor:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_agent_resource_monitor:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:application_connect';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:create_agent';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:delete_agent';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:ssh';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:start';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:stop';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_proxy:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_proxy:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_proxy:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_proxy:update';
|
||||
-- End enum extensions
|
||||
|
||||
-- Add new columns without defaults; backfill; then enforce NOT NULL
|
||||
ALTER TABLE api_keys ADD COLUMN scopes api_key_scope[];
|
||||
ALTER TABLE api_keys ADD COLUMN allow_list text[];
|
||||
|
||||
-- Backfill existing rows for compatibility
|
||||
UPDATE api_keys SET scopes = ARRAY[scope::api_key_scope];
|
||||
UPDATE api_keys SET allow_list = ARRAY['*:*'];
|
||||
|
||||
-- Enforce NOT NULL
|
||||
ALTER TABLE api_keys ALTER COLUMN scopes SET NOT NULL;
|
||||
ALTER TABLE api_keys ALTER COLUMN allow_list SET NOT NULL;
|
||||
|
||||
-- Drop legacy single-scope column
|
||||
ALTER TABLE api_keys DROP COLUMN scope;
|
||||
@@ -2,8 +2,10 @@ package database
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -137,10 +139,94 @@ func (s APIKeyScope) ToRBAC() rbac.ScopeName {
|
||||
case APIKeyScopeApplicationConnect:
|
||||
return rbac.ScopeApplicationConnect
|
||||
default:
|
||||
panic("developer error: unknown scope type " + string(s))
|
||||
// Allow low-level resource:action scopes to flow through to RBAC for
|
||||
// expansion via rbac.ExpandScope.
|
||||
return rbac.ScopeName(s)
|
||||
}
|
||||
}
|
||||
|
||||
// APIKeyScopes allows expanding multiple API key scopes into a single
|
||||
// RBAC scope for authorization. This implements rbac.ExpandableScope so
|
||||
// callers can pass the list directly without deriving a single scope.
|
||||
type APIKeyScopes []APIKeyScope
|
||||
|
||||
var _ rbac.ExpandableScope = APIKeyScopes{}
|
||||
|
||||
// Has returns true if the slice contains the provided scope.
|
||||
func (s APIKeyScopes) Has(target APIKeyScope) bool {
|
||||
return slices.Contains(s, target)
|
||||
}
|
||||
|
||||
// Expand merges the permissions of all scopes in the list into a single scope.
|
||||
// If the list is empty, it defaults to rbac.ScopeAll.
|
||||
func (s APIKeyScopes) Expand() (rbac.Scope, error) {
|
||||
// Default to ScopeAll for backward compatibility when no scopes provided.
|
||||
if len(s) == 0 {
|
||||
return rbac.ScopeAll.Expand()
|
||||
}
|
||||
|
||||
var merged rbac.Scope
|
||||
merged.Role = rbac.Role{
|
||||
// Identifier is informational; not used in policy evaluation.
|
||||
Identifier: rbac.RoleIdentifier{Name: "Scope_Multiple"},
|
||||
Site: nil,
|
||||
Org: map[string][]rbac.Permission{},
|
||||
User: nil,
|
||||
}
|
||||
|
||||
// Track allow list union, collapsing to wildcard if any child is wildcard.
|
||||
allowAll := false
|
||||
allowSet := make(map[string]rbac.AllowListElement)
|
||||
|
||||
for _, s := range s {
|
||||
expanded, err := s.ToRBAC().Expand()
|
||||
if err != nil {
|
||||
return rbac.Scope{}, err
|
||||
}
|
||||
|
||||
// Merge role permissions: union by simple concatenation.
|
||||
merged.Site = append(merged.Site, expanded.Site...)
|
||||
for orgID, perms := range expanded.Org {
|
||||
merged.Org[orgID] = append(merged.Org[orgID], perms...)
|
||||
}
|
||||
merged.User = append(merged.User, expanded.User...)
|
||||
|
||||
// Merge allow lists.
|
||||
for _, e := range expanded.AllowIDList {
|
||||
if e.ID == policy.WildcardSymbol && e.Type == policy.WildcardSymbol {
|
||||
allowAll = true
|
||||
// No need to track other entries once wildcard is present.
|
||||
continue
|
||||
}
|
||||
key := e.String()
|
||||
allowSet[key] = e
|
||||
}
|
||||
}
|
||||
|
||||
if allowAll || len(allowSet) == 0 {
|
||||
merged.AllowIDList = []rbac.AllowListElement{rbac.AllowListAll()}
|
||||
} else {
|
||||
merged.AllowIDList = make([]rbac.AllowListElement, 0, len(allowSet))
|
||||
for _, v := range allowSet {
|
||||
merged.AllowIDList = append(merged.AllowIDList, v)
|
||||
}
|
||||
}
|
||||
|
||||
return merged, nil
|
||||
}
|
||||
|
||||
// Name returns a human-friendly identifier for tracing/logging.
|
||||
func (s APIKeyScopes) Name() rbac.RoleIdentifier {
|
||||
if len(s) == 0 {
|
||||
return rbac.RoleIdentifier{Name: string(APIKeyScopeAll)}
|
||||
}
|
||||
names := make([]string, 0, len(s))
|
||||
for _, s := range s {
|
||||
names = append(names, string(s))
|
||||
}
|
||||
return rbac.RoleIdentifier{Name: "scopes[" + strings.Join(names, "+") + "]"}
|
||||
}
|
||||
|
||||
func (k APIKey) RBACObject() rbac.Object {
|
||||
return rbac.ResourceApiKey.WithIDString(k.ID).
|
||||
WithOwner(k.UserID.String())
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
)
|
||||
|
||||
func TestAPIKeyScopesExpand(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("builtins", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
scopes APIKeyScopes
|
||||
want func(t *testing.T, s rbac.Scope)
|
||||
}{
|
||||
{
|
||||
name: "all",
|
||||
scopes: APIKeyScopes{APIKeyScopeAll},
|
||||
want: func(t *testing.T, s rbac.Scope) {
|
||||
requirePermission(t, s, rbac.ResourceWildcard.Type, policy.Action(policy.WildcardSymbol))
|
||||
requireAllowAll(t, s)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "application_connect",
|
||||
scopes: APIKeyScopes{APIKeyScopeApplicationConnect},
|
||||
want: func(t *testing.T, s rbac.Scope) {
|
||||
requirePermission(t, s, rbac.ResourceWorkspace.Type, policy.ActionApplicationConnect)
|
||||
requireAllowAll(t, s)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
s, err := tc.scopes.Expand()
|
||||
require.NoError(t, err)
|
||||
tc.want(t, s)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("low_level_pairs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
scopes APIKeyScopes
|
||||
res string
|
||||
act policy.Action
|
||||
}{
|
||||
{name: "workspace:read", scopes: APIKeyScopes{ApiKeyScopeWorkspaceRead}, res: rbac.ResourceWorkspace.Type, act: policy.ActionRead},
|
||||
{name: "template:use", scopes: APIKeyScopes{ApiKeyScopeTemplateUse}, res: rbac.ResourceTemplate.Type, act: policy.ActionUse},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
s, err := tc.scopes.Expand()
|
||||
require.NoError(t, err)
|
||||
requirePermission(t, s, tc.res, tc.act)
|
||||
requireAllowAll(t, s)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("merge", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
scopes := APIKeyScopes{APIKeyScopeApplicationConnect, APIKeyScopeAll, ApiKeyScopeWorkspaceRead}
|
||||
s, err := scopes.Expand()
|
||||
require.NoError(t, err)
|
||||
requirePermission(t, s, rbac.ResourceWildcard.Type, policy.Action(policy.WildcardSymbol))
|
||||
requirePermission(t, s, rbac.ResourceWorkspace.Type, policy.ActionApplicationConnect)
|
||||
requirePermission(t, s, rbac.ResourceWorkspace.Type, policy.ActionRead)
|
||||
requireAllowAll(t, s)
|
||||
})
|
||||
|
||||
t.Run("empty_defaults_to_all", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
s, err := (APIKeyScopes{}).Expand()
|
||||
require.NoError(t, err)
|
||||
requirePermission(t, s, rbac.ResourceWildcard.Type, policy.Action(policy.WildcardSymbol))
|
||||
requireAllowAll(t, s)
|
||||
})
|
||||
}
|
||||
|
||||
// Helpers
|
||||
func requirePermission(t *testing.T, s rbac.Scope, resource string, action policy.Action) {
|
||||
t.Helper()
|
||||
for _, p := range s.Site {
|
||||
if p.ResourceType == resource && p.Action == action {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("permission not found: %s:%s", resource, action)
|
||||
}
|
||||
|
||||
func requireAllowAll(t *testing.T, s rbac.Scope) {
|
||||
t.Helper()
|
||||
require.Len(t, s.AllowIDList, 1)
|
||||
require.Equal(t, policy.WildcardSymbol, s.AllowIDList[0].ID)
|
||||
require.Equal(t, policy.WildcardSymbol, s.AllowIDList[0].Type)
|
||||
}
|
||||
+429
-14
@@ -19,8 +19,146 @@ import (
|
||||
type APIKeyScope string
|
||||
|
||||
const (
|
||||
APIKeyScopeAll APIKeyScope = "all"
|
||||
APIKeyScopeApplicationConnect APIKeyScope = "application_connect"
|
||||
APIKeyScopeAll APIKeyScope = "all"
|
||||
APIKeyScopeApplicationConnect APIKeyScope = "application_connect"
|
||||
ApiKeyScopeAibridgeInterceptionCreate APIKeyScope = "aibridge_interception:create"
|
||||
ApiKeyScopeAibridgeInterceptionRead APIKeyScope = "aibridge_interception:read"
|
||||
ApiKeyScopeAibridgeInterceptionUpdate APIKeyScope = "aibridge_interception:update"
|
||||
ApiKeyScopeApiKeyCreate APIKeyScope = "api_key:create"
|
||||
ApiKeyScopeApiKeyDelete APIKeyScope = "api_key:delete"
|
||||
ApiKeyScopeApiKeyRead APIKeyScope = "api_key:read"
|
||||
ApiKeyScopeApiKeyUpdate APIKeyScope = "api_key:update"
|
||||
ApiKeyScopeAssignOrgRoleAssign APIKeyScope = "assign_org_role:assign"
|
||||
ApiKeyScopeAssignOrgRoleCreate APIKeyScope = "assign_org_role:create"
|
||||
ApiKeyScopeAssignOrgRoleDelete APIKeyScope = "assign_org_role:delete"
|
||||
ApiKeyScopeAssignOrgRoleRead APIKeyScope = "assign_org_role:read"
|
||||
ApiKeyScopeAssignOrgRoleUnassign APIKeyScope = "assign_org_role:unassign"
|
||||
ApiKeyScopeAssignOrgRoleUpdate APIKeyScope = "assign_org_role:update"
|
||||
ApiKeyScopeAssignRoleAssign APIKeyScope = "assign_role:assign"
|
||||
ApiKeyScopeAssignRoleRead APIKeyScope = "assign_role:read"
|
||||
ApiKeyScopeAssignRoleUnassign APIKeyScope = "assign_role:unassign"
|
||||
ApiKeyScopeAuditLogCreate APIKeyScope = "audit_log:create"
|
||||
ApiKeyScopeAuditLogRead APIKeyScope = "audit_log:read"
|
||||
ApiKeyScopeConnectionLogRead APIKeyScope = "connection_log:read"
|
||||
ApiKeyScopeConnectionLogUpdate APIKeyScope = "connection_log:update"
|
||||
ApiKeyScopeCryptoKeyCreate APIKeyScope = "crypto_key:create"
|
||||
ApiKeyScopeCryptoKeyDelete APIKeyScope = "crypto_key:delete"
|
||||
ApiKeyScopeCryptoKeyRead APIKeyScope = "crypto_key:read"
|
||||
ApiKeyScopeCryptoKeyUpdate APIKeyScope = "crypto_key:update"
|
||||
ApiKeyScopeDebugInfoRead APIKeyScope = "debug_info:read"
|
||||
ApiKeyScopeDeploymentConfigRead APIKeyScope = "deployment_config:read"
|
||||
ApiKeyScopeDeploymentConfigUpdate APIKeyScope = "deployment_config:update"
|
||||
ApiKeyScopeDeploymentStatsRead APIKeyScope = "deployment_stats:read"
|
||||
ApiKeyScopeFileCreate APIKeyScope = "file:create"
|
||||
ApiKeyScopeFileRead APIKeyScope = "file:read"
|
||||
ApiKeyScopeGroupCreate APIKeyScope = "group:create"
|
||||
ApiKeyScopeGroupDelete APIKeyScope = "group:delete"
|
||||
ApiKeyScopeGroupRead APIKeyScope = "group:read"
|
||||
ApiKeyScopeGroupUpdate APIKeyScope = "group:update"
|
||||
ApiKeyScopeGroupMemberRead APIKeyScope = "group_member:read"
|
||||
ApiKeyScopeIdpsyncSettingsRead APIKeyScope = "idpsync_settings:read"
|
||||
ApiKeyScopeIdpsyncSettingsUpdate APIKeyScope = "idpsync_settings:update"
|
||||
ApiKeyScopeInboxNotificationCreate APIKeyScope = "inbox_notification:create"
|
||||
ApiKeyScopeInboxNotificationRead APIKeyScope = "inbox_notification:read"
|
||||
ApiKeyScopeInboxNotificationUpdate APIKeyScope = "inbox_notification:update"
|
||||
ApiKeyScopeLicenseCreate APIKeyScope = "license:create"
|
||||
ApiKeyScopeLicenseDelete APIKeyScope = "license:delete"
|
||||
ApiKeyScopeLicenseRead APIKeyScope = "license:read"
|
||||
ApiKeyScopeNotificationMessageCreate APIKeyScope = "notification_message:create"
|
||||
ApiKeyScopeNotificationMessageDelete APIKeyScope = "notification_message:delete"
|
||||
ApiKeyScopeNotificationMessageRead APIKeyScope = "notification_message:read"
|
||||
ApiKeyScopeNotificationMessageUpdate APIKeyScope = "notification_message:update"
|
||||
ApiKeyScopeNotificationPreferenceRead APIKeyScope = "notification_preference:read"
|
||||
ApiKeyScopeNotificationPreferenceUpdate APIKeyScope = "notification_preference:update"
|
||||
ApiKeyScopeNotificationTemplateRead APIKeyScope = "notification_template:read"
|
||||
ApiKeyScopeNotificationTemplateUpdate APIKeyScope = "notification_template:update"
|
||||
ApiKeyScopeOauth2AppCreate APIKeyScope = "oauth2_app:create"
|
||||
ApiKeyScopeOauth2AppDelete APIKeyScope = "oauth2_app:delete"
|
||||
ApiKeyScopeOauth2AppRead APIKeyScope = "oauth2_app:read"
|
||||
ApiKeyScopeOauth2AppUpdate APIKeyScope = "oauth2_app:update"
|
||||
ApiKeyScopeOauth2AppCodeTokenCreate APIKeyScope = "oauth2_app_code_token:create"
|
||||
ApiKeyScopeOauth2AppCodeTokenDelete APIKeyScope = "oauth2_app_code_token:delete"
|
||||
ApiKeyScopeOauth2AppCodeTokenRead APIKeyScope = "oauth2_app_code_token:read"
|
||||
ApiKeyScopeOauth2AppSecretCreate APIKeyScope = "oauth2_app_secret:create"
|
||||
ApiKeyScopeOauth2AppSecretDelete APIKeyScope = "oauth2_app_secret:delete"
|
||||
ApiKeyScopeOauth2AppSecretRead APIKeyScope = "oauth2_app_secret:read"
|
||||
ApiKeyScopeOauth2AppSecretUpdate APIKeyScope = "oauth2_app_secret:update"
|
||||
ApiKeyScopeOrganizationCreate APIKeyScope = "organization:create"
|
||||
ApiKeyScopeOrganizationDelete APIKeyScope = "organization:delete"
|
||||
ApiKeyScopeOrganizationRead APIKeyScope = "organization:read"
|
||||
ApiKeyScopeOrganizationUpdate APIKeyScope = "organization:update"
|
||||
ApiKeyScopeOrganizationMemberCreate APIKeyScope = "organization_member:create"
|
||||
ApiKeyScopeOrganizationMemberDelete APIKeyScope = "organization_member:delete"
|
||||
ApiKeyScopeOrganizationMemberRead APIKeyScope = "organization_member:read"
|
||||
ApiKeyScopeOrganizationMemberUpdate APIKeyScope = "organization_member:update"
|
||||
ApiKeyScopePrebuiltWorkspaceDelete APIKeyScope = "prebuilt_workspace:delete"
|
||||
ApiKeyScopePrebuiltWorkspaceUpdate APIKeyScope = "prebuilt_workspace:update"
|
||||
ApiKeyScopeProvisionerDaemonCreate APIKeyScope = "provisioner_daemon:create"
|
||||
ApiKeyScopeProvisionerDaemonDelete APIKeyScope = "provisioner_daemon:delete"
|
||||
ApiKeyScopeProvisionerDaemonRead APIKeyScope = "provisioner_daemon:read"
|
||||
ApiKeyScopeProvisionerDaemonUpdate APIKeyScope = "provisioner_daemon:update"
|
||||
ApiKeyScopeProvisionerJobsCreate APIKeyScope = "provisioner_jobs:create"
|
||||
ApiKeyScopeProvisionerJobsRead APIKeyScope = "provisioner_jobs:read"
|
||||
ApiKeyScopeProvisionerJobsUpdate APIKeyScope = "provisioner_jobs:update"
|
||||
ApiKeyScopeReplicasRead APIKeyScope = "replicas:read"
|
||||
ApiKeyScopeSystemCreate APIKeyScope = "system:create"
|
||||
ApiKeyScopeSystemDelete APIKeyScope = "system:delete"
|
||||
ApiKeyScopeSystemRead APIKeyScope = "system:read"
|
||||
ApiKeyScopeSystemUpdate APIKeyScope = "system:update"
|
||||
ApiKeyScopeTailnetCoordinatorCreate APIKeyScope = "tailnet_coordinator:create"
|
||||
ApiKeyScopeTailnetCoordinatorDelete APIKeyScope = "tailnet_coordinator:delete"
|
||||
ApiKeyScopeTailnetCoordinatorRead APIKeyScope = "tailnet_coordinator:read"
|
||||
ApiKeyScopeTailnetCoordinatorUpdate APIKeyScope = "tailnet_coordinator:update"
|
||||
ApiKeyScopeTemplateCreate APIKeyScope = "template:create"
|
||||
ApiKeyScopeTemplateDelete APIKeyScope = "template:delete"
|
||||
ApiKeyScopeTemplateRead APIKeyScope = "template:read"
|
||||
ApiKeyScopeTemplateUpdate APIKeyScope = "template:update"
|
||||
ApiKeyScopeTemplateUse APIKeyScope = "template:use"
|
||||
ApiKeyScopeTemplateViewInsights APIKeyScope = "template:view_insights"
|
||||
ApiKeyScopeUsageEventCreate APIKeyScope = "usage_event:create"
|
||||
ApiKeyScopeUsageEventRead APIKeyScope = "usage_event:read"
|
||||
ApiKeyScopeUsageEventUpdate APIKeyScope = "usage_event:update"
|
||||
ApiKeyScopeUserCreate APIKeyScope = "user:create"
|
||||
ApiKeyScopeUserDelete APIKeyScope = "user:delete"
|
||||
ApiKeyScopeUserRead APIKeyScope = "user:read"
|
||||
ApiKeyScopeUserReadPersonal APIKeyScope = "user:read_personal"
|
||||
ApiKeyScopeUserUpdate APIKeyScope = "user:update"
|
||||
ApiKeyScopeUserUpdatePersonal APIKeyScope = "user:update_personal"
|
||||
ApiKeyScopeUserSecretCreate APIKeyScope = "user_secret:create"
|
||||
ApiKeyScopeUserSecretDelete APIKeyScope = "user_secret:delete"
|
||||
ApiKeyScopeUserSecretRead APIKeyScope = "user_secret:read"
|
||||
ApiKeyScopeUserSecretUpdate APIKeyScope = "user_secret:update"
|
||||
ApiKeyScopeWebpushSubscriptionCreate APIKeyScope = "webpush_subscription:create"
|
||||
ApiKeyScopeWebpushSubscriptionDelete APIKeyScope = "webpush_subscription:delete"
|
||||
ApiKeyScopeWebpushSubscriptionRead APIKeyScope = "webpush_subscription:read"
|
||||
ApiKeyScopeWorkspaceApplicationConnect APIKeyScope = "workspace:application_connect"
|
||||
ApiKeyScopeWorkspaceCreate APIKeyScope = "workspace:create"
|
||||
ApiKeyScopeWorkspaceCreateAgent APIKeyScope = "workspace:create_agent"
|
||||
ApiKeyScopeWorkspaceDelete APIKeyScope = "workspace:delete"
|
||||
ApiKeyScopeWorkspaceDeleteAgent APIKeyScope = "workspace:delete_agent"
|
||||
ApiKeyScopeWorkspaceRead APIKeyScope = "workspace:read"
|
||||
ApiKeyScopeWorkspaceSsh APIKeyScope = "workspace:ssh"
|
||||
ApiKeyScopeWorkspaceStart APIKeyScope = "workspace:start"
|
||||
ApiKeyScopeWorkspaceStop APIKeyScope = "workspace:stop"
|
||||
ApiKeyScopeWorkspaceUpdate APIKeyScope = "workspace:update"
|
||||
ApiKeyScopeWorkspaceAgentDevcontainersCreate APIKeyScope = "workspace_agent_devcontainers:create"
|
||||
ApiKeyScopeWorkspaceAgentResourceMonitorCreate APIKeyScope = "workspace_agent_resource_monitor:create"
|
||||
ApiKeyScopeWorkspaceAgentResourceMonitorRead APIKeyScope = "workspace_agent_resource_monitor:read"
|
||||
ApiKeyScopeWorkspaceAgentResourceMonitorUpdate APIKeyScope = "workspace_agent_resource_monitor:update"
|
||||
ApiKeyScopeWorkspaceDormantApplicationConnect APIKeyScope = "workspace_dormant:application_connect"
|
||||
ApiKeyScopeWorkspaceDormantCreate APIKeyScope = "workspace_dormant:create"
|
||||
ApiKeyScopeWorkspaceDormantCreateAgent APIKeyScope = "workspace_dormant:create_agent"
|
||||
ApiKeyScopeWorkspaceDormantDelete APIKeyScope = "workspace_dormant:delete"
|
||||
ApiKeyScopeWorkspaceDormantDeleteAgent APIKeyScope = "workspace_dormant:delete_agent"
|
||||
ApiKeyScopeWorkspaceDormantRead APIKeyScope = "workspace_dormant:read"
|
||||
ApiKeyScopeWorkspaceDormantSsh APIKeyScope = "workspace_dormant:ssh"
|
||||
ApiKeyScopeWorkspaceDormantStart APIKeyScope = "workspace_dormant:start"
|
||||
ApiKeyScopeWorkspaceDormantStop APIKeyScope = "workspace_dormant:stop"
|
||||
ApiKeyScopeWorkspaceDormantUpdate APIKeyScope = "workspace_dormant:update"
|
||||
ApiKeyScopeWorkspaceProxyCreate APIKeyScope = "workspace_proxy:create"
|
||||
ApiKeyScopeWorkspaceProxyDelete APIKeyScope = "workspace_proxy:delete"
|
||||
ApiKeyScopeWorkspaceProxyRead APIKeyScope = "workspace_proxy:read"
|
||||
ApiKeyScopeWorkspaceProxyUpdate APIKeyScope = "workspace_proxy:update"
|
||||
)
|
||||
|
||||
func (e *APIKeyScope) Scan(src interface{}) error {
|
||||
@@ -61,7 +199,145 @@ func (ns NullAPIKeyScope) Value() (driver.Value, error) {
|
||||
func (e APIKeyScope) Valid() bool {
|
||||
switch e {
|
||||
case APIKeyScopeAll,
|
||||
APIKeyScopeApplicationConnect:
|
||||
APIKeyScopeApplicationConnect,
|
||||
ApiKeyScopeAibridgeInterceptionCreate,
|
||||
ApiKeyScopeAibridgeInterceptionRead,
|
||||
ApiKeyScopeAibridgeInterceptionUpdate,
|
||||
ApiKeyScopeApiKeyCreate,
|
||||
ApiKeyScopeApiKeyDelete,
|
||||
ApiKeyScopeApiKeyRead,
|
||||
ApiKeyScopeApiKeyUpdate,
|
||||
ApiKeyScopeAssignOrgRoleAssign,
|
||||
ApiKeyScopeAssignOrgRoleCreate,
|
||||
ApiKeyScopeAssignOrgRoleDelete,
|
||||
ApiKeyScopeAssignOrgRoleRead,
|
||||
ApiKeyScopeAssignOrgRoleUnassign,
|
||||
ApiKeyScopeAssignOrgRoleUpdate,
|
||||
ApiKeyScopeAssignRoleAssign,
|
||||
ApiKeyScopeAssignRoleRead,
|
||||
ApiKeyScopeAssignRoleUnassign,
|
||||
ApiKeyScopeAuditLogCreate,
|
||||
ApiKeyScopeAuditLogRead,
|
||||
ApiKeyScopeConnectionLogRead,
|
||||
ApiKeyScopeConnectionLogUpdate,
|
||||
ApiKeyScopeCryptoKeyCreate,
|
||||
ApiKeyScopeCryptoKeyDelete,
|
||||
ApiKeyScopeCryptoKeyRead,
|
||||
ApiKeyScopeCryptoKeyUpdate,
|
||||
ApiKeyScopeDebugInfoRead,
|
||||
ApiKeyScopeDeploymentConfigRead,
|
||||
ApiKeyScopeDeploymentConfigUpdate,
|
||||
ApiKeyScopeDeploymentStatsRead,
|
||||
ApiKeyScopeFileCreate,
|
||||
ApiKeyScopeFileRead,
|
||||
ApiKeyScopeGroupCreate,
|
||||
ApiKeyScopeGroupDelete,
|
||||
ApiKeyScopeGroupRead,
|
||||
ApiKeyScopeGroupUpdate,
|
||||
ApiKeyScopeGroupMemberRead,
|
||||
ApiKeyScopeIdpsyncSettingsRead,
|
||||
ApiKeyScopeIdpsyncSettingsUpdate,
|
||||
ApiKeyScopeInboxNotificationCreate,
|
||||
ApiKeyScopeInboxNotificationRead,
|
||||
ApiKeyScopeInboxNotificationUpdate,
|
||||
ApiKeyScopeLicenseCreate,
|
||||
ApiKeyScopeLicenseDelete,
|
||||
ApiKeyScopeLicenseRead,
|
||||
ApiKeyScopeNotificationMessageCreate,
|
||||
ApiKeyScopeNotificationMessageDelete,
|
||||
ApiKeyScopeNotificationMessageRead,
|
||||
ApiKeyScopeNotificationMessageUpdate,
|
||||
ApiKeyScopeNotificationPreferenceRead,
|
||||
ApiKeyScopeNotificationPreferenceUpdate,
|
||||
ApiKeyScopeNotificationTemplateRead,
|
||||
ApiKeyScopeNotificationTemplateUpdate,
|
||||
ApiKeyScopeOauth2AppCreate,
|
||||
ApiKeyScopeOauth2AppDelete,
|
||||
ApiKeyScopeOauth2AppRead,
|
||||
ApiKeyScopeOauth2AppUpdate,
|
||||
ApiKeyScopeOauth2AppCodeTokenCreate,
|
||||
ApiKeyScopeOauth2AppCodeTokenDelete,
|
||||
ApiKeyScopeOauth2AppCodeTokenRead,
|
||||
ApiKeyScopeOauth2AppSecretCreate,
|
||||
ApiKeyScopeOauth2AppSecretDelete,
|
||||
ApiKeyScopeOauth2AppSecretRead,
|
||||
ApiKeyScopeOauth2AppSecretUpdate,
|
||||
ApiKeyScopeOrganizationCreate,
|
||||
ApiKeyScopeOrganizationDelete,
|
||||
ApiKeyScopeOrganizationRead,
|
||||
ApiKeyScopeOrganizationUpdate,
|
||||
ApiKeyScopeOrganizationMemberCreate,
|
||||
ApiKeyScopeOrganizationMemberDelete,
|
||||
ApiKeyScopeOrganizationMemberRead,
|
||||
ApiKeyScopeOrganizationMemberUpdate,
|
||||
ApiKeyScopePrebuiltWorkspaceDelete,
|
||||
ApiKeyScopePrebuiltWorkspaceUpdate,
|
||||
ApiKeyScopeProvisionerDaemonCreate,
|
||||
ApiKeyScopeProvisionerDaemonDelete,
|
||||
ApiKeyScopeProvisionerDaemonRead,
|
||||
ApiKeyScopeProvisionerDaemonUpdate,
|
||||
ApiKeyScopeProvisionerJobsCreate,
|
||||
ApiKeyScopeProvisionerJobsRead,
|
||||
ApiKeyScopeProvisionerJobsUpdate,
|
||||
ApiKeyScopeReplicasRead,
|
||||
ApiKeyScopeSystemCreate,
|
||||
ApiKeyScopeSystemDelete,
|
||||
ApiKeyScopeSystemRead,
|
||||
ApiKeyScopeSystemUpdate,
|
||||
ApiKeyScopeTailnetCoordinatorCreate,
|
||||
ApiKeyScopeTailnetCoordinatorDelete,
|
||||
ApiKeyScopeTailnetCoordinatorRead,
|
||||
ApiKeyScopeTailnetCoordinatorUpdate,
|
||||
ApiKeyScopeTemplateCreate,
|
||||
ApiKeyScopeTemplateDelete,
|
||||
ApiKeyScopeTemplateRead,
|
||||
ApiKeyScopeTemplateUpdate,
|
||||
ApiKeyScopeTemplateUse,
|
||||
ApiKeyScopeTemplateViewInsights,
|
||||
ApiKeyScopeUsageEventCreate,
|
||||
ApiKeyScopeUsageEventRead,
|
||||
ApiKeyScopeUsageEventUpdate,
|
||||
ApiKeyScopeUserCreate,
|
||||
ApiKeyScopeUserDelete,
|
||||
ApiKeyScopeUserRead,
|
||||
ApiKeyScopeUserReadPersonal,
|
||||
ApiKeyScopeUserUpdate,
|
||||
ApiKeyScopeUserUpdatePersonal,
|
||||
ApiKeyScopeUserSecretCreate,
|
||||
ApiKeyScopeUserSecretDelete,
|
||||
ApiKeyScopeUserSecretRead,
|
||||
ApiKeyScopeUserSecretUpdate,
|
||||
ApiKeyScopeWebpushSubscriptionCreate,
|
||||
ApiKeyScopeWebpushSubscriptionDelete,
|
||||
ApiKeyScopeWebpushSubscriptionRead,
|
||||
ApiKeyScopeWorkspaceApplicationConnect,
|
||||
ApiKeyScopeWorkspaceCreate,
|
||||
ApiKeyScopeWorkspaceCreateAgent,
|
||||
ApiKeyScopeWorkspaceDelete,
|
||||
ApiKeyScopeWorkspaceDeleteAgent,
|
||||
ApiKeyScopeWorkspaceRead,
|
||||
ApiKeyScopeWorkspaceSsh,
|
||||
ApiKeyScopeWorkspaceStart,
|
||||
ApiKeyScopeWorkspaceStop,
|
||||
ApiKeyScopeWorkspaceUpdate,
|
||||
ApiKeyScopeWorkspaceAgentDevcontainersCreate,
|
||||
ApiKeyScopeWorkspaceAgentResourceMonitorCreate,
|
||||
ApiKeyScopeWorkspaceAgentResourceMonitorRead,
|
||||
ApiKeyScopeWorkspaceAgentResourceMonitorUpdate,
|
||||
ApiKeyScopeWorkspaceDormantApplicationConnect,
|
||||
ApiKeyScopeWorkspaceDormantCreate,
|
||||
ApiKeyScopeWorkspaceDormantCreateAgent,
|
||||
ApiKeyScopeWorkspaceDormantDelete,
|
||||
ApiKeyScopeWorkspaceDormantDeleteAgent,
|
||||
ApiKeyScopeWorkspaceDormantRead,
|
||||
ApiKeyScopeWorkspaceDormantSsh,
|
||||
ApiKeyScopeWorkspaceDormantStart,
|
||||
ApiKeyScopeWorkspaceDormantStop,
|
||||
ApiKeyScopeWorkspaceDormantUpdate,
|
||||
ApiKeyScopeWorkspaceProxyCreate,
|
||||
ApiKeyScopeWorkspaceProxyDelete,
|
||||
ApiKeyScopeWorkspaceProxyRead,
|
||||
ApiKeyScopeWorkspaceProxyUpdate:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -71,6 +347,144 @@ func AllAPIKeyScopeValues() []APIKeyScope {
|
||||
return []APIKeyScope{
|
||||
APIKeyScopeAll,
|
||||
APIKeyScopeApplicationConnect,
|
||||
ApiKeyScopeAibridgeInterceptionCreate,
|
||||
ApiKeyScopeAibridgeInterceptionRead,
|
||||
ApiKeyScopeAibridgeInterceptionUpdate,
|
||||
ApiKeyScopeApiKeyCreate,
|
||||
ApiKeyScopeApiKeyDelete,
|
||||
ApiKeyScopeApiKeyRead,
|
||||
ApiKeyScopeApiKeyUpdate,
|
||||
ApiKeyScopeAssignOrgRoleAssign,
|
||||
ApiKeyScopeAssignOrgRoleCreate,
|
||||
ApiKeyScopeAssignOrgRoleDelete,
|
||||
ApiKeyScopeAssignOrgRoleRead,
|
||||
ApiKeyScopeAssignOrgRoleUnassign,
|
||||
ApiKeyScopeAssignOrgRoleUpdate,
|
||||
ApiKeyScopeAssignRoleAssign,
|
||||
ApiKeyScopeAssignRoleRead,
|
||||
ApiKeyScopeAssignRoleUnassign,
|
||||
ApiKeyScopeAuditLogCreate,
|
||||
ApiKeyScopeAuditLogRead,
|
||||
ApiKeyScopeConnectionLogRead,
|
||||
ApiKeyScopeConnectionLogUpdate,
|
||||
ApiKeyScopeCryptoKeyCreate,
|
||||
ApiKeyScopeCryptoKeyDelete,
|
||||
ApiKeyScopeCryptoKeyRead,
|
||||
ApiKeyScopeCryptoKeyUpdate,
|
||||
ApiKeyScopeDebugInfoRead,
|
||||
ApiKeyScopeDeploymentConfigRead,
|
||||
ApiKeyScopeDeploymentConfigUpdate,
|
||||
ApiKeyScopeDeploymentStatsRead,
|
||||
ApiKeyScopeFileCreate,
|
||||
ApiKeyScopeFileRead,
|
||||
ApiKeyScopeGroupCreate,
|
||||
ApiKeyScopeGroupDelete,
|
||||
ApiKeyScopeGroupRead,
|
||||
ApiKeyScopeGroupUpdate,
|
||||
ApiKeyScopeGroupMemberRead,
|
||||
ApiKeyScopeIdpsyncSettingsRead,
|
||||
ApiKeyScopeIdpsyncSettingsUpdate,
|
||||
ApiKeyScopeInboxNotificationCreate,
|
||||
ApiKeyScopeInboxNotificationRead,
|
||||
ApiKeyScopeInboxNotificationUpdate,
|
||||
ApiKeyScopeLicenseCreate,
|
||||
ApiKeyScopeLicenseDelete,
|
||||
ApiKeyScopeLicenseRead,
|
||||
ApiKeyScopeNotificationMessageCreate,
|
||||
ApiKeyScopeNotificationMessageDelete,
|
||||
ApiKeyScopeNotificationMessageRead,
|
||||
ApiKeyScopeNotificationMessageUpdate,
|
||||
ApiKeyScopeNotificationPreferenceRead,
|
||||
ApiKeyScopeNotificationPreferenceUpdate,
|
||||
ApiKeyScopeNotificationTemplateRead,
|
||||
ApiKeyScopeNotificationTemplateUpdate,
|
||||
ApiKeyScopeOauth2AppCreate,
|
||||
ApiKeyScopeOauth2AppDelete,
|
||||
ApiKeyScopeOauth2AppRead,
|
||||
ApiKeyScopeOauth2AppUpdate,
|
||||
ApiKeyScopeOauth2AppCodeTokenCreate,
|
||||
ApiKeyScopeOauth2AppCodeTokenDelete,
|
||||
ApiKeyScopeOauth2AppCodeTokenRead,
|
||||
ApiKeyScopeOauth2AppSecretCreate,
|
||||
ApiKeyScopeOauth2AppSecretDelete,
|
||||
ApiKeyScopeOauth2AppSecretRead,
|
||||
ApiKeyScopeOauth2AppSecretUpdate,
|
||||
ApiKeyScopeOrganizationCreate,
|
||||
ApiKeyScopeOrganizationDelete,
|
||||
ApiKeyScopeOrganizationRead,
|
||||
ApiKeyScopeOrganizationUpdate,
|
||||
ApiKeyScopeOrganizationMemberCreate,
|
||||
ApiKeyScopeOrganizationMemberDelete,
|
||||
ApiKeyScopeOrganizationMemberRead,
|
||||
ApiKeyScopeOrganizationMemberUpdate,
|
||||
ApiKeyScopePrebuiltWorkspaceDelete,
|
||||
ApiKeyScopePrebuiltWorkspaceUpdate,
|
||||
ApiKeyScopeProvisionerDaemonCreate,
|
||||
ApiKeyScopeProvisionerDaemonDelete,
|
||||
ApiKeyScopeProvisionerDaemonRead,
|
||||
ApiKeyScopeProvisionerDaemonUpdate,
|
||||
ApiKeyScopeProvisionerJobsCreate,
|
||||
ApiKeyScopeProvisionerJobsRead,
|
||||
ApiKeyScopeProvisionerJobsUpdate,
|
||||
ApiKeyScopeReplicasRead,
|
||||
ApiKeyScopeSystemCreate,
|
||||
ApiKeyScopeSystemDelete,
|
||||
ApiKeyScopeSystemRead,
|
||||
ApiKeyScopeSystemUpdate,
|
||||
ApiKeyScopeTailnetCoordinatorCreate,
|
||||
ApiKeyScopeTailnetCoordinatorDelete,
|
||||
ApiKeyScopeTailnetCoordinatorRead,
|
||||
ApiKeyScopeTailnetCoordinatorUpdate,
|
||||
ApiKeyScopeTemplateCreate,
|
||||
ApiKeyScopeTemplateDelete,
|
||||
ApiKeyScopeTemplateRead,
|
||||
ApiKeyScopeTemplateUpdate,
|
||||
ApiKeyScopeTemplateUse,
|
||||
ApiKeyScopeTemplateViewInsights,
|
||||
ApiKeyScopeUsageEventCreate,
|
||||
ApiKeyScopeUsageEventRead,
|
||||
ApiKeyScopeUsageEventUpdate,
|
||||
ApiKeyScopeUserCreate,
|
||||
ApiKeyScopeUserDelete,
|
||||
ApiKeyScopeUserRead,
|
||||
ApiKeyScopeUserReadPersonal,
|
||||
ApiKeyScopeUserUpdate,
|
||||
ApiKeyScopeUserUpdatePersonal,
|
||||
ApiKeyScopeUserSecretCreate,
|
||||
ApiKeyScopeUserSecretDelete,
|
||||
ApiKeyScopeUserSecretRead,
|
||||
ApiKeyScopeUserSecretUpdate,
|
||||
ApiKeyScopeWebpushSubscriptionCreate,
|
||||
ApiKeyScopeWebpushSubscriptionDelete,
|
||||
ApiKeyScopeWebpushSubscriptionRead,
|
||||
ApiKeyScopeWorkspaceApplicationConnect,
|
||||
ApiKeyScopeWorkspaceCreate,
|
||||
ApiKeyScopeWorkspaceCreateAgent,
|
||||
ApiKeyScopeWorkspaceDelete,
|
||||
ApiKeyScopeWorkspaceDeleteAgent,
|
||||
ApiKeyScopeWorkspaceRead,
|
||||
ApiKeyScopeWorkspaceSsh,
|
||||
ApiKeyScopeWorkspaceStart,
|
||||
ApiKeyScopeWorkspaceStop,
|
||||
ApiKeyScopeWorkspaceUpdate,
|
||||
ApiKeyScopeWorkspaceAgentDevcontainersCreate,
|
||||
ApiKeyScopeWorkspaceAgentResourceMonitorCreate,
|
||||
ApiKeyScopeWorkspaceAgentResourceMonitorRead,
|
||||
ApiKeyScopeWorkspaceAgentResourceMonitorUpdate,
|
||||
ApiKeyScopeWorkspaceDormantApplicationConnect,
|
||||
ApiKeyScopeWorkspaceDormantCreate,
|
||||
ApiKeyScopeWorkspaceDormantCreateAgent,
|
||||
ApiKeyScopeWorkspaceDormantDelete,
|
||||
ApiKeyScopeWorkspaceDormantDeleteAgent,
|
||||
ApiKeyScopeWorkspaceDormantRead,
|
||||
ApiKeyScopeWorkspaceDormantSsh,
|
||||
ApiKeyScopeWorkspaceDormantStart,
|
||||
ApiKeyScopeWorkspaceDormantStop,
|
||||
ApiKeyScopeWorkspaceDormantUpdate,
|
||||
ApiKeyScopeWorkspaceProxyCreate,
|
||||
ApiKeyScopeWorkspaceProxyDelete,
|
||||
ApiKeyScopeWorkspaceProxyRead,
|
||||
ApiKeyScopeWorkspaceProxyUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3009,17 +3423,18 @@ type AIBridgeUserPrompt struct {
|
||||
type APIKey struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
// 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.
|
||||
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
LastUsed time.Time `db:"last_used" json:"last_used"`
|
||||
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
LoginType LoginType `db:"login_type" json:"login_type"`
|
||||
LifetimeSeconds int64 `db:"lifetime_seconds" json:"lifetime_seconds"`
|
||||
IPAddress pqtype.Inet `db:"ip_address" json:"ip_address"`
|
||||
Scope APIKeyScope `db:"scope" json:"scope"`
|
||||
TokenName string `db:"token_name" json:"token_name"`
|
||||
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
LastUsed time.Time `db:"last_used" json:"last_used"`
|
||||
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
LoginType LoginType `db:"login_type" json:"login_type"`
|
||||
LifetimeSeconds int64 `db:"lifetime_seconds" json:"lifetime_seconds"`
|
||||
IPAddress pqtype.Inet `db:"ip_address" json:"ip_address"`
|
||||
TokenName string `db:"token_name" json:"token_name"`
|
||||
Scopes APIKeyScopes `db:"scopes" json:"scopes"`
|
||||
AllowList AllowList `db:"allow_list" json:"allow_list"`
|
||||
}
|
||||
|
||||
type AuditLog struct {
|
||||
|
||||
@@ -287,7 +287,7 @@ DELETE FROM
|
||||
api_keys
|
||||
WHERE
|
||||
user_id = $1 AND
|
||||
scope = 'application_connect'::api_key_scope
|
||||
'application_connect'::api_key_scope = ANY(scopes)
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error {
|
||||
@@ -337,7 +337,7 @@ func (q *sqlQuerier) ExpirePrebuildsAPIKeys(ctx context.Context, now time.Time)
|
||||
|
||||
const getAPIKeyByID = `-- name: GetAPIKeyByID :one
|
||||
SELECT
|
||||
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope, token_name
|
||||
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list
|
||||
FROM
|
||||
api_keys
|
||||
WHERE
|
||||
@@ -360,15 +360,16 @@ func (q *sqlQuerier) GetAPIKeyByID(ctx context.Context, id string) (APIKey, erro
|
||||
&i.LoginType,
|
||||
&i.LifetimeSeconds,
|
||||
&i.IPAddress,
|
||||
&i.Scope,
|
||||
&i.TokenName,
|
||||
&i.Scopes,
|
||||
&i.AllowList,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAPIKeyByName = `-- name: GetAPIKeyByName :one
|
||||
SELECT
|
||||
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope, token_name
|
||||
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list
|
||||
FROM
|
||||
api_keys
|
||||
WHERE
|
||||
@@ -399,14 +400,15 @@ func (q *sqlQuerier) GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNamePar
|
||||
&i.LoginType,
|
||||
&i.LifetimeSeconds,
|
||||
&i.IPAddress,
|
||||
&i.Scope,
|
||||
&i.TokenName,
|
||||
&i.Scopes,
|
||||
&i.AllowList,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAPIKeysByLoginType = `-- name: GetAPIKeysByLoginType :many
|
||||
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope, token_name FROM api_keys WHERE login_type = $1
|
||||
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list FROM api_keys WHERE login_type = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetAPIKeysByLoginType(ctx context.Context, loginType LoginType) ([]APIKey, error) {
|
||||
@@ -429,8 +431,9 @@ func (q *sqlQuerier) GetAPIKeysByLoginType(ctx context.Context, loginType LoginT
|
||||
&i.LoginType,
|
||||
&i.LifetimeSeconds,
|
||||
&i.IPAddress,
|
||||
&i.Scope,
|
||||
&i.TokenName,
|
||||
&i.Scopes,
|
||||
&i.AllowList,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -446,7 +449,7 @@ func (q *sqlQuerier) GetAPIKeysByLoginType(ctx context.Context, loginType LoginT
|
||||
}
|
||||
|
||||
const getAPIKeysByUserID = `-- name: GetAPIKeysByUserID :many
|
||||
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope, token_name FROM api_keys WHERE login_type = $1 AND user_id = $2
|
||||
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list FROM api_keys WHERE login_type = $1 AND user_id = $2
|
||||
`
|
||||
|
||||
type GetAPIKeysByUserIDParams struct {
|
||||
@@ -474,8 +477,9 @@ func (q *sqlQuerier) GetAPIKeysByUserID(ctx context.Context, arg GetAPIKeysByUse
|
||||
&i.LoginType,
|
||||
&i.LifetimeSeconds,
|
||||
&i.IPAddress,
|
||||
&i.Scope,
|
||||
&i.TokenName,
|
||||
&i.Scopes,
|
||||
&i.AllowList,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -491,7 +495,7 @@ func (q *sqlQuerier) GetAPIKeysByUserID(ctx context.Context, arg GetAPIKeysByUse
|
||||
}
|
||||
|
||||
const getAPIKeysLastUsedAfter = `-- name: GetAPIKeysLastUsedAfter :many
|
||||
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope, token_name FROM api_keys WHERE last_used > $1
|
||||
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list FROM api_keys WHERE last_used > $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error) {
|
||||
@@ -514,8 +518,9 @@ func (q *sqlQuerier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.
|
||||
&i.LoginType,
|
||||
&i.LifetimeSeconds,
|
||||
&i.IPAddress,
|
||||
&i.Scope,
|
||||
&i.TokenName,
|
||||
&i.Scopes,
|
||||
&i.AllowList,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -543,7 +548,8 @@ INSERT INTO
|
||||
created_at,
|
||||
updated_at,
|
||||
login_type,
|
||||
scope,
|
||||
scopes,
|
||||
allow_list,
|
||||
token_name
|
||||
)
|
||||
VALUES
|
||||
@@ -553,22 +559,23 @@ VALUES
|
||||
WHEN 0 THEN 86400
|
||||
ELSE $2::bigint
|
||||
END
|
||||
, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope, token_name
|
||||
, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list
|
||||
`
|
||||
|
||||
type InsertAPIKeyParams struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
LifetimeSeconds int64 `db:"lifetime_seconds" json:"lifetime_seconds"`
|
||||
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
|
||||
IPAddress pqtype.Inet `db:"ip_address" json:"ip_address"`
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
LastUsed time.Time `db:"last_used" json:"last_used"`
|
||||
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
LoginType LoginType `db:"login_type" json:"login_type"`
|
||||
Scope APIKeyScope `db:"scope" json:"scope"`
|
||||
TokenName string `db:"token_name" json:"token_name"`
|
||||
ID string `db:"id" json:"id"`
|
||||
LifetimeSeconds int64 `db:"lifetime_seconds" json:"lifetime_seconds"`
|
||||
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
|
||||
IPAddress pqtype.Inet `db:"ip_address" json:"ip_address"`
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
LastUsed time.Time `db:"last_used" json:"last_used"`
|
||||
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
LoginType LoginType `db:"login_type" json:"login_type"`
|
||||
Scopes APIKeyScopes `db:"scopes" json:"scopes"`
|
||||
AllowList AllowList `db:"allow_list" json:"allow_list"`
|
||||
TokenName string `db:"token_name" json:"token_name"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) {
|
||||
@@ -583,7 +590,8 @@ func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
arg.LoginType,
|
||||
arg.Scope,
|
||||
arg.Scopes,
|
||||
arg.AllowList,
|
||||
arg.TokenName,
|
||||
)
|
||||
var i APIKey
|
||||
@@ -598,8 +606,9 @@ func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (
|
||||
&i.LoginType,
|
||||
&i.LifetimeSeconds,
|
||||
&i.IPAddress,
|
||||
&i.Scope,
|
||||
&i.TokenName,
|
||||
&i.Scopes,
|
||||
&i.AllowList,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ INSERT INTO
|
||||
created_at,
|
||||
updated_at,
|
||||
login_type,
|
||||
scope,
|
||||
scopes,
|
||||
allow_list,
|
||||
token_name
|
||||
)
|
||||
VALUES
|
||||
@@ -53,7 +54,7 @@ VALUES
|
||||
WHEN 0 THEN 86400
|
||||
ELSE @lifetime_seconds::bigint
|
||||
END
|
||||
, @hashed_secret, @ip_address, @user_id, @last_used, @expires_at, @created_at, @updated_at, @login_type, @scope, @token_name) RETURNING *;
|
||||
, @hashed_secret, @ip_address, @user_id, @last_used, @expires_at, @created_at, @updated_at, @login_type, @scopes, @allow_list, @token_name) RETURNING *;
|
||||
|
||||
-- name: UpdateAPIKeyByID :exec
|
||||
UPDATE
|
||||
@@ -76,7 +77,7 @@ DELETE FROM
|
||||
api_keys
|
||||
WHERE
|
||||
user_id = $1 AND
|
||||
scope = 'application_connect'::api_key_scope;
|
||||
'application_connect'::api_key_scope = ANY(scopes);
|
||||
|
||||
-- name: DeleteAPIKeysByUserID :exec
|
||||
DELETE FROM
|
||||
|
||||
@@ -28,6 +28,12 @@ sql:
|
||||
emit_enum_valid_method: true
|
||||
emit_all_enum_values: true
|
||||
overrides:
|
||||
- column: "api_keys.scopes"
|
||||
go_type:
|
||||
type: "APIKeyScopes"
|
||||
- column: "api_keys.allow_list"
|
||||
go_type:
|
||||
type: "AllowList"
|
||||
- db_type: "agent_id_name_pair"
|
||||
go_type:
|
||||
type: "AgentIDNamePair"
|
||||
|
||||
@@ -9,9 +9,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/lib/pq"
|
||||
"github.com/sqlc-dev/pqtype"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
)
|
||||
|
||||
@@ -161,6 +163,29 @@ func (m StringMapOfInt) Value() (driver.Value, error) {
|
||||
|
||||
type CustomRolePermissions []CustomRolePermission
|
||||
|
||||
// APIKeyScopes implements sql.Scanner and driver.Valuer so it can be read from
|
||||
// and written to the Postgres api_key_scope[] enum array column.
|
||||
func (s *APIKeyScopes) Scan(src interface{}) error {
|
||||
var arr []string
|
||||
if err := pq.Array(&arr).Scan(src); err != nil {
|
||||
return err
|
||||
}
|
||||
out := make(APIKeyScopes, len(arr))
|
||||
for i, v := range arr {
|
||||
out[i] = APIKeyScope(v)
|
||||
}
|
||||
*s = out
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s APIKeyScopes) Value() (driver.Value, error) {
|
||||
arr := make([]string, len(s))
|
||||
for i, v := range s {
|
||||
arr[i] = string(v)
|
||||
}
|
||||
return pq.Array(arr).Value()
|
||||
}
|
||||
|
||||
func (a *CustomRolePermissions) Scan(src interface{}) error {
|
||||
switch v := src.(type) {
|
||||
case string:
|
||||
@@ -288,3 +313,63 @@ func ParseIP(ipStr string) pqtype.Inet {
|
||||
Valid: ip != nil,
|
||||
}
|
||||
}
|
||||
|
||||
// AllowListTarget represents a single scope allow-list entry.
|
||||
// It encodes a resource tuple (type, id) and provides helpers for
|
||||
// consistent string and JSON representations across the codebase.
|
||||
type AllowListTarget struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// String returns the canonical database representation "type:id".
|
||||
func (t AllowListTarget) String() string {
|
||||
return t.Type + ":" + t.ID
|
||||
}
|
||||
|
||||
// ParseAllowListTarget parses the canonical string form "type:id".
|
||||
func ParseAllowListTarget(s string) (AllowListTarget, error) {
|
||||
targetType, id, ok := rbac.ParseResourceAction(s)
|
||||
if !ok {
|
||||
return AllowListTarget{}, xerrors.Errorf("invalid allow list target: %q", s)
|
||||
}
|
||||
return AllowListTarget{Type: targetType, ID: id}, nil
|
||||
}
|
||||
|
||||
// AllowListWildcard returns the wildcard allow-list entry {"*","*"}.
|
||||
func AllowListWildcard() AllowListTarget { return AllowListTarget{Type: "*", ID: "*"} }
|
||||
|
||||
// AllowList is a typed wrapper around a list of AllowListTarget entries.
|
||||
// It implements sql.Scanner and driver.Valuer so it can be stored in and
|
||||
// loaded from a Postgres text[] column that stores each entry in the
|
||||
// canonical form "type:id".
|
||||
type AllowList []AllowListTarget
|
||||
|
||||
// Scan implements sql.Scanner. It supports inputs that pq.Array can decode
|
||||
// into []string, and then converts each element to an AllowListTarget.
|
||||
func (a *AllowList) Scan(src any) error {
|
||||
var raw []string
|
||||
if err := pq.Array(&raw).Scan(src); err != nil {
|
||||
return err
|
||||
}
|
||||
out := make([]AllowListTarget, len(raw))
|
||||
for i, s := range raw {
|
||||
t, err := ParseAllowListTarget(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out[i] = t
|
||||
}
|
||||
*a = out
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements driver.Valuer by converting the list to []string using the
|
||||
// canonical "type:id" form and delegating to pq.Array for encoding.
|
||||
func (a AllowList) Value() (driver.Value, error) {
|
||||
raw := make([]string, len(a))
|
||||
for i, t := range a {
|
||||
raw[i] = t.String()
|
||||
}
|
||||
return pq.Array(raw).Value()
|
||||
}
|
||||
|
||||
@@ -434,7 +434,7 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon
|
||||
// If the key is valid, we also fetch the user roles and status.
|
||||
// The roles are used for RBAC authorize checks, and the status
|
||||
// is to block 'suspended' users from accessing the platform.
|
||||
actor, userStatus, err := UserRBACSubject(ctx, cfg.DB, key.UserID, rbac.ScopeName(key.Scope))
|
||||
actor, userStatus, err := UserRBACSubject(ctx, cfg.DB, key.UserID, key.Scopes)
|
||||
if err != nil {
|
||||
return write(http.StatusUnauthorized, codersdk.Response{
|
||||
Message: internalErrorMessage,
|
||||
|
||||
@@ -313,7 +313,7 @@ func TestAPIKey(t *testing.T) {
|
||||
_, token = dbgen.APIKey(t, db, database.APIKey{
|
||||
UserID: user.ID,
|
||||
ExpiresAt: dbtime.Now().AddDate(0, 0, 1),
|
||||
Scope: database.APIKeyScopeApplicationConnect,
|
||||
Scopes: database.APIKeyScopes{database.APIKeyScopeApplicationConnect},
|
||||
})
|
||||
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
@@ -330,7 +330,7 @@ func TestAPIKey(t *testing.T) {
|
||||
})(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Checks that it exists on the context!
|
||||
apiKey := httpmw.APIKey(r)
|
||||
assert.Equal(t, database.APIKeyScopeApplicationConnect, apiKey.Scope)
|
||||
assert.Equal(t, database.APIKeyScopeApplicationConnect, apiKey.Scopes[0])
|
||||
assertActorOk(t, r)
|
||||
|
||||
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.Response{
|
||||
|
||||
@@ -172,7 +172,8 @@ func addUser(t *testing.T, db database.Store, roles ...string) (database.User, s
|
||||
LastUsed: dbtime.Now(),
|
||||
ExpiresAt: dbtime.Now().Add(time.Minute),
|
||||
LoginType: database.LoginTypePassword,
|
||||
Scope: database.APIKeyScopeAll,
|
||||
Scopes: database.APIKeyScopes{database.APIKeyScopeAll},
|
||||
AllowList: database.AllowList{database.AllowListWildcard()},
|
||||
IPAddress: pqtype.Inet{
|
||||
IPNet: net.IPNet{
|
||||
IP: net.ParseIP("0.0.0.0"),
|
||||
|
||||
@@ -66,7 +66,8 @@ func TestWorkspaceParam(t *testing.T) {
|
||||
LastUsed: dbtime.Now(),
|
||||
ExpiresAt: dbtime.Now().Add(time.Minute),
|
||||
LoginType: database.LoginTypePassword,
|
||||
Scope: database.APIKeyScopeAll,
|
||||
Scopes: database.APIKeyScopes{database.APIKeyScopeAll},
|
||||
AllowList: database.AllowList{database.AllowListWildcard()},
|
||||
IPAddress: pqtype.Inet{
|
||||
IPNet: net.IPNet{
|
||||
IP: net.IPv4(127, 0, 0, 1),
|
||||
|
||||
+60
-4
@@ -2,6 +2,7 @@ package rbac
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
@@ -143,18 +144,73 @@ func AllowListAll() AllowListElement {
|
||||
return AllowListElement{ID: policy.WildcardSymbol, Type: policy.WildcardSymbol}
|
||||
}
|
||||
|
||||
// String encodes the allow list element into the canonical database representation
|
||||
// "type:id". This avoids fragile manual concatenations scattered across the codebase.
|
||||
func (e AllowListElement) String() string {
|
||||
return e.Type + ":" + e.ID
|
||||
}
|
||||
|
||||
func (s Scope) Expand() (Scope, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s Scope) Name() RoleIdentifier {
|
||||
return s.Role.Identifier
|
||||
return s.Identifier
|
||||
}
|
||||
|
||||
func ExpandScope(scope ScopeName) (Scope, error) {
|
||||
role, ok := builtinScopes[scope]
|
||||
if role, ok := builtinScopes[scope]; ok {
|
||||
return role, nil
|
||||
}
|
||||
if res, act, ok := parseLowLevelScope(scope); ok {
|
||||
return expandLowLevel(res, act), nil
|
||||
}
|
||||
return Scope{}, xerrors.Errorf("no scope named %q", scope)
|
||||
}
|
||||
|
||||
// ParseResourceAction parses a scope string formatted as "<resource>:<action>"
|
||||
// and returns the resource and action components. This is the common parsing
|
||||
// logic shared between RBAC and database validation.
|
||||
func ParseResourceAction(scope string) (resource string, action string, ok bool) {
|
||||
parts := strings.SplitN(scope, ":", 2)
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
return "", "", false
|
||||
}
|
||||
return parts[0], parts[1], true
|
||||
}
|
||||
|
||||
// parseLowLevelScope parses a low-level scope name formatted as
|
||||
// "<resource>:<action>" and validates it against RBACPermissions.
|
||||
// Returns the resource and action if valid.
|
||||
func parseLowLevelScope(name ScopeName) (resource string, action policy.Action, ok bool) {
|
||||
res, act, ok := ParseResourceAction(string(name))
|
||||
if !ok {
|
||||
return Scope{}, xerrors.Errorf("no scope named %q", scope)
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
def, exists := policy.RBACPermissions[res]
|
||||
if !exists {
|
||||
return "", "", false
|
||||
}
|
||||
if _, exists := def.Actions[policy.Action(act)]; !exists {
|
||||
return "", "", false
|
||||
}
|
||||
return res, policy.Action(act), true
|
||||
}
|
||||
|
||||
// expandLowLevel constructs a site-only Scope with a single permission for the
|
||||
// given resource and action. This mirrors how builtin scopes are represented
|
||||
// but is restricted to site-level only.
|
||||
func expandLowLevel(resource string, action policy.Action) Scope {
|
||||
return Scope{
|
||||
Role: Role{
|
||||
Identifier: RoleIdentifier{Name: fmt.Sprintf("Scope_%s:%s", resource, action)},
|
||||
DisplayName: fmt.Sprintf("%s:%s", resource, action),
|
||||
Site: []Permission{{ResourceType: resource, Action: action}},
|
||||
Org: map[string][]Permission{},
|
||||
User: []Permission{},
|
||||
},
|
||||
// Low-level scopes intentionally return an empty allow list.
|
||||
AllowIDList: []AllowListElement{},
|
||||
}
|
||||
return role, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package rbac_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
)
|
||||
|
||||
func TestExpandScope(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("low_level_pairs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
resource string
|
||||
action policy.Action
|
||||
}{
|
||||
{name: "workspace:start", resource: rbac.ResourceWorkspace.Type, action: policy.ActionWorkspaceStart},
|
||||
{name: "workspace:ssh", resource: rbac.ResourceWorkspace.Type, action: policy.ActionSSH},
|
||||
{name: "template:use", resource: rbac.ResourceTemplate.Type, action: policy.ActionUse},
|
||||
{name: "api_key:read", resource: rbac.ResourceApiKey.Type, action: policy.ActionRead},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
s, err := rbac.ScopeName(tc.name).Expand()
|
||||
require.NoError(t, err)
|
||||
|
||||
// site-only single permission
|
||||
require.Len(t, s.Site, 1)
|
||||
require.Equal(t, tc.resource, s.Site[0].ResourceType)
|
||||
require.Equal(t, tc.action, s.Site[0].Action)
|
||||
require.Empty(t, s.Org)
|
||||
require.Empty(t, s.User)
|
||||
|
||||
require.Len(t, s.AllowIDList, 0)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid_low_level", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
invalid := []string{
|
||||
"", // empty
|
||||
"workspace:", // missing action
|
||||
":read", // missing resource
|
||||
"unknown:read", // unknown resource
|
||||
"workspace:bogus", // unknown action
|
||||
"a:b:c", // too many parts
|
||||
}
|
||||
for _, name := range invalid {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := rbac.ScopeName(name).Expand()
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1955,20 +1955,20 @@ func TestUserLogout(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create a few application_connect-scoped API keys that should be deleted.
|
||||
for i := 0; i < 3; i++ {
|
||||
for i := range 3 {
|
||||
key, _ := dbgen.APIKey(t, db, database.APIKey{
|
||||
UserID: newUser.ID,
|
||||
Scope: database.APIKeyScopeApplicationConnect,
|
||||
Scopes: database.APIKeyScopes{database.APIKeyScopeApplicationConnect},
|
||||
})
|
||||
shouldBeDeleted[fmt.Sprintf("application_connect key owned by logout user %d", i)] = key.ID
|
||||
}
|
||||
|
||||
// Create a few application_connect-scoped API keys for the admin user that
|
||||
// should not be deleted.
|
||||
for i := 0; i < 3; i++ {
|
||||
for i := range 3 {
|
||||
key, _ := dbgen.APIKey(t, db, database.APIKey{
|
||||
UserID: firstUser.UserID,
|
||||
Scope: database.APIKeyScopeApplicationConnect,
|
||||
Scopes: database.APIKeyScopes{database.APIKeyScopeApplicationConnect},
|
||||
})
|
||||
shouldNotBeDeleted[fmt.Sprintf("application_connect key owned by admin user %d", i)] = key.ID
|
||||
}
|
||||
|
||||
+7
-1
@@ -1570,6 +1570,12 @@ func userOrganizationIDs(ctx context.Context, api *API, user database.User) ([]u
|
||||
}
|
||||
|
||||
func convertAPIKey(k database.APIKey) codersdk.APIKey {
|
||||
// Derive a single scope from arrays for response compatibility.
|
||||
scope := database.APIKeyScopeAll
|
||||
if k.Scopes.Has(database.APIKeyScopeApplicationConnect) {
|
||||
scope = database.APIKeyScopeApplicationConnect
|
||||
}
|
||||
|
||||
return codersdk.APIKey{
|
||||
ID: k.ID,
|
||||
UserID: k.UserID,
|
||||
@@ -1578,7 +1584,7 @@ func convertAPIKey(k database.APIKey) codersdk.APIKey {
|
||||
CreatedAt: k.CreatedAt,
|
||||
UpdatedAt: k.UpdatedAt,
|
||||
LoginType: codersdk.LoginType(k.LoginType),
|
||||
Scope: codersdk.APIKeyScope(k.Scope),
|
||||
Scope: codersdk.APIKeyScope(scope),
|
||||
LifetimeSeconds: k.LifetimeSeconds,
|
||||
TokenName: k.TokenName,
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ We track the following resources:
|
||||
|
||||
| <b>Resource<b> | | |
|
||||
|----------------------------------------------------------|----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| APIKey<br><i>login, logout, register, create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scope</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| APIKey<br><i>login, logout, register, create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>allow_list</td><td>false</td></tr><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scopes</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
|
||||
| AuditableOrganizationMember<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>roles</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||
|
||||
@@ -221,7 +221,8 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
||||
"login_type": ActionIgnore,
|
||||
"lifetime_seconds": ActionIgnore,
|
||||
"ip_address": ActionIgnore,
|
||||
"scope": ActionIgnore,
|
||||
"scopes": ActionIgnore,
|
||||
"allow_list": ActionIgnore,
|
||||
"token_name": ActionIgnore,
|
||||
},
|
||||
&database.AuditOAuthConvertState{}: {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
)
|
||||
|
||||
func main() {
|
||||
seen := map[string]struct{}{}
|
||||
var vals []string
|
||||
for resource, def := range policy.RBACPermissions {
|
||||
if resource == policy.WildcardSymbol {
|
||||
continue
|
||||
}
|
||||
for action := range def.Actions {
|
||||
vals = append(vals, fmt.Sprintf("%s:%s", resource, action))
|
||||
}
|
||||
}
|
||||
sort.Strings(vals)
|
||||
for _, v := range vals {
|
||||
if _, ok := seen[v]; ok {
|
||||
continue
|
||||
}
|
||||
seen[v] = struct{}{}
|
||||
_, _ = fmt.Printf("ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS '%s';\n", v)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user