From 79126ab6c7f5a13a8f54e6e62c4b36a46632cc1e Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 29 Sep 2025 13:17:08 +0200 Subject: [PATCH] feat: implement composite API key scopes for workspaces and templates (#19945) # Add Composite API Key Scopes This PR adds high-level composite API key scopes to simplify token creation with common permission sets: - `coder:workspaces.create` - Create and update workspaces - `coder:workspaces.operate` - Read and update workspaces - `coder:workspaces.delete` - Read and delete workspaces - `coder:workspaces.access` - Read, SSH, and connect to workspace applications - `coder:templates.build` - Read templates and create/read files - `coder:templates.author` - Full template management with insights - `coder:apikeys.manage_self` - Manage your own API keys These composite scopes are persisted in the database and expanded during authorization, providing a more intuitive way to grant permissions compared to the granular resource:action scopes. --- coderd/apidoc/docs.go | 14 ++++ coderd/apidoc/swagger.json | 14 ++++ coderd/database/dump.sql | 9 ++- ...0375_add_composite_api_key_scopes.down.sql | 3 + ...000375_add_composite_api_key_scopes.up.sql | 9 +++ coderd/database/modelmethods.go | 7 ++ coderd/database/models.go | 23 ++++++- coderd/rbac/roles.go | 20 ++++++ coderd/rbac/roles_internal_test.go | 21 ++++++ coderd/rbac/scopes.go | 64 +++++++++++++++++++ coderd/rbac/scopes_catalog.go | 27 ++++++-- coderd/rbac/scopes_catalog_internal_test.go | 18 +++++- codersdk/apikey_scopes_gen.go | 14 ++++ docs/reference/api/schemas.md | 7 ++ scripts/apikeyscopesgen/main.go | 1 + scripts/check-scopes/main.go | 6 ++ scripts/generate_api_key_scope_enum/main.go | 3 + site/src/api/typesGenerated.ts | 14 ++++ 18 files changed, 267 insertions(+), 7 deletions(-) create mode 100644 coderd/database/migrations/000375_add_composite_api_key_scopes.down.sql create mode 100644 coderd/database/migrations/000375_add_composite_api_key_scopes.up.sql diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index df881137bc..b93c647dad 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11529,7 +11529,14 @@ const docTemplate = `{ "api_key:read", "api_key:update", "coder:all", + "coder:apikeys.manage_self", "coder:application_connect", + "coder:templates.author", + "coder:templates.build", + "coder:workspaces.access", + "coder:workspaces.create", + "coder:workspaces.delete", + "coder:workspaces.operate", "file:*", "file:create", "file:read", @@ -11565,7 +11572,14 @@ const docTemplate = `{ "APIKeyScopeApiKeyRead", "APIKeyScopeApiKeyUpdate", "APIKeyScopeCoderAll", + "APIKeyScopeCoderApikeysManageSelf", "APIKeyScopeCoderApplicationConnect", + "APIKeyScopeCoderTemplatesAuthor", + "APIKeyScopeCoderTemplatesBuild", + "APIKeyScopeCoderWorkspacesAccess", + "APIKeyScopeCoderWorkspacesCreate", + "APIKeyScopeCoderWorkspacesDelete", + "APIKeyScopeCoderWorkspacesOperate", "APIKeyScopeFileAll", "APIKeyScopeFileCreate", "APIKeyScopeFileRead", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 5fa20c5867..731e1720c0 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10245,7 +10245,14 @@ "api_key:read", "api_key:update", "coder:all", + "coder:apikeys.manage_self", "coder:application_connect", + "coder:templates.author", + "coder:templates.build", + "coder:workspaces.access", + "coder:workspaces.create", + "coder:workspaces.delete", + "coder:workspaces.operate", "file:*", "file:create", "file:read", @@ -10281,7 +10288,14 @@ "APIKeyScopeApiKeyRead", "APIKeyScopeApiKeyUpdate", "APIKeyScopeCoderAll", + "APIKeyScopeCoderApikeysManageSelf", "APIKeyScopeCoderApplicationConnect", + "APIKeyScopeCoderTemplatesAuthor", + "APIKeyScopeCoderTemplatesBuild", + "APIKeyScopeCoderWorkspacesAccess", + "APIKeyScopeCoderWorkspacesCreate", + "APIKeyScopeCoderWorkspacesDelete", + "APIKeyScopeCoderWorkspacesOperate", "APIKeyScopeFileAll", "APIKeyScopeFileCreate", "APIKeyScopeFileRead", diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index b517099522..7a4be98b95 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -150,7 +150,14 @@ CREATE TYPE api_key_scope AS ENUM ( 'workspace_proxy:create', 'workspace_proxy:delete', 'workspace_proxy:read', - 'workspace_proxy:update' + 'workspace_proxy:update', + 'coder:workspaces.create', + 'coder:workspaces.operate', + 'coder:workspaces.delete', + 'coder:workspaces.access', + 'coder:templates.build', + 'coder:templates.author', + 'coder:apikeys.manage_self' ); CREATE TYPE app_sharing_level AS ENUM ( diff --git a/coderd/database/migrations/000375_add_composite_api_key_scopes.down.sql b/coderd/database/migrations/000375_add_composite_api_key_scopes.down.sql new file mode 100644 index 0000000000..46aa4042e0 --- /dev/null +++ b/coderd/database/migrations/000375_add_composite_api_key_scopes.down.sql @@ -0,0 +1,3 @@ +-- No-op: keep enum values to avoid dependency churn. +-- If strict removal is required, create a new enum type without these values, +-- cast columns, drop the old type, and rename. diff --git a/coderd/database/migrations/000375_add_composite_api_key_scopes.up.sql b/coderd/database/migrations/000375_add_composite_api_key_scopes.up.sql new file mode 100644 index 0000000000..705689cf7e --- /dev/null +++ b/coderd/database/migrations/000375_add_composite_api_key_scopes.up.sql @@ -0,0 +1,9 @@ +-- Add high-level composite coder:* API key scopes +-- These values are persisted so that tokens can store coder:* names directly. +ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'coder:workspaces.create'; +ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'coder:workspaces.operate'; +ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'coder:workspaces.delete'; +ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'coder:workspaces.access'; +ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'coder:templates.build'; +ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'coder:templates.author'; +ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'coder:apikeys.manage_self'; diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 765a053a14..230b9274ea 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -203,6 +203,13 @@ func (s APIKeyScopes) Expand() (rbac.Scope, error) { } } + // De-duplicate permissions across Site/Org/User + merged.Site = rbac.DeduplicatePermissions(merged.Site) + for orgID, perms := range merged.Org { + merged.Org[orgID] = rbac.DeduplicatePermissions(perms) + } + merged.User = rbac.DeduplicatePermissions(merged.User) + if allowAll || len(allowSet) == 0 { merged.AllowIDList = []rbac.AllowListElement{rbac.AllowListAll()} } else { diff --git a/coderd/database/models.go b/coderd/database/models.go index a3224f3857..eecb657edb 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -159,6 +159,13 @@ const ( ApiKeyScopeWorkspaceProxyDelete APIKeyScope = "workspace_proxy:delete" ApiKeyScopeWorkspaceProxyRead APIKeyScope = "workspace_proxy:read" ApiKeyScopeWorkspaceProxyUpdate APIKeyScope = "workspace_proxy:update" + ApiKeyScopeCoderWorkspacescreate APIKeyScope = "coder:workspaces.create" + ApiKeyScopeCoderWorkspacesoperate APIKeyScope = "coder:workspaces.operate" + ApiKeyScopeCoderWorkspacesdelete APIKeyScope = "coder:workspaces.delete" + ApiKeyScopeCoderWorkspacesaccess APIKeyScope = "coder:workspaces.access" + ApiKeyScopeCoderTemplatesbuild APIKeyScope = "coder:templates.build" + ApiKeyScopeCoderTemplatesauthor APIKeyScope = "coder:templates.author" + ApiKeyScopeCoderApikeysmanageSelf APIKeyScope = "coder:apikeys.manage_self" ) func (e *APIKeyScope) Scan(src interface{}) error { @@ -337,7 +344,14 @@ func (e APIKeyScope) Valid() bool { ApiKeyScopeWorkspaceProxyCreate, ApiKeyScopeWorkspaceProxyDelete, ApiKeyScopeWorkspaceProxyRead, - ApiKeyScopeWorkspaceProxyUpdate: + ApiKeyScopeWorkspaceProxyUpdate, + ApiKeyScopeCoderWorkspacescreate, + ApiKeyScopeCoderWorkspacesoperate, + ApiKeyScopeCoderWorkspacesdelete, + ApiKeyScopeCoderWorkspacesaccess, + ApiKeyScopeCoderTemplatesbuild, + ApiKeyScopeCoderTemplatesauthor, + ApiKeyScopeCoderApikeysmanageSelf: return true } return false @@ -485,6 +499,13 @@ func AllAPIKeyScopeValues() []APIKeyScope { ApiKeyScopeWorkspaceProxyDelete, ApiKeyScopeWorkspaceProxyRead, ApiKeyScopeWorkspaceProxyUpdate, + ApiKeyScopeCoderWorkspacescreate, + ApiKeyScopeCoderWorkspacesoperate, + ApiKeyScopeCoderWorkspacesdelete, + ApiKeyScopeCoderWorkspacesaccess, + ApiKeyScopeCoderTemplatesbuild, + ApiKeyScopeCoderTemplatesauthor, + ApiKeyScopeCoderApikeysmanageSelf, } } diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index bc75991d01..e5f822b91e 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "sort" + "strconv" "strings" "github.com/google/uuid" @@ -863,3 +864,22 @@ func Permissions(perms map[string][]policy.Action) []Permission { }) return list } + +// DeduplicatePermissions removes duplicate Permission entries while preserving +// the original order of the first occurrence for deterministic evaluation. +func DeduplicatePermissions(perms []Permission) []Permission { + if len(perms) == 0 { + return perms + } + seen := make(map[string]struct{}, len(perms)) + deduped := make([]Permission, 0, len(perms)) + for _, perm := range perms { + key := perm.ResourceType + "\x00" + string(perm.Action) + "\x00" + strconv.FormatBool(perm.Negate) + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + deduped = append(deduped, perm) + } + return deduped +} diff --git a/coderd/rbac/roles_internal_test.go b/coderd/rbac/roles_internal_test.go index f851280a04..5c7a00d899 100644 --- a/coderd/rbac/roles_internal_test.go +++ b/coderd/rbac/roles_internal_test.go @@ -249,6 +249,27 @@ func TestRoleByName(t *testing.T) { }) } +func TestDeduplicatePermissions(t *testing.T) { + t.Parallel() + + perms := []Permission{ + {ResourceType: ResourceWorkspace.Type, Action: policy.ActionRead}, + {ResourceType: ResourceWorkspace.Type, Action: policy.ActionRead}, + {ResourceType: ResourceWorkspace.Type, Action: policy.ActionUpdate}, + {ResourceType: ResourceWorkspace.Type, Action: policy.ActionRead, Negate: true}, + {ResourceType: ResourceWorkspace.Type, Action: policy.ActionRead, Negate: true}, + } + + got := DeduplicatePermissions(perms) + want := []Permission{ + {ResourceType: ResourceWorkspace.Type, Action: policy.ActionRead}, + {ResourceType: ResourceWorkspace.Type, Action: policy.ActionUpdate}, + {ResourceType: ResourceWorkspace.Type, Action: policy.ActionRead, Negate: true}, + } + + require.Equal(t, want, got) +} + // SameAs compares 2 roles for equality. func equalRoles(t *testing.T, a, b Role) { require.Equal(t, a.Identifier, b.Identifier, "role names") diff --git a/coderd/rbac/scopes.go b/coderd/rbac/scopes.go index eb057e1dc1..72a825003c 100644 --- a/coderd/rbac/scopes.go +++ b/coderd/rbac/scopes.go @@ -3,6 +3,7 @@ package rbac import ( "fmt" "slices" + "sort" "strings" "github.com/google/uuid" @@ -120,6 +121,56 @@ func BuiltinScopeNames() []ScopeName { return names } +// Composite coder:* scopes expand to multiple low-level resource:action permissions +// at Site level. These names are persisted in the DB and expanded during +// authorization. +var compositePerms = map[ScopeName]map[string][]policy.Action{ + "coder:workspaces.create": { + ResourceTemplate.Type: {policy.ActionRead, policy.ActionUse}, + ResourceWorkspace.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionRead}, + }, + "coder:workspaces.operate": { + ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate}, + }, + "coder:workspaces.delete": { + ResourceWorkspace.Type: {policy.ActionRead, policy.ActionDelete}, + }, + "coder:workspaces.access": { + ResourceWorkspace.Type: {policy.ActionRead, policy.ActionSSH, policy.ActionApplicationConnect}, + }, + "coder:templates.build": { + ResourceTemplate.Type: {policy.ActionRead}, + ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, + "provisioner_jobs": {policy.ActionRead}, + }, + "coder:templates.author": { + ResourceTemplate.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete, policy.ActionViewInsights}, + ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, + }, + "coder:apikeys.manage_self": { + ResourceApiKey.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, + }, +} + +// CompositeSitePermissions returns the site-level Permission list for a coder:* scope. +func CompositeSitePermissions(name ScopeName) ([]Permission, bool) { + perms, ok := compositePerms[name] + if !ok { + return nil, false + } + return Permissions(perms), true +} + +// CompositeScopeNames lists all high-level coder:* names in sorted order. +func CompositeScopeNames() []string { + out := make([]string, 0, len(compositePerms)) + for k := range compositePerms { + out = append(out, string(k)) + } + sort.Strings(out) + return out +} + type ExpandableScope interface { Expand() (Scope, error) // Name is for logging and tracing purposes, we want to know the human @@ -175,6 +226,19 @@ func ExpandScope(scope ScopeName) (Scope, error) { if role, ok := builtinScopes[scope]; ok { return role, nil } + if site, ok := CompositeSitePermissions(scope); ok { + return Scope{ + Role: Role{ + Identifier: RoleIdentifier{Name: fmt.Sprintf("Scope_%s", scope)}, + DisplayName: string(scope), + Site: site, + Org: map[string][]Permission{}, + User: []Permission{}, + }, + // Composites are site-level; allow-list empty by default + AllowIDList: []AllowListElement{}, + }, nil + } if res, act, ok := parseLowLevelScope(scope); ok { return expandLowLevel(res, act), nil } diff --git a/coderd/rbac/scopes_catalog.go b/coderd/rbac/scopes_catalog.go index a5ae10199c..f8deb57b46 100644 --- a/coderd/rbac/scopes_catalog.go +++ b/coderd/rbac/scopes_catalog.go @@ -52,6 +52,17 @@ var externalLowLevel = map[ScopeName]struct{}{ "user_secret:*": {}, } +// Public composite coder:* scopes exposed to users. +var externalComposite = map[ScopeName]struct{}{ + "coder:workspaces.create": {}, + "coder:workspaces.operate": {}, + "coder:workspaces.delete": {}, + "coder:workspaces.access": {}, + "coder:templates.build": {}, + "coder:templates.author": {}, + "coder:apikeys.manage_self": {}, +} + // IsExternalScope returns true if the scope is public, including the // `all` and `application_connect` special scopes and the curated // low-level resource:action scopes. @@ -64,15 +75,18 @@ func IsExternalScope(name ScopeName) bool { if _, ok := externalLowLevel[name]; ok { return true } + if _, ok := externalComposite[name]; ok { + return true + } return false } -// ExternalScopeNames returns a sorted list of all public scopes, which includes -// the `all` and `application_connect` special scopes and the curated public -// low-level names. +// ExternalScopeNames returns a sorted list of all public scopes, which +// includes the `all` and `application_connect` special scopes, curated +// low-level resource:action names, and curated composite coder:* scopes. func ExternalScopeNames() []string { - names := make([]string, 0, len(externalLowLevel)+2) + names := make([]string, 0, len(externalLowLevel)+len(externalComposite)+2) names = append(names, string(ScopeAll)) names = append(names, string(ScopeApplicationConnect)) @@ -83,6 +97,11 @@ func ExternalScopeNames() []string { } } + // curated composite names + for name := range externalComposite { + names = append(names, string(name)) + } + sort.Slice(names, func(i, j int) bool { return strings.Compare(names[i], names[j]) < 0 }) return names } diff --git a/coderd/rbac/scopes_catalog_internal_test.go b/coderd/rbac/scopes_catalog_internal_test.go index 5c47ebe8fa..4530447e99 100644 --- a/coderd/rbac/scopes_catalog_internal_test.go +++ b/coderd/rbac/scopes_catalog_internal_test.go @@ -2,6 +2,7 @@ package rbac import ( "sort" + "strings" "testing" "github.com/stretchr/testify/require" @@ -18,7 +19,7 @@ func TestExternalScopeNames(t *testing.T) { sort.Strings(sorted) require.Equal(t, sorted, names) - // Ensure each entry parses and expands to site-only + // Ensure each entry expands to site-only for _, name := range names { // Skip `all` and `application_connect` since they do not // expand into a low level scope. @@ -27,6 +28,20 @@ func TestExternalScopeNames(t *testing.T) { continue } + // Composite coder:* scopes expand to one or more site permissions. + if strings.HasPrefix(name, "coder:") { + s, err := ScopeName(name).Expand() + require.NoErrorf(t, err, "catalog entry should expand: %s", name) + require.NotEmpty(t, s.Site) + expected, ok := CompositeSitePermissions(ScopeName(name)) + require.Truef(t, ok, "expected composite scope definition: %s", name) + require.ElementsMatchf(t, expected, s.Site, "unexpected expanded permissions for %s", name) + require.Empty(t, s.Org) + require.Empty(t, s.User) + continue + } + + // Low-level scopes must parse to a single permission. res, act, ok := parseLowLevelScope(ScopeName(name)) require.Truef(t, ok, "catalog entry should parse: %s", name) @@ -46,6 +61,7 @@ func TestIsExternalScope(t *testing.T) { require.True(t, IsExternalScope("workspace:read")) require.True(t, IsExternalScope("template:use")) require.True(t, IsExternalScope("workspace:*")) + require.True(t, IsExternalScope("coder:workspaces.create")) require.False(t, IsExternalScope("debug_info:read")) // internal-only require.False(t, IsExternalScope("unknown:read")) } diff --git a/codersdk/apikey_scopes_gen.go b/codersdk/apikey_scopes_gen.go index 4378b1847c..a399a014b4 100644 --- a/codersdk/apikey_scopes_gen.go +++ b/codersdk/apikey_scopes_gen.go @@ -12,7 +12,14 @@ const ( APIKeyScopeApiKeyRead APIKeyScope = "api_key:read" APIKeyScopeApiKeyUpdate APIKeyScope = "api_key:update" APIKeyScopeCoderAll APIKeyScope = "coder:all" + APIKeyScopeCoderApikeysManageSelf APIKeyScope = "coder:apikeys.manage_self" APIKeyScopeCoderApplicationConnect APIKeyScope = "coder:application_connect" + APIKeyScopeCoderTemplatesAuthor APIKeyScope = "coder:templates.author" + APIKeyScopeCoderTemplatesBuild APIKeyScope = "coder:templates.build" + APIKeyScopeCoderWorkspacesAccess APIKeyScope = "coder:workspaces.access" + APIKeyScopeCoderWorkspacesCreate APIKeyScope = "coder:workspaces.create" + APIKeyScopeCoderWorkspacesDelete APIKeyScope = "coder:workspaces.delete" + APIKeyScopeCoderWorkspacesOperate APIKeyScope = "coder:workspaces.operate" APIKeyScopeFileAll APIKeyScope = "file:*" APIKeyScopeFileCreate APIKeyScope = "file:create" APIKeyScopeFileRead APIKeyScope = "file:read" @@ -48,7 +55,14 @@ var PublicAPIKeyScopes = []APIKeyScope{ APIKeyScopeApiKeyRead, APIKeyScopeApiKeyUpdate, APIKeyScopeCoderAll, + APIKeyScopeCoderApikeysManageSelf, APIKeyScopeCoderApplicationConnect, + APIKeyScopeCoderTemplatesAuthor, + APIKeyScopeCoderTemplatesBuild, + APIKeyScopeCoderWorkspacesAccess, + APIKeyScopeCoderWorkspacesCreate, + APIKeyScopeCoderWorkspacesDelete, + APIKeyScopeCoderWorkspacesOperate, APIKeyScopeFileAll, APIKeyScopeFileCreate, APIKeyScopeFileRead, diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 0459093732..98324941a1 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -721,7 +721,14 @@ | `api_key:read` | | `api_key:update` | | `coder:all` | +| `coder:apikeys.manage_self` | | `coder:application_connect` | +| `coder:templates.author` | +| `coder:templates.build` | +| `coder:workspaces.access` | +| `coder:workspaces.create` | +| `coder:workspaces.delete` | +| `coder:workspaces.operate` | | `file:*` | | `file:create` | | `file:read` | diff --git a/scripts/apikeyscopesgen/main.go b/scripts/apikeyscopesgen/main.go index 4bb0bd3727..988c4cb2f0 100644 --- a/scripts/apikeyscopesgen/main.go +++ b/scripts/apikeyscopesgen/main.go @@ -112,6 +112,7 @@ func pascal(s string) string { s = strings.ReplaceAll(s, "_", " ") s = strings.ReplaceAll(s, "-", " ") s = strings.ReplaceAll(s, ":", " ") + s = strings.ReplaceAll(s, ".", " ") words := strings.Fields(s) for i := range words { words[i] = strings.ToUpper(words[i][:1]) + words[i][1:] diff --git a/scripts/check-scopes/main.go b/scripts/check-scopes/main.go index 503af5b4ad..e79be506ae 100644 --- a/scripts/check-scopes/main.go +++ b/scripts/check-scopes/main.go @@ -11,6 +11,7 @@ import ( "golang.org/x/xerrors" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" ) @@ -60,6 +61,7 @@ func main() { // expectedFromRBAC returns the set of : pairs derived from RBACPermissions. func expectedFromRBAC() map[string]struct{} { want := make(map[string]struct{}) + // Low-level : for resource, def := range policy.RBACPermissions { if resource == policy.WildcardSymbol { // Ignore wildcard entry; it has no concrete : pairs. @@ -70,6 +72,10 @@ func expectedFromRBAC() map[string]struct{} { want[key] = struct{}{} } } + // Composite coder:* names + for _, n := range rbac.CompositeScopeNames() { + want[n] = struct{}{} + } return want } diff --git a/scripts/generate_api_key_scope_enum/main.go b/scripts/generate_api_key_scope_enum/main.go index 9569db77b4..130dd86533 100644 --- a/scripts/generate_api_key_scope_enum/main.go +++ b/scripts/generate_api_key_scope_enum/main.go @@ -4,6 +4,7 @@ import ( "fmt" "sort" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" ) @@ -18,6 +19,8 @@ func main() { vals = append(vals, fmt.Sprintf("%s:%s", resource, action)) } } + // Include composite coder:* scopes as first-class enum values + vals = append(vals, rbac.CompositeScopeNames()...) sort.Strings(vals) for _, v := range vals { if _, ok := seen[v]; ok { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 0cea1c5920..5e798baecf 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -120,7 +120,14 @@ export type APIKeyScope = | "api_key:update" | "application_connect" | "coder:all" + | "coder:apikeys.manage_self" | "coder:application_connect" + | "coder:templates.author" + | "coder:templates.build" + | "coder:workspaces.access" + | "coder:workspaces.create" + | "coder:workspaces.delete" + | "coder:workspaces.operate" | "file:*" | "file:create" | "file:read" @@ -156,7 +163,14 @@ export const APIKeyScopes: APIKeyScope[] = [ "api_key:update", "application_connect", "coder:all", + "coder:apikeys.manage_self", "coder:application_connect", + "coder:templates.author", + "coder:templates.build", + "coder:workspaces.access", + "coder:workspaces.create", + "coder:workspaces.delete", + "coder:workspaces.operate", "file:*", "file:create", "file:read",