mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add allow_list to resource-scoped API tokens (#19964)
# Add API key allow_list for resource-scoped tokens This PR adds support for API key allow lists, enabling tokens to be scoped to specific resources. The implementation: 1. Adds a new `allow_list` field to the `CreateTokenRequest` struct, allowing clients to specify resource-specific scopes when creating API tokens 2. Implements `APIAllowListTarget` type to represent resource targets in the format `<type>:<id>` with support for wildcards 3. Adds validation and normalization logic for allow lists to handle wildcards and deduplication 4. Integrates with RBAC by creating an `APIKeyEffectiveScope` that merges API key scopes with allow list restrictions 5. Updates API documentation and TypeScript types to reflect the new functionality This feature enables creating tokens that are limited to specific resources (like workspaces or templates) by ID, making it possible to create more granular API tokens with limited access.
This commit is contained in:
@@ -145,24 +145,30 @@ func (s APIKeyScope) ToRBAC() rbac.ScopeName {
|
||||
}
|
||||
}
|
||||
|
||||
// APIKeyScopes allows expanding multiple API key scopes into a single
|
||||
// RBAC scope for authorization. This implements rbac.ExpandableScope so
|
||||
// callers can pass the list directly without deriving a single scope.
|
||||
// APIKeyScopes represents a collection of individual API key scope 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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user