Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: implement composite API key scopes for workspaces and templates#19945

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletionscoderd/apidoc/docs.go
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

14 changes: 14 additions & 0 deletionscoderd/apidoc/swagger.json
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

9 changes: 8 additions & 1 deletioncoderd/database/dump.sql
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -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.
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
-- Add high-level composite coder:* API key scopes
-- These values are persisted so that tokens can store coder:* names directly.
ALTERTYPE api_key_scope ADD VALUE IF NOT EXISTS'coder:workspaces.create';
ALTERTYPE api_key_scope ADD VALUE IF NOT EXISTS'coder:workspaces.operate';
ALTERTYPE api_key_scope ADD VALUE IF NOT EXISTS'coder:workspaces.delete';
ALTERTYPE api_key_scope ADD VALUE IF NOT EXISTS'coder:workspaces.access';
ALTERTYPE api_key_scope ADD VALUE IF NOT EXISTS'coder:templates.build';
ALTERTYPE api_key_scope ADD VALUE IF NOT EXISTS'coder:templates.author';
ALTERTYPE api_key_scope ADD VALUE IF NOT EXISTS'coder:apikeys.manage_self';
7 changes: 7 additions & 0 deletionscoderd/database/modelmethods.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -203,6 +203,13 @@ func (s APIKeyScopes) Expand() (rbac.Scope, error) {
}
}

// De-duplicate permissions across Site/Org/User
merged.Site=rbac.DeduplicatePermissions(merged.Site)
fororgID,perms:=rangemerged.Org {
merged.Org[orgID]=rbac.DeduplicatePermissions(perms)
}
merged.User=rbac.DeduplicatePermissions(merged.User)

ifallowAll||len(allowSet)==0 {
merged.AllowIDList= []rbac.AllowListElement{rbac.AllowListAll()}
}else {
Expand Down
23 changes: 22 additions & 1 deletioncoderd/database/models.go
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

20 changes: 20 additions & 0 deletionscoderd/rbac/roles.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"sort"
"strconv"
"strings"

"github.com/google/uuid"
Expand DownExpand Up@@ -863,3 +864,22 @@ func Permissions(perms map[string][]policy.Action) []Permission {
})
returnlist
}

// DeduplicatePermissions removes duplicate Permission entries while preserving
// the original order of the first occurrence for deterministic evaluation.
funcDeduplicatePermissions(perms []Permission) []Permission {
iflen(perms)==0 {
returnperms
}
seen:=make(map[string]struct{},len(perms))
deduped:=make([]Permission,0,len(perms))
for_,perm:=rangeperms {
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)
}
returndeduped
}
21 changes: 21 additions & 0 deletionscoderd/rbac/roles_internal_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -249,6 +249,27 @@ func TestRoleByName(t *testing.T) {
})
}

funcTestDeduplicatePermissions(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.
funcequalRoles(t*testing.T,a,bRole) {
require.Equal(t,a.Identifier,b.Identifier,"role names")
Expand Down
64 changes: 64 additions & 0 deletionscoderd/rbac/scopes.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ package rbac
import (
"fmt"
"slices"
"sort"
"strings"

"github.com/google/uuid"
Expand DownExpand Up@@ -120,6 +121,56 @@ func BuiltinScopeNames() []ScopeName {
returnnames
}

// 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.
varcompositePerms=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.
funcCompositeSitePermissions(nameScopeName) ([]Permission,bool) {
perms,ok:=compositePerms[name]
if!ok {
returnnil,false
}
returnPermissions(perms),true
}

// CompositeScopeNames lists all high-level coder:* names in sorted order.
funcCompositeScopeNames() []string {
out:=make([]string,0,len(compositePerms))
fork:=rangecompositePerms {
out=append(out,string(k))
}
sort.Strings(out)
returnout
}

typeExpandableScopeinterface {
Expand() (Scope,error)
// Name is for logging and tracing purposes, we want to know the human
Expand DownExpand Up@@ -175,6 +226,19 @@ func ExpandScope(scope ScopeName) (Scope, error) {
ifrole,ok:=builtinScopes[scope];ok {
returnrole,nil
}
ifsite,ok:=CompositeSitePermissions(scope);ok {
returnScope{
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
}
ifres,act,ok:=parseLowLevelScope(scope);ok {
returnexpandLowLevel(res,act),nil
}
Expand Down
27 changes: 23 additions & 4 deletionscoderd/rbac/scopes_catalog.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,17 @@ var externalLowLevel = map[ScopeName]struct{}{
"user_secret:*": {},
}

// Public composite coder:* scopes exposed to users.
varexternalComposite=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.
Expand All@@ -64,15 +75,18 @@ func IsExternalScope(name ScopeName) bool {
if_,ok:=externalLowLevel[name];ok {
returntrue
}
if_,ok:=externalComposite[name];ok {
returntrue
}

returnfalse
}

// ExternalScopeNames returns a sorted list of all public scopes, which includes
// the `all` and `application_connect` special scopes and thecurated public
// low-level names.
// ExternalScopeNames returns a sorted list of all public scopes, which
//includesthe `all` and `application_connect` special scopes,curated
// low-levelresource:actionnames, and curated composite coder:* scopes.
funcExternalScopeNames() []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))

Expand All@@ -83,6 +97,11 @@ func ExternalScopeNames() []string {
}
}

// curated composite names
forname:=rangeexternalComposite {
names=append(names,string(name))
}

sort.Slice(names,func(i,jint)bool {returnstrings.Compare(names[i],names[j])<0 })
returnnames
}
18 changes: 17 additions & 1 deletioncoderd/rbac/scopes_catalog_internal_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ package rbac

import (
"sort"
"strings"
"testing"

"github.com/stretchr/testify/require"
Expand All@@ -18,7 +19,7 @@ func TestExternalScopeNames(t *testing.T) {
sort.Strings(sorted)
require.Equal(t,sorted,names)

// Ensure each entryparses andexpands to site-only
// Ensure each entry expands to site-only
for_,name:=rangenames {
// Skip `all` and `application_connect` since they do not
// expand into a low level scope.
Expand All@@ -27,6 +28,20 @@ func TestExternalScopeNames(t *testing.T) {
continue
}

// Composite coder:* scopes expand to one or more site permissions.
ifstrings.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)

Expand All@@ -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"))
}
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp