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

Commit6258186

Browse files
committed
feat: add curated public API key scope catalog
Add public low-level scope catalog to RBAC system with curated set ofuser-requestable scopes. Includes workspace, template, API key, file,personal user, and user secret scopes. Updates scope checkingdocumentation to reference new catalog location in rbac package.
1 parenta78790c commit6258186

File tree

5 files changed

+250
-4
lines changed

5 files changed

+250
-4
lines changed

‎coderd/rbac/scopes.go‎

Lines changed: 71 additions & 2 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"
@@ -61,8 +62,8 @@ func WorkspaceAgentScope(params WorkspaceAgentScopeParams) Scope {
6162
}
6263

6364
const (
64-
ScopeAllScopeName="all"
65-
ScopeApplicationConnectScopeName="application_connect"
65+
ScopeAllScopeName="coder:all"
66+
ScopeApplicationConnectScopeName="coder:application_connect"
6667
ScopeNoUserDataScopeName="no_user_data"
6768
)
6869

@@ -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
}
@@ -205,6 +269,11 @@ func parseLowLevelScope(name ScopeName) (resource string, action policy.Action,
205269
if!exists {
206270
return"","",false
207271
}
272+
273+
ifact==policy.WildcardSymbol {
274+
returnres,policy.WildcardSymbol,true
275+
}
276+
208277
if_,exists:=def.Actions[policy.Action(act)];!exists {
209278
return"","",false
210279
}

‎coderd/rbac/scopes_catalog.go‎

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package rbac
2+
3+
import (
4+
"sort"
5+
"strings"
6+
)
7+
8+
// externalLowLevel is the curated set of low-level scope names exposed to users.
9+
// Any valid resource:action pair not in this set is considered internal-only
10+
// and must not be user-requestable.
11+
varexternalLowLevel=map[ScopeName]struct{}{
12+
// Workspaces
13+
"workspace:read": {},
14+
"workspace:create": {},
15+
"workspace:update": {},
16+
"workspace:delete": {},
17+
"workspace:ssh": {},
18+
"workspace:start": {},
19+
"workspace:stop": {},
20+
"workspace:application_connect": {},
21+
"workspace:*": {},
22+
23+
// Templates
24+
"template:read": {},
25+
"template:create": {},
26+
"template:update": {},
27+
"template:delete": {},
28+
"template:use": {},
29+
"template:*": {},
30+
31+
// API keys (self-management)
32+
"api_key:read": {},
33+
"api_key:create": {},
34+
"api_key:update": {},
35+
"api_key:delete": {},
36+
"api_key:*": {},
37+
38+
// Files
39+
"file:read": {},
40+
"file:create": {},
41+
"file:*": {},
42+
43+
// Users (personal profile only)
44+
"user:read_personal": {},
45+
"user:update_personal": {},
46+
47+
// User secrets
48+
"user_secret:read": {},
49+
"user_secret:create": {},
50+
"user_secret:update": {},
51+
"user_secret:delete": {},
52+
"user_secret:*": {},
53+
}
54+
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+
66+
// IsExternalScope returns true if the scope is public, including the
67+
// `all` and `application_connect` special scopes and the curated
68+
// low-level resource:action scopes.
69+
funcIsExternalScope(nameScopeName)bool {
70+
switchname {
71+
// Include `all` and `application_connect` for backward compatibility.
72+
case"all",ScopeAll,"application_connect",ScopeApplicationConnect:
73+
returntrue
74+
}
75+
if_,ok:=externalLowLevel[name];ok {
76+
returntrue
77+
}
78+
if_,ok:=externalComposite[name];ok {
79+
returntrue
80+
}
81+
82+
returnfalse
83+
}
84+
85+
// ExternalScopeNames returns a sorted list of all public scopes, which
86+
// includes the `all` and `application_connect` special scopes, curated
87+
// low-level resource:action names, and curated composite coder:* scopes.
88+
funcExternalScopeNames() []string {
89+
names:=make([]string,0,len(externalLowLevel)+len(externalComposite)+2)
90+
names=append(names,string(ScopeAll))
91+
names=append(names,string(ScopeApplicationConnect))
92+
93+
// curated low-level names, filtered for validity
94+
forname:=rangeexternalLowLevel {
95+
if_,_,ok:=parseLowLevelScope(name);ok {
96+
names=append(names,string(name))
97+
}
98+
}
99+
100+
// curated composite names
101+
forname:=rangeexternalComposite {
102+
names=append(names,string(name))
103+
}
104+
105+
sort.Slice(names,func(i,jint)bool {returnstrings.Compare(names[i],names[j])<0 })
106+
returnnames
107+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package rbac
2+
3+
import (
4+
"sort"
5+
"strings"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
funcTestExternalScopeNames(t*testing.T) {
12+
t.Parallel()
13+
14+
names:=ExternalScopeNames()
15+
require.NotEmpty(t,names)
16+
17+
// Ensure sorted ascending
18+
sorted:=append([]string(nil),names...)
19+
sort.Strings(sorted)
20+
require.Equal(t,sorted,names)
21+
22+
// Ensure each entry expands to site-only
23+
for_,name:=rangenames {
24+
// Skip `all` and `application_connect` since they do not
25+
// expand into a low level scope.
26+
// They are handled differently.
27+
ifname==string(ScopeAll)||name==string(ScopeApplicationConnect) {
28+
continue
29+
}
30+
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.
42+
res,act,ok:=parseLowLevelScope(ScopeName(name))
43+
require.Truef(t,ok,"catalog entry should parse: %s",name)
44+
45+
s,err:=ScopeName(name).Expand()
46+
require.NoErrorf(t,err,"catalog entry should expand: %s",name)
47+
require.Len(t,s.Site,1)
48+
require.Equal(t,res,s.Site[0].ResourceType)
49+
require.Equal(t,act,s.Site[0].Action)
50+
require.Empty(t,s.Org)
51+
require.Empty(t,s.User)
52+
}
53+
}
54+
55+
funcTestIsExternalScope(t*testing.T) {
56+
t.Parallel()
57+
58+
require.True(t,IsExternalScope("workspace:read"))
59+
require.True(t,IsExternalScope("template:use"))
60+
require.True(t,IsExternalScope("workspace:*"))
61+
require.False(t,IsExternalScope("debug_info:read"))// internal-only
62+
require.False(t,IsExternalScope("unknown:read"))
63+
}

‎scripts/check-scopes/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ When the tool reports missing values:
4040
make -B gen/db&& make lint/check-scopes
4141
```
4242

43-
3. Decide whether each new scope is public (exposed in the catalog) or internal-only (handled by the catalog task).
43+
3. Decide whether each new scope is public (exposed in the catalog) or internal-only.
44+
- If public, add it to the curated map in`coderd/rbac/scopes_catalog.go` (`externalLowLevel`) so it appears in the public catalog and can be requested by users.

‎scripts/check-scopes/main.go‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"golang.org/x/xerrors"
1313

14+
"github.com/coder/coder/v2/coderd/rbac"
1415
"github.com/coder/coder/v2/coderd/rbac/policy"
1516
)
1617

@@ -53,13 +54,14 @@ func main() {
5354
_,_=fmt.Fprintf(os.Stderr," ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS '%s';\n",m)
5455
}
5556
_,_=fmt.Fprintln(os.Stderr)
56-
_,_=fmt.Fprintln(os.Stderr,"Also decide if each new scope ispublic (exposed in thecatalog) or internal-only (catalog task).")
57+
_,_=fmt.Fprintln(os.Stderr,"Also decide if each new scope isexternal (exposed in the`externalLowLevel` in coderd/rbac/scopes_catalog.go) or internal-only.")
5758
os.Exit(1)
5859
}
5960

6061
// expectedFromRBAC returns the set of <resource>:<action> pairs derived from RBACPermissions.
6162
funcexpectedFromRBAC()map[string]struct{} {
6263
want:=make(map[string]struct{})
64+
// Low-level <resource>:<action>
6365
forresource,def:=rangepolicy.RBACPermissions {
6466
ifresource==policy.WildcardSymbol {
6567
// Ignore wildcard entry; it has no concrete <resource>:<action> pairs.
@@ -70,6 +72,10 @@ func expectedFromRBAC() map[string]struct{} {
7072
want[key]=struct{}{}
7173
}
7274
}
75+
// Composite coder:* names
76+
for_,n:=rangerbac.CompositeScopeNames() {
77+
want[n]=struct{}{}
78+
}
7379
returnwant
7480
}
7581

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp