mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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.
This commit is contained in:
Generated
+14
@@ -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",
|
||||
|
||||
Generated
+14
@@ -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",
|
||||
|
||||
Generated
+8
-1
@@ -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 (
|
||||
|
||||
@@ -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.
|
||||
@@ -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';
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Generated
+7
@@ -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` |
|
||||
|
||||
@@ -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:]
|
||||
|
||||
@@ -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 <resource>:<action> pairs derived from RBACPermissions.
|
||||
func expectedFromRBAC() map[string]struct{} {
|
||||
want := make(map[string]struct{})
|
||||
// Low-level <resource>:<action>
|
||||
for resource, def := range policy.RBACPermissions {
|
||||
if resource == policy.WildcardSymbol {
|
||||
// Ignore wildcard entry; it has no concrete <resource>:<action> 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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Generated
+14
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user