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

feat: add allow_list to resource-scoped API tokens#19964

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
ThomasK33 wants to merge1 commit intomain
base:main
Choose a base branch
Loading
fromthomask33/09-25-feat_add_allow_list_field_api_keys
Open
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletionscoderd/apidoc/docs.go
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

17 changes: 17 additions & 0 deletionscoderd/apidoc/swagger.json
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

31 changes: 31 additions & 0 deletionscoderd/apikey.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,37 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
TokenName: tokenName,
}

if len(createToken.AllowList) > 0 {
rbacAllowListElements := make([]rbac.AllowListElement, 0, len(createToken.AllowList))
for _, t := range createToken.AllowList {
entry, err := rbac.NewAllowListElement(string(t.Type), t.ID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Failed to create API key.",
Detail: err.Error(),
})
return
}
rbacAllowListElements = append(rbacAllowListElements, entry)
}

rbacAllowList, err := rbac.NormalizeAllowList(rbacAllowListElements)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Failed to create API key.",
Detail: err.Error(),
})
return
}

dbAllowList := make(database.AllowList, 0, len(rbacAllowList))
for _, e := range rbacAllowList {
dbAllowList = append(dbAllowList, rbac.AllowListElement{Type: e.Type, ID: e.ID})
}

params.AllowList = dbAllowList
}

if createToken.Lifetime != 0 {
err := api.validateAPIKeyLifetime(ctx, user.ID, createToken.Lifetime)
if err != nil {
Expand Down
10 changes: 9 additions & 1 deletioncoderd/apikey/apikey.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ import (

"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/cryptorand"
)

Expand All@@ -34,6 +35,9 @@ type CreateParams struct {
Scopes database.APIKeyScopes
TokenName string
RemoteAddr string
// AllowList is an optional, normalized allow-list
// of resource type and uuid entries. If empty, defaults to wildcard.
AllowList database.AllowList
}

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

if len(params.AllowList) == 0 {
params.AllowList = database.AllowList{{Type: policy.WildcardSymbol, ID: policy.WildcardSymbol}}
}

ip := net.ParseIP(params.RemoteAddr)
if ip == nil {
ip = net.IPv4(0, 0, 0, 0)
Expand DownExpand Up@@ -115,7 +123,7 @@ func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error)
HashedSecret: hashed[:],
LoginType: params.LoginType,
Scopes: scopes,
AllowList:database.AllowList{database.AllowListWildcard()},
AllowList:params.AllowList,
TokenName: params.TokenName,
}, token, nil
}
Expand Down
2 changes: 1 addition & 1 deletioncoderd/coderdtest/authorize.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ func AssertRBAC(t *testing.T, api *coderd.API, client *codersdk.Client) RBACAsse
ID: key.UserID.String(),
Roles: rbac.RoleIdentifiers(roleNames),
Groups: roles.Groups,
Scope: key.Scopes,
Scope: key.ScopeSet(),
},
Recorder: recorder,
}
Expand Down
14 changes: 9 additions & 5 deletionscoderd/database/dbauthz/setup_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,17 +225,20 @@ func (s *MethodTestSuite) SubtestWithDB(db database.Store, testCaseF func(db dat
if testCase.outputs != nil {
// Assert the required outputs
s.Equal(len(testCase.outputs), len(outputs), "method %q returned unexpected number of outputs", methodName)
cmpOptions := []cmp.Option{
// Equate nil and empty slices.
cmpopts.EquateEmpty(),
}
for i := range outputs {
a, b := testCase.outputs[i].Interface(), outputs[i].Interface()

// To avoid the extra small overhead of gob encoding, we can
// first check if the values are equal with regard to order.
// If not, re-check disregarding order and show a nice diff
// output of the two values.
if !cmp.Equal(a, b, cmpopts.EquateEmpty()) {
if diff := cmp.Diff(a, b,
// Equate nil and empty slices.
cmpopts.EquateEmpty(),
if !cmp.Equal(a, b, cmpOptions...) {
diffOpts := append(
append([]cmp.Option{}, cmpOptions...),
// Allow slice order to be ignored.
cmpopts.SortSlices(func(a, b any) bool {
var ab, bb strings.Builder
Expand All@@ -247,7 +250,8 @@ func (s *MethodTestSuite) SubtestWithDB(db database.Store, testCaseF func(db dat
// https://github.com/google/go-cmp/issues/67
return ab.String() < bb.String()
}),
); diff != "" {
)
if diff := cmp.Diff(a, b, diffOpts...); diff != "" {
s.Failf("compare outputs failed", "method %q returned unexpected output %d (-want +got):\n%s", methodName, i, diff)
}
}
Expand Down
3 changes: 2 additions & 1 deletioncoderd/database/dbgen/dbgen.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,7 @@ import (
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
"github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/provisionerd/proto"
Expand DownExpand Up@@ -186,7 +187,7 @@ func APIKey(t testing.TB, db database.Store, seed database.APIKey, munge ...func
UpdatedAt: takeFirst(seed.UpdatedAt, dbtime.Now()),
LoginType: takeFirst(seed.LoginType, database.LoginTypePassword),
Scopes: takeFirstSlice([]database.APIKeyScope(seed.Scopes), []database.APIKeyScope{database.ApiKeyScopeCoderAll}),
AllowList: takeFirstSlice(seed.AllowList, database.AllowList{database.AllowListWildcard()}),
AllowList: takeFirstSlice(seed.AllowList, database.AllowList{{Type: policy.WildcardSymbol, ID: policy.WildcardSymbol}}),
TokenName: takeFirst(seed.TokenName),
}
for _, fn := range munge {
Expand Down
80 changes: 52 additions & 28 deletionscoderd/database/modelmethods.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -145,24 +145,30 @@ func (s APIKeyScope) ToRBAC() rbac.ScopeName {
}
}

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

var _ rbac.ExpandableScope = APIKeyScopes{}
// WithAllowList wraps the scopes with a database allow list, producing an
// ExpandableScope that always enforces the allow list overlay when expanded.
func (s APIKeyScopes) WithAllowList(list AllowList) APIKeyScopeSet {
return APIKeyScopeSet{Scopes: s, AllowList: list}
}

// Has returns true if the slice contains the provided scope.
func (s APIKeyScopes) Has(target APIKeyScope) bool {
return slices.Contains(s, target)
}

// Expand merges the permissions of all scopes in the list into a single scope.
// If the list is empty, it defaults to rbac.ScopeAll.
func (s APIKeyScopes) Expand() (rbac.Scope, error) {
// expandRBACScope merges the permissions of all scopes in the list into a
// single RBAC scope. If the list is empty, it defaults to rbac.ScopeAll for
// backward compatibility. This method is internal; use ScopeSet() to combine
// scopes with the API key's allow list for authorization.
func (s APIKeyScopes) expandRBACScope() (rbac.Scope, error) {
// Default to ScopeAll for backward compatibility when no scopes provided.
if len(s) == 0 {
return rbac.ScopeAll.Expand()
return rbac.Scope{}, xerrors.New("no scopes provided")
}

var merged rbac.Scope
Expand All@@ -174,9 +180,8 @@ func (s APIKeyScopes) Expand() (rbac.Scope, error) {
User: nil,
}

// Track allow list union, collapsing to wildcard if any child is wildcard.
allowAll := false
allowSet := make(map[string]rbac.AllowListElement)
// Collect allow lists for a union after expanding all scopes.
allowLists := make([][]rbac.AllowListElement, 0, len(s))

for _, s := range s {
expanded, err := s.ToRBAC().Expand()
Expand All@@ -191,16 +196,7 @@ func (s APIKeyScopes) Expand() (rbac.Scope, error) {
}
merged.User = append(merged.User, expanded.User...)

// Merge allow lists.
for _, e := range expanded.AllowIDList {
if e.ID == policy.WildcardSymbol && e.Type == policy.WildcardSymbol {
allowAll = true
// No need to track other entries once wildcard is present.
continue
}
key := e.String()
allowSet[key] = e
}
allowLists = append(allowLists, expanded.AllowIDList)
}

// De-duplicate permissions across Site/Org/User
Expand All@@ -210,14 +206,11 @@ func (s APIKeyScopes) Expand() (rbac.Scope, error) {
}
merged.User = rbac.DeduplicatePermissions(merged.User)

if allowAll || len(allowSet) == 0 {
merged.AllowIDList = []rbac.AllowListElement{rbac.AllowListAll()}
} else {
merged.AllowIDList = make([]rbac.AllowListElement, 0, len(allowSet))
for _, v := range allowSet {
merged.AllowIDList = append(merged.AllowIDList, v)
}
union, err := rbac.UnionAllowLists(allowLists...)
if err != nil {
return rbac.Scope{}, err
}
merged.AllowIDList = union

return merged, nil
}
Expand All@@ -235,6 +228,37 @@ func (s APIKeyScopes) Name() rbac.RoleIdentifier {
return rbac.RoleIdentifier{Name: "scopes[" + strings.Join(names, "+") + "]"}
}

// APIKeyScopeSet merges expanded scopes with the API key's DB allow_list. If
// the DB allow_list is a wildcard or empty, the merged scope's allow list is
// unchanged. Otherwise, the DB allow_list overrides the merged AllowIDList to
// enforce the token's resource scoping consistently across all permissions.
type APIKeyScopeSet struct {
Scopes APIKeyScopes
AllowList AllowList
}

var _ rbac.ExpandableScope = APIKeyScopeSet{}

func (s APIKeyScopeSet) Name() rbac.RoleIdentifier { return s.Scopes.Name() }

func (s APIKeyScopeSet) Expand() (rbac.Scope, error) {
merged, err := s.Scopes.expandRBACScope()
if err != nil {
return rbac.Scope{}, err
}
merged.AllowIDList = rbac.IntersectAllowLists(merged.AllowIDList, s.AllowList)
return merged, nil
}

// ScopeSet returns the scopes combined with the database allow list. It is the
// canonical way to expose an API key's effective scope for authorization.
func (k APIKey) ScopeSet() APIKeyScopeSet {
return APIKeyScopeSet{
Scopes: k.Scopes,
AllowList: k.AllowList,
}
}

func (k APIKey) RBACObject() rbac.Object {
return rbac.ResourceApiKey.WithIDString(k.ID).
WithOwner(k.UserID.String())
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp