feat(saas): scoped API keys with read, execute, lifecycle, and admin grants#824
feat(saas): scoped API keys with read, execute, lifecycle, and admin grants#824stubbi wants to merge 2 commits into
Conversation
…grants Hosted API keys were org-wide all-or-nothing: a key that can list could also create, fork, and terminate. This adds mint-time SCOPES so a team can embed a browser-safe or CI-safe key whose blast radius is smaller than the org. Scopes: read (list/get/status), execute (exec/files/run_code inside an existing sandbox), lifecycle (create/fork/terminate, pause/resume), admin (org management: keys, billing). The gateway authz path maps each op to its required scope and denies a key that lacks it with an LLM-legible forbidden (403) naming the exact scope and reporting op plus required_scope in context; an unmapped op fails closed to lifecycle. A one-way implication graph keeps the surface walkable: every resource scope implies read (no dead end), execute and lifecycle are orthogonal, admin is orthogonal to the resource scopes, and the legacy sandboxes scope still satisfies read, execute, and lifecycle. Backward compatibility is mandatory and enforced: a key stored with NO scopes is a legacy full-access key that satisfies every scope, and a key minted without scopes defaults to the full set, so no current caller loses access. An unknown scope is refused at mint rather than silently minting a key that grants nothing. The per-resource qualifier (a scope narrowed to a named sandbox or pool) is a documented follow-up, not this change. Since this moves the security surface, docs/threat-model.md, docs/saas/accounts-gateway.md, and docs/api/v2-spec.md are updated in the same change. Closes #784 Signed-off-by: Jannes Stubbemann <jannes@openclaw.rocks>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds scoped API key vocabulary and implication rules, default mint-time scope handling, and gateway authorization enforcement with scope-specific denials. The CLI default scope set, tests, and documentation are updated to match the new behavior. ChangesScoped API Keys
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant Gateway
participant KeyModel
participant ControlPlane
Client->>Gateway: request with op
Gateway->>Gateway: requiredScopeFor(op)
Gateway->>KeyModel: HasScope(requiredScope)
KeyModel-->>Gateway: satisfied / ErrKeyScope
alt scope satisfied
Gateway->>ControlPlane: forward request
ControlPlane-->>Client: response
else scope denied
Gateway->>Gateway: failVerify(op, requiredScope)
Gateway-->>Client: 403 forbidden with required_scope
end
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/saas/keys.go`:
- Around line 163-177: The scope validation in create key uses knownScopes, but
the error message hardcodes the “valid” list and can drift from the accepted
vocabulary. Update the validation branch in internal/saas/keys.go so the error
text is derived from knownScopes (including the legacy sandboxes scope) rather
than a literal list, keeping the message aligned with the actual accepted
scopes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: d109b0c4-f08b-43d7-a654-33b567cbd1df
📒 Files selected for processing (10)
docs/api/v2-spec.mddocs/saas/accounts-gateway.mddocs/threat-model.mdinternal/agentcli/auth.gointernal/saas/gateway.gointernal/saas/gateway_livefork_test.gointernal/saas/gateway_scopes_test.gointernal/saas/keys.gointernal/saas/model.gointernal/saas/scopes_test.go
…event drift Address CodeRabbit review on #824: the unknown-scope error string hardcoded the valid scope list, which could drift from the accepted vocabulary. Build it from FullScopes() instead. The legacy sandboxes alias stays accepted but is deliberately not advertised: new keys should carry the finer scopes. Signed-off-by: Jannes Stubbemann <jannes@openclaw.rocks>
|
CI note for the reviewer/merger: go-test needed retries because |
|
Full security review done (token/attenuation code, so extra rigor). The scope split, the single-funnel enforcement, the implication graph, and the hygiene are good work, but two findings block merge as-is:
Also worth fixing in the same pass: the console key form still offers only the old Holding this one. Per CLAUDE.md this is also a named-human-reviewer path. |
Thinking Path
Linked Issues or Issue Description
Closes #784
What Changed
ScopeExecute,ScopeLifecycle,ScopeAdminalongside the existingreadand legacysandboxesscopes; a one-way implication graph (scopeSatisfies) where every resource scope impliesread, execute and lifecycle are orthogonal, admin is orthogonal, and legacysandboxessatisfies read/execute/lifecycle.HasScopenow treats a scopeless key as full-access. AddedFullScopes()and a closedknownScopesvocabulary.CreateKeydefaults an empty scope request to the full set and rejects an unknown scope with the newErrUnknownScope.requiredScopeFormaps read ops toread, runtime (exec/files/run_code) toexecute, and every mutating verb tolifecycle, failing closed tolifecyclefor unmapped ops. A scope denial is now an actionable 403 naming the required scope and carryingopplusrequired_scopein context.auth keys create --scopeshelp now names the scope vocabulary; default isread,execute,lifecycle.Verification
go test ./internal/saas/... ./cmd/gateway/... ./internal/agentcli/... ./cmd/mitos/...(all pass)gofmt -lclean on changed filesgolangci-lint run ./internal/saas/... ./cmd/gateway/... ./internal/agentcli/... --timeout=5mclean (both default andGOOS=linux)TestLegacyScopelessKeyHasFullAccessandTestGatewayLegacyScopelessKeyRetainsFullAccess(a scopeless key reaches every op);TestCreateKeyDefaultsToFullScopes(empty mint = full)Risks
Low risk. The change is additive and backward compatible by construction: existing keys carry the legacy
sandboxesscope (which still satisfies read/execute/lifecycle) or no scopes (treated as full access), so no current caller loses access. The persisted store shape is unchanged (scopes are already aTEXT[]column in pgstore and a slice in the mem store), so there is no migration. The only behavior change for a NEW key is that naming a narrower scope now restricts it, which is the intended feature.Model Used
Claude Opus 4.8 (1M context), model id claude-opus-4-8, extended thinking enabled.
Checklist
#NNN/ mitos-run/mitos URLs)Signed-off-bytrailer (git commit -s)Summary by CodeRabbit
read,execute,lifecycle, andadmin.