- Notifications
You must be signed in to change notification settings - Fork1k
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
base:main
Are you sure you want to change the base?
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
) | ||
@@ -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 | ||
Emyrk marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
} | ||
// 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) | ||
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) | ||
@@ -115,7 +123,7 @@ func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error) | ||
HashedSecret: hashed[:], | ||
LoginType: params.LoginType, | ||
Scopes: scopes, | ||
AllowList:params.AllowList, | ||
TokenName: params.TokenName, | ||
}, token, nil | ||
} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -145,24 +145,30 @@ func (s APIKeyScope) ToRBAC() rbac.ScopeName { | ||
} | ||
} | ||
// 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 | ||
// 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) | ||
} | ||
// 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 | ||
ThomasK33 marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
// 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.Scope{}, xerrors.New("no scopes provided") | ||
} | ||
var merged rbac.Scope | ||
@@ -174,9 +180,8 @@ func (s APIKeyScopes) Expand() (rbac.Scope, error) { | ||
User: nil, | ||
} | ||
// 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...) | ||
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) | ||
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, "+") + "]"} | ||
ThomasK33 marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
} | ||
// 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()) | ||
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.