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

Commit5ff29e3

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 parentb630882 commit5ff29e3

File tree

16 files changed

+238
-7
lines changed

16 files changed

+238
-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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,29 @@ func (s APIKeyScopes) Expand() (rbac.Scope, error) {
203203
}
204204
}
205205

206+
// De-duplicate permissions across Site/Org/User
207+
dedup:=func(in []rbac.Permission) []rbac.Permission {
208+
iflen(in)==0 {
209+
returnin
210+
}
211+
seen:=make(map[string]struct{},len(in))
212+
out:=make([]rbac.Permission,0,len(in))
213+
for_,p:=rangein {
214+
key:=p.ResourceType+"\x00"+string(p.Action)+"\x00"+strconv.FormatBool(p.Negate)
215+
if_,ok:=seen[key];ok {
216+
continue
217+
}
218+
seen[key]=struct{}{}
219+
out=append(out,p)
220+
}
221+
returnout
222+
}
223+
merged.Site=dedup(merged.Site)
224+
fororgID,perms:=rangemerged.Org {
225+
merged.Org[orgID]=dedup(perms)
226+
}
227+
merged.User=dedup(merged.User)
228+
206229
ifallowAll||len(allowSet)==0 {
207230
merged.AllowIDList= []rbac.AllowListElement{rbac.AllowListAll()}
208231
}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/scopes.go‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package rbac
22

33
import (
44
"fmt"
5+
"sort"
56
"strings"
67

78
"github.com/google/uuid"
@@ -107,6 +108,56 @@ var builtinScopes = map[ScopeName]Scope{
107108
},
108109
}
109110

111+
// Composite coder:* scopes expand to multiple low-level resource:action permissions
112+
// at Site level. These names are persisted in the DB and expanded during
113+
// authorization.
114+
varcompositePerms=map[ScopeName]map[string][]policy.Action{
115+
"coder:workspaces.create": {
116+
ResourceTemplate.Type: {policy.ActionRead,policy.ActionUse},
117+
ResourceWorkspace.Type: {policy.ActionCreate,policy.ActionUpdate,policy.ActionRead},
118+
},
119+
"coder:workspaces.operate": {
120+
ResourceWorkspace.Type: {policy.ActionRead,policy.ActionUpdate},
121+
},
122+
"coder:workspaces.delete": {
123+
ResourceWorkspace.Type: {policy.ActionRead,policy.ActionDelete},
124+
},
125+
"coder:workspaces.access": {
126+
ResourceWorkspace.Type: {policy.ActionRead,policy.ActionSSH,policy.ActionApplicationConnect},
127+
},
128+
"coder:templates.build": {
129+
ResourceTemplate.Type: {policy.ActionRead},
130+
ResourceFile.Type: {policy.ActionCreate,policy.ActionRead},
131+
"provisioner_jobs": {policy.ActionRead},
132+
},
133+
"coder:templates.author": {
134+
ResourceTemplate.Type: {policy.ActionRead,policy.ActionCreate,policy.ActionUpdate,policy.ActionDelete,policy.ActionViewInsights},
135+
ResourceFile.Type: {policy.ActionCreate,policy.ActionRead},
136+
},
137+
"coder:apikeys.manage_self": {
138+
ResourceApiKey.Type: {policy.ActionRead,policy.ActionCreate,policy.ActionUpdate,policy.ActionDelete},
139+
},
140+
}
141+
142+
// CompositeSitePermissions returns the site-level Permission list for a coder:* scope.
143+
funcCompositeSitePermissions(nameScopeName) ([]Permission,bool) {
144+
perms,ok:=compositePerms[name]
145+
if!ok {
146+
returnnil,false
147+
}
148+
returnPermissions(perms),true
149+
}
150+
151+
// CompositeScopeNames lists all high-level coder:* names in sorted order.
152+
funcCompositeScopeNames() []string {
153+
out:=make([]string,0,len(compositePerms))
154+
fork:=rangecompositePerms {
155+
out=append(out,string(k))
156+
}
157+
sort.Strings(out)
158+
returnout
159+
}
160+
110161
typeExpandableScopeinterface {
111162
Expand() (Scope,error)
112163
// Name is for logging and tracing purposes, we want to know the human
@@ -162,6 +213,19 @@ func ExpandScope(scope ScopeName) (Scope, error) {
162213
ifrole,ok:=builtinScopes[scope];ok {
163214
returnrole,nil
164215
}
216+
ifsite,ok:=CompositeSitePermissions(scope);ok {
217+
returnScope{
218+
Role:Role{
219+
Identifier:RoleIdentifier{Name:fmt.Sprintf("Scope_%s",scope)},
220+
DisplayName:string(scope),
221+
Site:site,
222+
Org:map[string][]Permission{},
223+
User: []Permission{},
224+
},
225+
// Composites are site-level; allow-list empty by default
226+
AllowIDList: []AllowListElement{},
227+
},nil
228+
}
165229
ifres,act,ok:=parseLowLevelScope(scope);ok {
166230
returnexpandLowLevel(res,act),nil
167231
}

‎coderd/rbac/scopes_catalog.go‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ var externalLowLevel = map[ScopeName]struct{}{
5252
"user_secret:*": {},
5353
}
5454

55+
// Public composite coder:* scopes exposed to users.
56+
varexternalComposite=map[ScopeName]struct{}{
57+
"coder:workspaces.create": {},
58+
"coder:workspaces.operate": {},
59+
"coder:workspaces.delete": {},
60+
"coder:workspaces.access": {},
61+
"coder:templates.build": {},
62+
"coder:templates.author": {},
63+
"coder:apikeys.manage_self": {},
64+
}
65+
5566
// IsExternalScope returns true if the scope is public, including the
5667
// `all` and `application_connect` special scopes and the curated
5768
// low-level resource:action scopes.
@@ -64,15 +75,18 @@ func IsExternalScope(name ScopeName) bool {
6475
if_,ok:=externalLowLevel[name];ok {
6576
returntrue
6677
}
78+
if_,ok:=externalComposite[name];ok {
79+
returntrue
80+
}
6781

6882
returnfalse
6983
}
7084

71-
// ExternalScopeNames returns a sorted list of all public scopes, which includes
72-
// the `all` and `application_connect` special scopes and thecurated public
73-
// low-level names.
85+
// ExternalScopeNames returns a sorted list of all public scopes, which
86+
//includesthe `all` and `application_connect` special scopes,curated
87+
// low-levelresource:actionnames, and curated composite coder:* scopes.
7488
funcExternalScopeNames() []string {
75-
names:=make([]string,0,len(externalLowLevel)+2)
89+
names:=make([]string,0,len(externalLowLevel)+len(externalComposite)+2)
7690
names=append(names,string(ScopeAll))
7791
names=append(names,string(ScopeApplicationConnect))
7892

@@ -83,6 +97,11 @@ func ExternalScopeNames() []string {
8397
}
8498
}
8599

100+
// curated composite names
101+
forname:=rangeexternalComposite {
102+
names=append(names,string(name))
103+
}
104+
86105
sort.Slice(names,func(i,jint)bool {returnstrings.Compare(names[i],names[j])<0 })
87106
returnnames
88107
}

‎coderd/rbac/scopes_catalog_internal_test.go‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package rbac
22

33
import (
44
"sort"
5+
"strings"
56
"testing"
67

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

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

31+
// Composite coder:* scopes expand to one or more site permissions.
32+
ifstrings.HasPrefix(name,"coder:") {
33+
s,err:=ScopeName(name).Expand()
34+
require.NoErrorf(t,err,"catalog entry should expand: %s",name)
35+
require.NotEmpty(t,s.Site)
36+
require.Empty(t,s.Org)
37+
require.Empty(t,s.User)
38+
continue
39+
}
40+
41+
// Low-level scopes must parse to a single permission.
3042
res,act,ok:=parseLowLevelScope(ScopeName(name))
3143
require.Truef(t,ok,"catalog entry should parse: %s",name)
3244

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp