Repository navigation
fix(api): enforce cancel permission constraints on session cancels - #975
Conversation
The session cancel path (POST /api/purchases/cancel/{id} and the session
branch of POST /api/purchases/revoke/{id}) checked only the cancel-any /
cancel-own verbs. The Constraints configured on the granting permission
(providers, services, regions, account ids) were never evaluated, so a
cancel-any grant limited to one provider could still cancel any execution.
authorizeSessionCancel now reuses requirePermissionConstraints and the strict
per-recommendation sets from the #404/#681 revoke fix
(revokeExecutionConstraintSets). A constrained cancel-any falls back to
cancel-own only on a 403, and cancel-own is bounded by its own Constraints.
A Constraints denial is wrapped with wrapConstraintDenied so it stays final
and does not fall through to the contact_email token branch. The token branch
is a separate capability and is not Constraints-checked.
Tests answer from the real matcher and cover every constraint dimension for
both verbs, strict unknown dimensions, multi-recommendation executions, the
fallback, the cancel and revoke-token routes, admin wildcards carrying
Constraints and user API key sessions. The planned-purchase DELETE route has
the same gap and follows in a stacked change.
Refs #678, #404, #681
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@coderabbitai review |
|
Warning Review limit reached
This review includes 4 billable files and costs up to $1.00.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Or wait 7 minutes for your next included review. View limit details
Comment |
|
Independent review verdict: CLEAN at 75b906f (for the paths this PR covers)Reviewer: fresh Claude agent (Sonnet, high effort), not the author, cold brief, acting as a security reviewer. The PR body was treated as a claim. Coordinator: claude-cc-reviewer. Blocking findings in this diff: none. No bypass or fail-open was found in the changed paths. Merge-order condition: GitHub links this PR as closing #678, but one session path is still unconstrained here. Entry points:
Non-blocking notes:
What was attacked and how (local macOS, unit tests with mocks):
Gate: labels are mirrored from #678. CI is still finishing at this SHA. #975 merges together with #978 as described above. |
Summary
Part of #678. Covers session cancels and the session branch of the email-token revoke route. Planned-purchase DELETE follows in a stacked PR, which closes #678.
authorizeSessionCancel(internal/api/handler_purchases.go) gatedcancel-anyandcancel-ownwith bareHasPermissionAPIand never evaluated the granting permission's Constraints (Providers/Services/Regions/AccountIDs). So a session whose cancel permission is scoped to one provider could cancel executions for any provider. This is the same SEC-01 fail-open class #404 fixed for revoke (e5baaab, #681).Changes:
authorizeSessionCanceland a newauthorizeCancelOwncallrequirePermissionConstraintswithrevokeExecutionConstraintSets(exec), the strict per-recommendation sets fix(api): enforce permission Constraints on purchase revoke #681 already builds from the stored execution, never from the request. Existing helpers are reused, not copied.wrapConstraintDenied. A constrained user who is also a contact approver therefore cannot fall through to the email-token branch and bypass the scope, the same rule approve uses.cancelPurchase(session mode);cancelPurchaseViaSession;tryRevokeViaSession).No migration. No change to concealment (404 vs 403) for callers without the verb.
Tests
New
internal/api/handler_purchases_cancel_constraints_test.go:{admin,*}and a user-API-key session;CancelExecution.On unfixed code, 32 subtests failed (every deny case); the allow controls passed.
Mutation probes, all killed:
Verification (local, macOS)
go build ./...andgo vet ./...are clean.go test -race -count=1 ./internal/api/... ./internal/auth/... ./internal/server/...passes.Review
plans/678-cancel-constraints.md): independent adversarial review, two passes plus an F7 revision.Related follow-ups: #961 (planned DELETE update-any branch), #956 (admin key constraints on non-strict checks).
🤖 Generated with Claude Code