Skip to content

Commit 5dd3400

Browse files
committed
feat: add API key allow list handling
Expose allow_list targets on CreateTokenRequest and persist them in the database so API keys can be scoped to resources. Introduce codersdk and rbac helpers to parse, validate, and normalize allow lists to enforce consistent wildcard handling. Regenerate OpenAPI documentation, API typing outputs, and TypeScript bindings with stable serialization ordering for generated files.
1 parent bf0f8e8 commit 5dd3400

23 files changed

Lines changed: 809 additions & 48 deletions

File tree

coderd/apidoc/docs.go

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing 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 about customizing 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+
if len(createToken.AllowList) > 0 {
120+
rbacAllowListElements := make([]rbac.AllowListElement, 0, len(createToken.AllowList))
121+
for _, t := range createToken.AllowList {
122+
entry, err := rbac.NewAllowListElement(string(t.Type), t.ID)
123+
if err != 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, 128)
134+
if err != 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 := range rbacAllowList {
144+
dbAllowList = append(dbAllowList, database.AllowListTarget{Type: e.Type, ID: e.ID})
145+
}
146+
147+
params.AllowList = dbAllowList
148+
}
149+
119150
if createToken.Lifetime != 0 {
120151
err := api.validateAPIKeyLifetime(ctx, user.ID, createToken.Lifetime)
121152
if err != 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
TokenName string
3637
RemoteAddr string
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+
if len(params.AllowList) == 0 {
69+
params.AllowList = database.AllowList{database.AllowListTarget{Type: policy.WildcardSymbol, ID: policy.WildcardSymbol}}
70+
}
71+
6472
ip := net.ParseIP(params.RemoteAddr)
6573
if ip == 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
if testCase.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
for i := range outputs {
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-
if diff := 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, b any) bool {
241244
var ab, 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
return ab.String() < bb.String()
249252
}),
250-
); diff != "" {
253+
)
254+
if diff := 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 := range munge {

0 commit comments

Comments
 (0)