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

Commit1fea0d8

Browse files
committed
refactor: add allow_list field to API keys for resource scoping
- Add allow_list field to CreateTokenRequest API and database schema- Implement APIKeyEffectiveScope that merges scopes with token allow_list- Create x/wildcard package for type-safe wildcard values- Add rbac.ParseAllowList for validating and normalizing allow lists- Support resource targeting like "workspace:*" or "template:<uuid>"- Default to wildcard (*:*) for backward compatibility
1 parentc84abde commit1fea0d8

File tree

26 files changed

+836
-40
lines changed

26 files changed

+836
-40
lines changed

‎.swaggo‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,11 @@ replace time.Duration int64
66
replace github.com/coder/coder/v2/codersdk.ProvisionerType string
77
// Do not render netip.Addr
88
replace netip.Addr string
9+
10+
// Map generics of wildcard.Value[T] to concrete, client-friendly types.
11+
12+
// Type: wildcard.Value[codersdk.RBACResource] -> codersdk.RBACResource (already includes "*")
13+
replace github.com/coder/coder/v2/x/wildcard.$wildcard.Value-codersdk_RBACResource github.com/coder/coder/v2/codersdk.RBACResource
14+
15+
// ID: wildcard.Value[uuid.UUID] -> string (we’ll add a doc note to allow "*")
16+
replace github.com/coder/coder/v2/x/wildcard.$wildcard.Value-uuid_UUID string

‎coderd/apidoc/docs.go‎

Lines changed: 17 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: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/apikey.go‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,45 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
116116
TokenName:tokenName,
117117
}
118118

119+
iflen(createToken.AllowList)>0 {
120+
rbacAllowListElements:=make([]rbac.AllowListElement,0,len(createToken.AllowList))
121+
for_,t:=rangecreateToken.AllowList {
122+
entry,err:=rbac.NewAllowListElement(t.Type.String(),t.ID.String())
123+
iferr!=nil {
124+
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
125+
Message:"Failed to create API key.",
126+
Detail:err.Error(),
127+
})
128+
return
129+
}
130+
rbacAllowListElements=append(rbacAllowListElements,entry)
131+
}
132+
133+
rbacAllowList,err:=rbac.NewAllowList(rbacAllowListElements,128)
134+
iferr!=nil {
135+
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
136+
Message:"Failed to create API key.",
137+
Detail:err.Error(),
138+
})
139+
return
140+
}
141+
142+
dbAllowList:=make(database.AllowList,0,len(rbacAllowList))
143+
for_,e:=rangerbacAllowList {
144+
target,err:=database.NewAllowListTarget(e.Type,e.ID)
145+
iferr!=nil {
146+
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
147+
Message:"Failed to create API key.",
148+
Detail:err.Error(),
149+
})
150+
return
151+
}
152+
dbAllowList=append(dbAllowList,target)
153+
}
154+
155+
params.AllowList=dbAllowList
156+
}
157+
119158
ifcreateToken.Lifetime!=0 {
120159
err:=api.validateAPIKeyLifetime(ctx,user.ID,createToken.Lifetime)
121160
iferr!=nil {

‎coderd/apikey/apikey.go‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ type CreateParams struct {
3434
Scopes database.APIKeyScopes
3535
TokenNamestring
3636
RemoteAddrstring
37+
// AllowList is an optional, normalized allow-list
38+
// of resource type and uuid entries. If empty, defaults to wildcard.
39+
AllowList database.AllowList
3740
}
3841

3942
// Generate generates an API key, returning the key as a string as well as the
@@ -115,7 +118,7 @@ func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error)
115118
HashedSecret:hashed[:],
116119
LoginType:params.LoginType,
117120
Scopes:scopes,
118-
AllowList:database.AllowList{database.AllowListWildcard()},
121+
AllowList:params.AllowList,
119122
TokenName:params.TokenName,
120123
},token,nil
121124
}

‎coderd/database/dbauthz/setup_test.go‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/coder/coder/v2/coderd/rbac/policy"
3232
"github.com/coder/coder/v2/coderd/rbac/regosql"
3333
"github.com/coder/coder/v2/coderd/util/slice"
34+
"github.com/coder/coder/v2/x/wildcard"
3435
)
3536

3637
varerrMatchAny=xerrors.New("match any error")
@@ -225,17 +226,21 @@ func (s *MethodTestSuite) SubtestWithDB(db database.Store, testCaseF func(db dat
225226
iftestCase.outputs!=nil {
226227
// Assert the required outputs
227228
s.Equal(len(testCase.outputs),len(outputs),"method %q returned unexpected number of outputs",methodName)
229+
cmpOptions:= []cmp.Option{
230+
// Equate nil and empty slices.
231+
cmpopts.EquateEmpty(),
232+
cmpopts.EquateComparable(wildcard.Value[string]{}, wildcard.Value[uuid.UUID]{}),
233+
}
228234
fori:=rangeoutputs {
229235
a,b:=testCase.outputs[i].Interface(),outputs[i].Interface()
230236

231237
// To avoid the extra small overhead of gob encoding, we can
232238
// first check if the values are equal with regard to order.
233239
// If not, re-check disregarding order and show a nice diff
234240
// output of the two values.
235-
if!cmp.Equal(a,b,cmpopts.EquateEmpty()) {
236-
ifdiff:=cmp.Diff(a,b,
237-
// Equate nil and empty slices.
238-
cmpopts.EquateEmpty(),
241+
if!cmp.Equal(a,b,cmpOptions...) {
242+
diffOpts:=append(
243+
append([]cmp.Option{},cmpOptions...),
239244
// Allow slice order to be ignored.
240245
cmpopts.SortSlices(func(a,bany)bool {
241246
varab,bb strings.Builder
@@ -247,7 +252,8 @@ func (s *MethodTestSuite) SubtestWithDB(db database.Store, testCaseF func(db dat
247252
// https://github.com/google/go-cmp/issues/67
248253
returnab.String()<bb.String()
249254
}),
250-
);diff!="" {
255+
)
256+
ifdiff:=cmp.Diff(a,b,diffOpts...);diff!="" {
251257
s.Failf("compare outputs failed","method %q returned unexpected output %d (-want +got):\n%s",methodName,i,diff)
252258
}
253259
}

‎coderd/database/dbgen/dbgen.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ func APIKey(t testing.TB, db database.Store, seed database.APIKey, munge ...func
186186
UpdatedAt:takeFirst(seed.UpdatedAt,dbtime.Now()),
187187
LoginType:takeFirst(seed.LoginType,database.LoginTypePassword),
188188
Scopes:takeFirstSlice([]database.APIKeyScope(seed.Scopes), []database.APIKeyScope{database.ApiKeyScopeCoderAll}),
189-
AllowList:takeFirstSlice(seed.AllowList, database.AllowList{database.AllowListWildcard()}),
189+
AllowList:takeFirstSlice(seed.AllowList, database.AllowList{}),
190190
TokenName:takeFirst(seed.TokenName),
191191
}
192192
for_,fn:=rangemunge {

‎coderd/database/modelmethods.go‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,41 @@ func (s APIKeyScopes) Name() rbac.RoleIdentifier {
251251
return rbac.RoleIdentifier{Name:"scopes["+strings.Join(names,"+")+"]"}
252252
}
253253

254+
// APIKeyEffectiveScope merges expanded scopes with the API key's DB allow_list.
255+
// If the DB allow_list is a wildcard or empty, the merged scope's allow list is unchanged.
256+
// Otherwise, the DB allow_list overrides the merged AllowIDList to enforce the token's
257+
// resource scoping consistently across all permissions.
258+
typeAPIKeyEffectiveScopestruct {
259+
ScopesAPIKeyScopes
260+
AllowListAllowList
261+
}
262+
263+
func (eAPIKeyEffectiveScope)Name() rbac.RoleIdentifier {returne.Scopes.Name() }
264+
265+
func (eAPIKeyEffectiveScope)Expand() (rbac.Scope,error) {
266+
merged,err:=e.Scopes.Expand()
267+
iferr!=nil {
268+
return rbac.Scope{},err
269+
}
270+
iflen(e.AllowList)==0 {
271+
returnmerged,nil
272+
}
273+
274+
// If allow list contains a single wildcard (*:*), keep merged allow list as-is
275+
for_,entry:=rangee.AllowList {
276+
ifentry.Type.IsAny()&&entry.ID.IsAny() {
277+
returnmerged,nil
278+
}
279+
}
280+
281+
out:=make([]rbac.AllowListElement,0,len(e.AllowList))
282+
for_,t:=rangee.AllowList {
283+
out=append(out, rbac.AllowListElement{Type:t.Type.String(),ID:t.ID.String()})
284+
}
285+
merged.AllowIDList=out
286+
returnmerged,nil
287+
}
288+
254289
func (kAPIKey)RBACObject() rbac.Object {
255290
returnrbac.ResourceApiKey.WithIDString(k.ID).
256291
WithOwner(k.UserID.String())

‎coderd/database/types.go‎

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"github.com/coder/coder/v2/coderd/rbac"
1717
"github.com/coder/coder/v2/coderd/rbac/policy"
18+
"github.com/coder/coder/v2/x/wildcard"
1819
)
1920

2021
// AuditOAuthConvertState is never stored in the database. It is stored in a cookie
@@ -163,9 +164,7 @@ func (m StringMapOfInt) Value() (driver.Value, error) {
163164

164165
typeCustomRolePermissions []CustomRolePermission
165166

166-
// APIKeyScopes implements sql.Scanner and driver.Valuer so it can be read from
167-
// and written to the Postgres api_key_scope[] enum array column.
168-
func (s*APIKeyScopes)Scan(srcinterface{})error {
167+
func (s*APIKeyScopes)Scan(srcany)error {
169168
vararr []string
170169
iferr:=pq.Array(&arr).Scan(src);err!=nil {
171170
returnerr
@@ -318,26 +317,43 @@ func ParseIP(ipStr string) pqtype.Inet {
318317
// It encodes a resource tuple (type, id) and provides helpers for
319318
// consistent string and JSON representations across the codebase.
320319
typeAllowListTargetstruct {
321-
Typestring`json:"type"`
322-
IDstring`json:"id"`
320+
Typewildcard.Value[string]`json:"type"`
321+
IDwildcard.Value[uuid.UUID]`json:"id"`
323322
}
324323

325324
// String returns the canonical database representation "type:id".
326-
func (tAllowListTarget)String()string {
327-
returnt.Type+":"+t.ID
325+
func (tAllowListTarget)String()string {returnt.Type.String()+":"+t.ID.String() }
326+
327+
funcNewAllowListTarget(typstring,idstring) (AllowListTarget,error) {
328+
var (
329+
anyType wildcard.Value[string]
330+
anyID wildcard.Value[uuid.UUID]
331+
)
332+
333+
iftyp!=policy.WildcardSymbol {
334+
anyType=wildcard.Of(typ)
335+
}
336+
337+
ifid!=policy.WildcardSymbol {
338+
u,err:=uuid.Parse(id)
339+
iferr!=nil {
340+
returnAllowListTarget{},xerrors.Errorf("invalid %s ID: %q",typ,id)
341+
}
342+
anyID=wildcard.Of(u)
343+
}
344+
345+
returnAllowListTarget{Type:anyType,ID:anyID},nil
328346
}
329347

330348
// ParseAllowListTarget parses the canonical string form "type:id".
331349
funcParseAllowListTarget(sstring) (AllowListTarget,error) {
332-
targetType,id,ok:=rbac.ParseResourceAction(s)
350+
targetType,rawID,ok:=rbac.ParseResourceAction(s)
333351
if!ok {
334352
returnAllowListTarget{},xerrors.Errorf("invalid allow list target: %q",s)
335353
}
336-
returnAllowListTarget{Type:targetType,ID:id},nil
337-
}
338354

339-
// AllowListWildcard returns the wildcard allow-list entry {"*","*"}.
340-
funcAllowListWildcard()AllowListTarget {returnAllowListTarget{Type:"*",ID:"*"}}
355+
returnNewAllowListTarget(targetType,rawID)
356+
}
341357

342358
// AllowList is a typed wrapper around a list of AllowListTarget entries.
343359
// It implements sql.Scanner and driver.Valuer so it can be stored in and

‎coderd/httpmw/apikey.go‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,10 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon
434434
// If the key is valid, we also fetch the user roles and status.
435435
// The roles are used for RBAC authorize checks, and the status
436436
// is to block 'suspended' users from accessing the platform.
437-
actor,userStatus,err:=UserRBACSubject(ctx,cfg.DB,key.UserID,key.Scopes)
437+
actor,userStatus,err:=UserRBACSubject(ctx,cfg.DB,key.UserID, database.APIKeyEffectiveScope{
438+
Scopes:key.Scopes,
439+
AllowList:key.AllowList,
440+
})
438441
iferr!=nil {
439442
returnwrite(http.StatusUnauthorized, codersdk.Response{
440443
Message:internalErrorMessage,

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp