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

Commitd590c1d

Browse files
committed
feat: add API key allow list handling
Expose allow_list targets on CreateTokenRequest and persist them in thedatabase so API keys can be scoped to resources.Introduce codersdk and rbac helpers to parse, validate, and normalizeallow lists to enforce consistent wildcard handling.Regenerate OpenAPI documentation, API typing outputs, and TypeScriptbindings with stable serialization ordering for generated files.
1 parentb60ae0a commitd590c1d

File tree

24 files changed

+818
-73
lines changed

24 files changed

+818
-73
lines changed

‎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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,37 @@ 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(string(t.Type),t.ID)
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.NormalizeAllowList(rbacAllowListElements)
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+
dbAllowList=append(dbAllowList, rbac.AllowListElement{Type:e.Type,ID:e.ID})
145+
}
146+
147+
params.AllowList=dbAllowList
148+
}
149+
119150
ifcreateToken.Lifetime!=0 {
120151
err:=api.validateAPIKeyLifetime(ctx,user.ID,createToken.Lifetime)
121152
iferr!=nil {

‎coderd/apikey/apikey.go‎

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

1313
"github.com/coder/coder/v2/coderd/database"
1414
"github.com/coder/coder/v2/coderd/database/dbtime"
15+
"github.com/coder/coder/v2/coderd/rbac/policy"
1516
"github.com/coder/coder/v2/cryptorand"
1617
)
1718

@@ -34,6 +35,9 @@ type CreateParams struct {
3435
Scopes database.APIKeyScopes
3536
TokenNamestring
3637
RemoteAddrstring
38+
// AllowList is an optional, normalized allow-list
39+
// of resource type and uuid entries. If empty, defaults to wildcard.
40+
AllowList database.AllowList
3741
}
3842

3943
// Generate generates an API key, returning the key as a string as well as the
@@ -61,6 +65,10 @@ func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error)
6165
params.LifetimeSeconds=int64(time.Until(params.ExpiresAt).Seconds())
6266
}
6367

68+
iflen(params.AllowList)==0 {
69+
params.AllowList= database.AllowList{{Type:policy.WildcardSymbol,ID:policy.WildcardSymbol}}
70+
}
71+
6472
ip:=net.ParseIP(params.RemoteAddr)
6573
ifip==nil {
6674
ip=net.IPv4(0,0,0,0)
@@ -115,7 +123,7 @@ func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error)
115123
HashedSecret:hashed[:],
116124
LoginType:params.LoginType,
117125
Scopes:scopes,
118-
AllowList:database.AllowList{database.AllowListWildcard()},
126+
AllowList:params.AllowList,
119127
TokenName:params.TokenName,
120128
},token,nil
121129
}

‎coderd/coderdtest/authorize.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func AssertRBAC(t *testing.T, api *coderd.API, client *codersdk.Client) RBACAsse
6868
ID:key.UserID.String(),
6969
Roles:rbac.RoleIdentifiers(roleNames),
7070
Groups:roles.Groups,
71-
Scope:key.Scopes,
71+
Scope:key.ScopeSet(),
7272
},
7373
Recorder:recorder,
7474
}

‎coderd/database/dbauthz/setup_test.go‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,17 +225,20 @@ func (s *MethodTestSuite) SubtestWithDB(db database.Store, testCaseF func(db dat
225225
iftestCase.outputs!=nil {
226226
// Assert the required outputs
227227
s.Equal(len(testCase.outputs),len(outputs),"method %q returned unexpected number of outputs",methodName)
228+
cmpOptions:= []cmp.Option{
229+
// Equate nil and empty slices.
230+
cmpopts.EquateEmpty(),
231+
}
228232
fori:=rangeoutputs {
229233
a,b:=testCase.outputs[i].Interface(),outputs[i].Interface()
230234

231235
// To avoid the extra small overhead of gob encoding, we can
232236
// first check if the values are equal with regard to order.
233237
// If not, re-check disregarding order and show a nice diff
234238
// 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(),
239+
if!cmp.Equal(a,b,cmpOptions...) {
240+
diffOpts:=append(
241+
append([]cmp.Option{},cmpOptions...),
239242
// Allow slice order to be ignored.
240243
cmpopts.SortSlices(func(a,bany)bool {
241244
varab,bb strings.Builder
@@ -247,7 +250,8 @@ func (s *MethodTestSuite) SubtestWithDB(db database.Store, testCaseF func(db dat
247250
// https://github.com/google/go-cmp/issues/67
248251
returnab.String()<bb.String()
249252
}),
250-
);diff!="" {
253+
)
254+
ifdiff:=cmp.Diff(a,b,diffOpts...);diff!="" {
251255
s.Failf("compare outputs failed","method %q returned unexpected output %d (-want +got):\n%s",methodName,i,diff)
252256
}
253257
}

‎coderd/database/dbgen/dbgen.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
2828
"github.com/coder/coder/v2/coderd/database/pubsub"
2929
"github.com/coder/coder/v2/coderd/rbac"
30+
"github.com/coder/coder/v2/coderd/rbac/policy"
3031
"github.com/coder/coder/v2/codersdk"
3132
"github.com/coder/coder/v2/cryptorand"
3233
"github.com/coder/coder/v2/provisionerd/proto"
@@ -186,7 +187,7 @@ func APIKey(t testing.TB, db database.Store, seed database.APIKey, munge ...func
186187
UpdatedAt:takeFirst(seed.UpdatedAt,dbtime.Now()),
187188
LoginType:takeFirst(seed.LoginType,database.LoginTypePassword),
188189
Scopes:takeFirstSlice([]database.APIKeyScope(seed.Scopes), []database.APIKeyScope{database.ApiKeyScopeCoderAll}),
189-
AllowList:takeFirstSlice(seed.AllowList, database.AllowList{database.AllowListWildcard()}),
190+
AllowList:takeFirstSlice(seed.AllowList, database.AllowList{{Type:policy.WildcardSymbol,ID:policy.WildcardSymbol}}),
190191
TokenName:takeFirst(seed.TokenName),
191192
}
192193
for_,fn:=rangemunge {

‎coderd/database/modelmethods.go‎

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,24 +145,30 @@ func (s APIKeyScope) ToRBAC() rbac.ScopeName {
145145
}
146146
}
147147

148-
// APIKeyScopesallows expanding multipleAPI keyscopes into a single
149-
//RBAC scope for authorization. This implements rbac.ExpandableScope so
150-
//callers can pass the list directly without deriving a single scope.
148+
// APIKeyScopesrepresents a collection of individualAPI keyscope names as
149+
//stored in the database. Helper methods on this type are used to derive the
150+
//RBAC scope that should be authorized for the key.
151151
typeAPIKeyScopes []APIKeyScope
152152

153-
var_ rbac.ExpandableScope=APIKeyScopes{}
153+
// WithAllowList wraps the scopes with a database allow list, producing an
154+
// ExpandableScope that always enforces the allow list overlay when expanded.
155+
func (sAPIKeyScopes)WithAllowList(listAllowList)APIKeyScopeSet {
156+
returnAPIKeyScopeSet{Scopes:s,AllowList:list}
157+
}
154158

155159
// Has returns true if the slice contains the provided scope.
156160
func (sAPIKeyScopes)Has(targetAPIKeyScope)bool {
157161
returnslices.Contains(s,target)
158162
}
159163

160-
// Expand merges the permissions of all scopes in the list into a single scope.
161-
// If the list is empty, it defaults to rbac.ScopeAll.
162-
func (sAPIKeyScopes)Expand() (rbac.Scope,error) {
164+
// expandRBACScope merges the permissions of all scopes in the list into a
165+
// single RBAC scope. If the list is empty, it defaults to rbac.ScopeAll for
166+
// backward compatibility. This method is internal; use ScopeSet() to combine
167+
// scopes with the API key's allow list for authorization.
168+
func (sAPIKeyScopes)expandRBACScope() (rbac.Scope,error) {
163169
// Default to ScopeAll for backward compatibility when no scopes provided.
164170
iflen(s)==0 {
165-
returnrbac.ScopeAll.Expand()
171+
return rbac.Scope{},xerrors.New("no scopes provided")
166172
}
167173

168174
varmerged rbac.Scope
@@ -235,6 +241,37 @@ func (s APIKeyScopes) Name() rbac.RoleIdentifier {
235241
return rbac.RoleIdentifier{Name:"scopes["+strings.Join(names,"+")+"]"}
236242
}
237243

244+
// APIKeyScopeSet merges expanded scopes with the API key's DB allow_list. If
245+
// the DB allow_list is a wildcard or empty, the merged scope's allow list is
246+
// unchanged. Otherwise, the DB allow_list overrides the merged AllowIDList to
247+
// enforce the token's resource scoping consistently across all permissions.
248+
typeAPIKeyScopeSetstruct {
249+
ScopesAPIKeyScopes
250+
AllowListAllowList
251+
}
252+
253+
var_ rbac.ExpandableScope=APIKeyScopeSet{}
254+
255+
func (sAPIKeyScopeSet)Name() rbac.RoleIdentifier {returns.Scopes.Name() }
256+
257+
func (sAPIKeyScopeSet)Expand() (rbac.Scope,error) {
258+
merged,err:=s.Scopes.expandRBACScope()
259+
iferr!=nil {
260+
return rbac.Scope{},err
261+
}
262+
merged.AllowIDList=rbac.IntersectAllowLists(merged.AllowIDList,s.AllowList)
263+
returnmerged,nil
264+
}
265+
266+
// ScopeSet returns the scopes combined with the database allow list. It is the
267+
// canonical way to expose an API key's effective scope for authorization.
268+
func (kAPIKey)ScopeSet()APIKeyScopeSet {
269+
returnAPIKeyScopeSet{
270+
Scopes:k.Scopes,
271+
AllowList:k.AllowList,
272+
}
273+
}
274+
238275
func (kAPIKey)RBACObject() rbac.Object {
239276
returnrbac.ResourceApiKey.WithIDString(k.ID).
240277
WithOwner(k.UserID.String())

‎coderd/database/modelmethods_internal_test.go‎

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package database
33
import (
44
"testing"
55

6+
"github.com/google/uuid"
67
"github.com/stretchr/testify/require"
78

89
"github.com/coder/coder/v2/coderd/rbac"
@@ -38,7 +39,7 @@ func TestAPIKeyScopesExpand(t *testing.T) {
3839
for_,tc:=rangecases {
3940
t.Run(tc.name,func(t*testing.T) {
4041
t.Parallel()
41-
s,err:=tc.scopes.Expand()
42+
s,err:=tc.scopes.expandRBACScope()
4243
require.NoError(t,err)
4344
tc.want(t,s)
4445
})
@@ -59,7 +60,7 @@ func TestAPIKeyScopesExpand(t *testing.T) {
5960
for_,tc:=rangecases {
6061
t.Run(tc.name,func(t*testing.T) {
6162
t.Parallel()
62-
s,err:=tc.scopes.Expand()
63+
s,err:=tc.scopes.expandRBACScope()
6364
require.NoError(t,err)
6465
requirePermission(t,s,tc.res,tc.act)
6566
requireAllowAll(t,s)
@@ -70,21 +71,76 @@ func TestAPIKeyScopesExpand(t *testing.T) {
7071
t.Run("merge",func(t*testing.T) {
7172
t.Parallel()
7273
scopes:=APIKeyScopes{ApiKeyScopeCoderApplicationConnect,ApiKeyScopeCoderAll,ApiKeyScopeWorkspaceRead}
73-
s,err:=scopes.Expand()
74+
s,err:=scopes.expandRBACScope()
7475
require.NoError(t,err)
7576
requirePermission(t,s,rbac.ResourceWildcard.Type,policy.Action(policy.WildcardSymbol))
7677
requirePermission(t,s,rbac.ResourceWorkspace.Type,policy.ActionApplicationConnect)
7778
requirePermission(t,s,rbac.ResourceWorkspace.Type,policy.ActionRead)
7879
requireAllowAll(t,s)
7980
})
8081

81-
t.Run("empty_defaults_to_all",func(t*testing.T) {
82+
t.Run("effective_scope_keep_types",func(t*testing.T) {
8283
t.Parallel()
83-
s,err:= (APIKeyScopes{}).Expand()
84+
workspaceID:=uuid.New()
85+
86+
effective:=APIKeyScopeSet{
87+
Scopes:APIKeyScopes{ApiKeyScopeWorkspaceRead},
88+
AllowList:AllowList{
89+
{Type:rbac.ResourceWorkspace.Type,ID:workspaceID.String()},
90+
},
91+
}
92+
93+
expanded,err:=effective.Expand()
8494
require.NoError(t,err)
85-
requirePermission(t,s,rbac.ResourceWildcard.Type,policy.Action(policy.WildcardSymbol))
95+
require.Len(t,expanded.AllowIDList,1)
96+
require.Equal(t,"workspace",expanded.AllowIDList[0].Type)
97+
require.Equal(t,workspaceID.String(),expanded.AllowIDList[0].ID)
98+
})
99+
100+
t.Run("empty_rejected",func(t*testing.T) {
101+
t.Parallel()
102+
_,err:= (APIKeyScopes{}).expandRBACScope()
103+
require.Error(t,err)
104+
require.ErrorContains(t,err,"no scopes provided")
105+
})
106+
107+
t.Run("allow_list_overrides",func(t*testing.T) {
108+
t.Parallel()
109+
allowID:=uuid.NewString()
110+
set:=APIKeyScopes{ApiKeyScopeWorkspaceRead}.WithAllowList(AllowList{
111+
{Type:rbac.ResourceWorkspace.Type,ID:allowID},
112+
})
113+
s,err:=set.Expand()
114+
require.NoError(t,err)
115+
require.Len(t,s.AllowIDList,1)
116+
require.Equal(t, rbac.AllowListElement{Type:rbac.ResourceWorkspace.Type,ID:allowID},s.AllowIDList[0])
117+
})
118+
119+
t.Run("allow_list_wildcard_keeps_merged",func(t*testing.T) {
120+
t.Parallel()
121+
set:=APIKeyScopes{ApiKeyScopeWorkspaceRead}.WithAllowList(AllowList{
122+
{Type:policy.WildcardSymbol,ID:policy.WildcardSymbol},
123+
})
124+
s,err:=set.Expand()
125+
require.NoError(t,err)
126+
requirePermission(t,s,rbac.ResourceWorkspace.Type,policy.ActionRead)
86127
requireAllowAll(t,s)
87128
})
129+
130+
t.Run("scope_set_helper",func(t*testing.T) {
131+
t.Parallel()
132+
allowID:=uuid.NewString()
133+
key:=APIKey{
134+
Scopes:APIKeyScopes{ApiKeyScopeWorkspaceRead},
135+
AllowList:AllowList{
136+
{Type:rbac.ResourceWorkspace.Type,ID:allowID},
137+
},
138+
}
139+
s,err:=key.ScopeSet().Expand()
140+
require.NoError(t,err)
141+
require.Len(t,s.AllowIDList,1)
142+
require.Equal(t, rbac.AllowListElement{Type:rbac.ResourceWorkspace.Type,ID:allowID},s.AllowIDList[0])
143+
})
88144
}
89145

90146
// Helpers

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp