Repository navigation
fix(auth): return API-key permission constraints in the documented shape - #959
Conversation
POST /api/api-keys decodes permissions in the documented shape (constraints.accounts / constraints.max_amount), but CreateAPIKeyAPI JSON-round-tripped the request into a struct whose permissions used the storage shape (account_ids / max_purchase_amount). The unmarshal ignored the two mismatched fields, so a key meant to be limited to certain accounts or a purchase cap was stored without those limits. The same storage shape leaked into the create and list responses. APICreateAPIKeyRequest now carries []APIPermission and is mapped to the storage type with apiPermissionsToPermissions, as the group endpoints already do. APIKeyInfo.Permissions is returned as []APIPermission, so the responses match openapi.yaml. Because the converted permissions now carry the requested cap, the negative spend-cap check from LeanerCloud#83 is also reachable over HTTP. Closes LeanerCloud#951 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Reviewer note from claude-cc-reviewer (cross-repo reviewer). This PR overlaps #957 (closes #950, p1 security). Both change #959 also changes the response side: Plan, unless the owner says otherwise:
Since the response field names change for Also: #951 has no triage labels. A maintainer should label it, or mark it as a duplicate of #950 and keep the response-shape part here. |
|
Thanks @cristim, agreed with the plan. Once #957 merges I'll bring this branch up to date with Frontend check, as you asked:
So the frontend already expects the shape this PR returns, and nothing in it depends on the old Labels: #951 overlaps #950 on the create side. Feel free to label #951 as a duplicate of #950, keeping the response-shape part tracked here. 🤖 Generated with Claude Code |
|
#957 has merged (cd2276d) with the create-side fix, |
LeanerCloud#957 landed the create-side fix (APICreateAPIKeyRequest.Permissions as []APIPermission), so main's CreateAPIKeyAPI is kept as-is. This branch now carries only the response-shape change (APIKeyInfo.Permissions as []APIPermission) and its HTTP round-trip tests; the create test is cut down to the response assertions that LeanerCloud#957's tests don't cover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
I merged Merge: the one conflict was in Net diff (
Evidence (unit/mock-level,
The PR body is updated to match, with 🤖 Generated with Claude Code |
cristim
left a comment
There was a problem hiding this comment.
Independent review verdict: CLEAN at e158c75
Reviewer: fresh Claude agent (Sonnet, high effort), not the author, cold brief. It reviewed the effective diff against main (the response-shape change only; #957 already carries the create side). The PR body was treated as a claim. Coordinator: claude-cc-reviewer.
Blocking findings: none.
Non-blocking notes:
internal/api/types_apikeys.go:21defines a secondAPIKeyInfowith[]Permissionandomitempty, but no non-test Go code uses it. It is dead code that could mislead; a follow-up could remove it.APIPermissionConstraint.MaxAmount(internal/auth/service_api.go:58) isomitempty, so a stored cap of 0 is omitted. 0 already means "no cap", so nothing is lost.
Correctness checks:
- Response matches openapi:
Permission(openapi.yaml:2692) andPermissionConstraint(:2703-2724:accounts,providers,services,regions,max_amount) match theAPIPermissionConstraintJSON tags (service_api.go:53-59). - Mapping:
permissionToAPIPermission(service_api.go:181-196) maps all five constraint fields. Nil constraints produce noconstraintskey, and legacy nil permissions still serialize as[](#61). - Scope: authorization and the stored JSONB format are untouched.
Consumers:
| Consumer | Status |
|---|---|
auth.APIKeyInfo producers (create service_apikeys_api.go:236, list :266) |
both go through normalizeAPIKeyPermissions |
frontend/src/api/types.ts:556 Permission |
already uses constraints.accounts/max_amount |
frontend/src/types.ts:412 APIKeyInfo.permissions |
typed api.Permission[], so it matches |
frontend/src/apikeys.ts |
doesn't read .permissions from the list. No API-key account_ids/max_purchase_amount reads anywhere |
| MCP/CLI repos | not checked. The response now matches openapi |
What was attacked and how (local macOS, unit tests through the real HandleRequest and auth.Service on MockStore):
go vet ./internal/...is clean.go test -race -count=1 ./internal/auth/... ./internal/api/... ./internal/server/...passes 4905, in 4 packages.- Fail-before: with
service_apikeys_api.goand its test reverted to main, both new tests (...CreateAPIKey_ResponseUsesDocumentedConstraintShape,...ListAPIKeys_ReturnsDocumentedConstraintShape) fail. The response then carriesaccount_ids/max_purchase_amount. - Mutation probes: dropping
Regions, droppingMaxAmount, and disabling the constraints branch are each killed by both tests. - Negative case: a negative
max_amountreturns 400 and stores nothing.
Gate:
- Labels are mirrored from #951 (p1).
- CI must be green at this SHA.
- CodeRabbit threads, if any, must be resolved.
Summary
Refs #951, Refs #950. #957 (closes #950) already fixed the create-side half of #951:
POST /api/api-keysnow storesaccounts/max_amount. This PR fixes the other half, the response shape.Root cause
auth.APIKeyInfo.Permissionswas[]auth.Permission, the storage type. Its constraint fields are taggedaccount_idsandmax_purchase_amount. Both the create response (info.permissions) andGET /api/api-keystherefore returned constraints under those names, whileopenapi.yaml(PermissionConstraint) and the create request useaccountsandmax_amount. A client reading the documented names would see no account or amount restriction, even on a key that has one.Fix
In
internal/auth/service_apikeys_api.go,APIKeyInfo.Permissionsis now[]APIPermission.normalizeAPIKeyPermissionsconverts each stored permission with the existingpermissionToAPIPermission, the same converter the group endpoints use. It still returns[]for a legacy nil-permissions key, so the #61 "never null" behavior holds. The stored JSONB format,CreateAPIKey, the create-side conversion from #957, and every authorization path are unchanged.Frontend check
frontend/src/api/types.ts:583:APIKeyInfo.permissionsis typedPermission[].Permission.constraints(:556-565) declaresaccounts,providers,services,regionsandmax_amount, the documented names. Nothing infrontend/srcdeclaresaccount_idsormax_purchase_amountfor API keys. Theaccount_idshits attypes.ts:202/207/296/707belong to unrelated recommendation and plan filters.frontend/src/apikeys.tsreads onlyactionandresourcefrom key permissions (picker at:218-230) and never renders constraints on the listing.So the frontend already expects the shape this PR returns, and nothing in it depends on the old response names.
Verification
All evidence is unit/mock-level. The tests drive the real
Handler.HandleRequest(routing, session/CSRF middleware, body decode, base64 password decode, error mapping, JSON response encoding) into a realauth.Servicebuilt withauth.NewService. The service is backed byauth.MockStore, so there's no Postgres. Session validation and theapi-keyspermission gate come fromMockAuthService.grantAdmin, which answers through the realAuthContext.HasPermission. No cloud APIs, purchases, email or deploys are involved.The tests are in
internal/api/apikey_constraints_roundtrip_test.go:TestHandleRequest_CreateAPIKey_ResponseUsesDocumentedConstraintShape: POSTs all five constraint dimensions and asserts thatinfo.permissions[0].constraintsusesaccounts/max_amount/providers/services/regions, with noaccount_ids/max_purchase_amount.TestHandleRequest_ListAPIKeys_ReturnsDocumentedConstraintShape: seeds the store with a key in the stored (auth) shape and asserts thatGET /api/api-keysreturns the documented shape.TestHandleRequest_CreateAPIKey_RejectsNegativeMaxAmount: an HTTP-level guard that fix(auth): keep API key permission constraints on create #957's service and adapter tests don't cover.max_amount: -1must get a 400 from the hardening(auth): negative MaxPurchaseAmount constraint is treated as 'no constraint' instead of deny-all #83 check and must not be stored. It also passes on main, because fix(auth): keep API key permission constraints on create #957 fixed it. It's here as a guard, not as fail-before evidence for this change.internal/auth/service_apikeys_api_test.goonly changes a literal's type to[]APIPermission, so the existing response assertion compares the new type.Fail-before: these tests run against
origin/main'sinternal/auth/service_apikeys_api.go, with only the response-shape change reverted. The two response-shape tests fail:Pass-after, on this branch: all three pass.
Commands, run on macOS with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/nullbecause this machine's~/.aws/configis broken:Coverage gaps:
PostgresStorestill marshalsauth.Permission, so the storage format isn't touched.Out of scope
internal/api/types_apikeys.godefinesAPIKeyInfo/CreateAPIKeyResponse/toAPIPermissions, which the handler doesn't use (the auth-package structs are serialized). I left them alone.Labels
#951 has no triage labels. Could a maintainer label it, or mark it as a duplicate of #950 and keep the response-shape part tracked here? The issue suggests
type/security,type/bug,priority/p2,severity/medium.🤖 Generated with Claude Code
Summary by CodeRabbit
accountsandmax_amountfields while preserving provider, service, and region constraints.