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

Commit8210ef2

Browse files
committed
feat: add composite coder:* API key scopes for better UX
Add high-level composite scopes that expand to multiple low-levelpermissions:- coder:workspaces.create - Template read/use + workspace CRUD- coder:workspaces.operate - Workspace read/update- coder:workspaces.delete - Workspace read/delete- coder:workspaces.access - Workspace read/SSH/app connect- coder:templates.build - Template read + file ops + provisioner jobs- coder:templates.author - Full template management + insights- coder:apikeys.manage_self - Self API key managementThese composite scopes provide intuitive high-level permissions whilemaintaining granular control through existing low-level scopes.Database enum values are persisted to enable storing composite namesdirectly in tokens.
1 parentaaa5071 commit8210ef2

File tree

18 files changed

+267
-7
lines changed

18 files changed

+267
-7
lines changed

‎coderd/apidoc/docs.go‎

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/apidoc/swagger.json‎

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/database/dump.sql‎

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- No-op: keep enum values to avoid dependency churn.
2+
-- If strict removal is required, create a new enum type without these values,
3+
-- cast columns, drop the old type, and rename.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- Add high-level composite coder:* API key scopes
2+
-- These values are persisted so that tokens can store coder:* names directly.
3+
ALTERTYPE api_key_scope ADD VALUE IF NOT EXISTS'coder:workspaces.create';
4+
ALTERTYPE api_key_scope ADD VALUE IF NOT EXISTS'coder:workspaces.operate';
5+
ALTERTYPE api_key_scope ADD VALUE IF NOT EXISTS'coder:workspaces.delete';
6+
ALTERTYPE api_key_scope ADD VALUE IF NOT EXISTS'coder:workspaces.access';
7+
ALTERTYPE api_key_scope ADD VALUE IF NOT EXISTS'coder:templates.build';
8+
ALTERTYPE api_key_scope ADD VALUE IF NOT EXISTS'coder:templates.author';
9+
ALTERTYPE api_key_scope ADD VALUE IF NOT EXISTS'coder:apikeys.manage_self';

‎coderd/database/modelmethods.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,13 @@ func (s APIKeyScopes) Expand() (rbac.Scope, error) {
203203
}
204204
}
205205

206+
// De-duplicate permissions across Site/Org/User
207+
merged.Site=rbac.DeduplicatePermissions(merged.Site)
208+
fororgID,perms:=rangemerged.Org {
209+
merged.Org[orgID]=rbac.DeduplicatePermissions(perms)
210+
}
211+
merged.User=rbac.DeduplicatePermissions(merged.User)
212+
206213
ifallowAll||len(allowSet)==0 {
207214
merged.AllowIDList= []rbac.AllowListElement{rbac.AllowListAll()}
208215
}else {

‎coderd/database/models.go‎

Lines changed: 22 additions & 1 deletion
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/rbac/roles.go‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"errors"
66
"sort"
7+
"strconv"
78
"strings"
89

910
"github.com/google/uuid"
@@ -863,3 +864,22 @@ func Permissions(perms map[string][]policy.Action) []Permission {
863864
})
864865
returnlist
865866
}
867+
868+
// DeduplicatePermissions removes duplicate Permission entries while preserving
869+
// the original order of the first occurrence for deterministic evaluation.
870+
funcDeduplicatePermissions(perms []Permission) []Permission {
871+
iflen(perms)==0 {
872+
returnperms
873+
}
874+
seen:=make(map[string]struct{},len(perms))
875+
deduped:=make([]Permission,0,len(perms))
876+
for_,perm:=rangeperms {
877+
key:=perm.ResourceType+"\x00"+string(perm.Action)+"\x00"+strconv.FormatBool(perm.Negate)
878+
if_,ok:=seen[key];ok {
879+
continue
880+
}
881+
seen[key]=struct{}{}
882+
deduped=append(deduped,perm)
883+
}
884+
returndeduped
885+
}

‎coderd/rbac/roles_internal_test.go‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,27 @@ func TestRoleByName(t *testing.T) {
249249
})
250250
}
251251

252+
funcTestDeduplicatePermissions(t*testing.T) {
253+
t.Parallel()
254+
255+
perms:= []Permission{
256+
{ResourceType:ResourceWorkspace.Type,Action:policy.ActionRead},
257+
{ResourceType:ResourceWorkspace.Type,Action:policy.ActionRead},
258+
{ResourceType:ResourceWorkspace.Type,Action:policy.ActionUpdate},
259+
{ResourceType:ResourceWorkspace.Type,Action:policy.ActionRead,Negate:true},
260+
{ResourceType:ResourceWorkspace.Type,Action:policy.ActionRead,Negate:true},
261+
}
262+
263+
got:=DeduplicatePermissions(perms)
264+
want:= []Permission{
265+
{ResourceType:ResourceWorkspace.Type,Action:policy.ActionRead},
266+
{ResourceType:ResourceWorkspace.Type,Action:policy.ActionUpdate},
267+
{ResourceType:ResourceWorkspace.Type,Action:policy.ActionRead,Negate:true},
268+
}
269+
270+
require.Equal(t,want,got)
271+
}
272+
252273
// SameAs compares 2 roles for equality.
253274
funcequalRoles(t*testing.T,a,bRole) {
254275
require.Equal(t,a.Identifier,b.Identifier,"role names")

‎coderd/rbac/scopes.go‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package rbac
33
import (
44
"fmt"
55
"slices"
6+
"sort"
67
"strings"
78

89
"github.com/google/uuid"
@@ -120,6 +121,56 @@ func BuiltinScopeNames() []ScopeName {
120121
returnnames
121122
}
122123

124+
// Composite coder:* scopes expand to multiple low-level resource:action permissions
125+
// at Site level. These names are persisted in the DB and expanded during
126+
// authorization.
127+
varcompositePerms=map[ScopeName]map[string][]policy.Action{
128+
"coder:workspaces.create": {
129+
ResourceTemplate.Type: {policy.ActionRead,policy.ActionUse},
130+
ResourceWorkspace.Type: {policy.ActionCreate,policy.ActionUpdate,policy.ActionRead},
131+
},
132+
"coder:workspaces.operate": {
133+
ResourceWorkspace.Type: {policy.ActionRead,policy.ActionUpdate},
134+
},
135+
"coder:workspaces.delete": {
136+
ResourceWorkspace.Type: {policy.ActionRead,policy.ActionDelete},
137+
},
138+
"coder:workspaces.access": {
139+
ResourceWorkspace.Type: {policy.ActionRead,policy.ActionSSH,policy.ActionApplicationConnect},
140+
},
141+
"coder:templates.build": {
142+
ResourceTemplate.Type: {policy.ActionRead},
143+
ResourceFile.Type: {policy.ActionCreate,policy.ActionRead},
144+
"provisioner_jobs": {policy.ActionRead},
145+
},
146+
"coder:templates.author": {
147+
ResourceTemplate.Type: {policy.ActionRead,policy.ActionCreate,policy.ActionUpdate,policy.ActionDelete,policy.ActionViewInsights},
148+
ResourceFile.Type: {policy.ActionCreate,policy.ActionRead},
149+
},
150+
"coder:apikeys.manage_self": {
151+
ResourceApiKey.Type: {policy.ActionRead,policy.ActionCreate,policy.ActionUpdate,policy.ActionDelete},
152+
},
153+
}
154+
155+
// CompositeSitePermissions returns the site-level Permission list for a coder:* scope.
156+
funcCompositeSitePermissions(nameScopeName) ([]Permission,bool) {
157+
perms,ok:=compositePerms[name]
158+
if!ok {
159+
returnnil,false
160+
}
161+
returnPermissions(perms),true
162+
}
163+
164+
// CompositeScopeNames lists all high-level coder:* names in sorted order.
165+
funcCompositeScopeNames() []string {
166+
out:=make([]string,0,len(compositePerms))
167+
fork:=rangecompositePerms {
168+
out=append(out,string(k))
169+
}
170+
sort.Strings(out)
171+
returnout
172+
}
173+
123174
typeExpandableScopeinterface {
124175
Expand() (Scope,error)
125176
// Name is for logging and tracing purposes, we want to know the human
@@ -175,6 +226,19 @@ func ExpandScope(scope ScopeName) (Scope, error) {
175226
ifrole,ok:=builtinScopes[scope];ok {
176227
returnrole,nil
177228
}
229+
ifsite,ok:=CompositeSitePermissions(scope);ok {
230+
returnScope{
231+
Role:Role{
232+
Identifier:RoleIdentifier{Name:fmt.Sprintf("Scope_%s",scope)},
233+
DisplayName:string(scope),
234+
Site:site,
235+
Org:map[string][]Permission{},
236+
User: []Permission{},
237+
},
238+
// Composites are site-level; allow-list empty by default
239+
AllowIDList: []AllowListElement{},
240+
},nil
241+
}
178242
ifres,act,ok:=parseLowLevelScope(scope);ok {
179243
returnexpandLowLevel(res,act),nil
180244
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp