feat(16.3-01): account token API with OpenAPI and phrase keys

- Add list/create/revoke/scopes handlers, validation, and soft revoke
- Regenerate admin OpenAPI and TS types; inventory and conformance cases
- Add server-side api_tokens phrase keys (en/pl)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-11 01:51:11 +02:00
parent 183583e0e9
commit ffdb5d00ca
13 changed files with 1430 additions and 49 deletions

View File

@@ -162,14 +162,13 @@ func validateTokenScopeMessageKeys(reg *Registry, tr *phrasebook.Translator) err
return nil
}
for _, scope := range reg.tokenScopes.ordered {
// Framework scopes use backend::lang.api_tokens.* keys added with the
// account token API; only plugin scopes are checked at boot here.
if scope.PluginID == "" {
continue
who := scope.PluginID
if who == "" {
who = "framework"
}
for _, text := range []string{scope.Label, scope.Description} {
if strings.Contains(text, "::") && !tr.Has(text) {
return fmt.Errorf("cabana: token scope %s (%s) names missing phrase key %s", scope.Name, scope.PluginID, text)
return fmt.Errorf("cabana: token scope %s (%s) names missing phrase key %s", scope.Name, who, text)
}
}
}
@@ -327,59 +326,42 @@ func scopeCoversAll(limit *bouncer.ScopeLimit, cover []string) bool {
return false
}
// scopeOverlaps is the D-17 picker/issue rule (Task 3 grows callers).
func scopeOverlaps(cat *tokenScopeCatalog, owner *bouncer.Principal, name string) bool {
if cat == nil || owner == nil {
// scopeOverlaps is the D-17 picker/issue rule: a plugin scope is offered when
// some registered permission code has a full match in the scope and the owner
// holds that code. "*" is always offered. "admin:read" is offered when the
// owner is a superuser or holds at least one catalog permission.
func scopeOverlaps(reg *Registry, owner *bouncer.Principal, name string) bool {
if reg == nil || reg.tokenScopes == nil || owner == nil {
return false
}
if name == scopeAll {
return true
}
scope, ok := cat.byName[name]
scope, ok := reg.tokenScopes.byName[name]
if !ok {
return false
}
if scope.Full {
// admin:read: offered when superuser or holds at least one catalog code.
if owner.IsSuperuser {
return true
}
for code := range catPermissionCodes(cat) {
for code := range reg.permissions {
if ownerHas(owner, code) {
return true
}
}
// Fall through: also check against registry permissions via owner grants.
for code := range owner.PermissionGrants {
if owner.PermissionGrants[code] {
return false
}
for code := range reg.permissions {
for _, pattern := range scope.Permissions {
if scopeMatch(pattern, code) == matchFull && ownerHas(owner, code) {
return true
}
}
return owner.IsSuperuser
}
for _, c := range scope.Permissions {
if scopeMatch(c, c) == matchFull && ownerHas(owner, c) {
return true
}
// Some registered catalog code has a full match in the scope and ownerHas(c).
_ = c
}
return false
}
func catPermissionCodes(cat *tokenScopeCatalog) map[string]bool {
out := map[string]bool{}
if cat == nil {
return out
}
for _, scope := range cat.ordered {
for _, p := range scope.Permissions {
out[p] = true
}
}
return out
}
func methodFilterScopes(cat *tokenScopeCatalog, stored []string, method string) (limit *bouncer.ScopeLimit, known int, methodOK int) {
if cat == nil {
return nil, 0, 0