Repository navigation
fix(api): return session authz denials when no email token is present - #507
Conversation
approvePurchase, cancelPurchase, revokeViaEmailToken and approveRIExchange handed any session 403 to the email-token branch, even with no token. With no token that branch re-entered the session path, which re-resolves the caller via requireSession (context principal first), so the outer denial was discarded and the outcome rested on the inner path resolving the same principal and re-running the same checks. fallsThroughToToken and the RI exchange pre-check now fall through only when a token is present; otherwise the denial is returned. The token contact_email flow for a logged-in user without approve-*/cancel-* is unchanged. The tokenless revoke path now returns the session's 403 rather than a 401 "sign in" prompt to a signed-in user. Existing tests that only passed through the fall-through are updated: the cancel status-guard tests now grant cancel-any, and the approve CSRF test uses an approver that passes RBAC so it still reaches the CSRF guard. Closes #173
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (5)
Included review availability: This review used your included allowance. 1 included review remains after this review. Your included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour. 📝 WalkthroughWalkthroughPurchase and RI-exchange handlers now return session permission denials when no token is present. When a token is present, eligible denials can proceed to token handling. Tests cover these paths and related cancellation-status checks. ChangesAuthorization dispatch
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Bug fix Sequence Diagram(s)sequenceDiagram
participant Client
participant approvePurchase
participant authorizeSessionApprove
participant approveViaToken
Client->>approvePurchase: Send request with session and optional token
approvePurchase->>authorizeSessionApprove: Check session permissions
authorizeSessionApprove-->>approvePurchase: Return permission denial
alt Token absent
approvePurchase-->>Client: Return session denial
else Token present
approvePurchase->>approveViaToken: Check eligible token fallback
approveViaToken-->>Client: Return token-path result
end
Merge Risk: ⚪ Minimal · up to The supplied evidence identifies no outstanding issue that would prevent merging after normal checks. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
The vacuous-assertion guard in internal/mocks resolves a mock receiver's type from its declaration and cannot see through a multi-value helper return, so it flagged the ApproveAndExecute assertion as unreviewable. The helper now takes the mocks as parameters. Refs #173
What
fallsThroughToToken(approve, cancel, email revoke) and thesessionHasApproveRightpre-check inapproveRIExchangenow hand a session 403 to the email-token branch only when a token is present. Without a token the denial is returned.Why
Issue #173: with
token == ""a session denial fell through to the session path again (approvePurchaseViaSession(...)/cancelPurchaseViaSession(...)/approveRIExchangeViaSession(..., nil)). That path re-resolves the caller throughrequireSession, which reads the context principal first. So the outer denial was thrown away, and safety depended on both lookups resolving the same principal. The email revoke path returned a 401 "sign in" to a signed-in user instead of the 403.Unchanged: with a token, a logged-in user without approve-/cancel- still reaches the contact_email token flow (owner's design, PR #101). The account-scope 404 with a token is unchanged too, and so are CSRF and constraint denials, which were already terminal.
Tests
New
internal/api/session_denial_final_test.go. The bearer session is denied, CSRF is valid, and the request context carries a different, authorized principal. This is the divergence the issue describes, so the test checks "outer denies, so the mutation never runs" directly.ApproveAndExecute/CancelExecutionAtomic/TransitionRIExchangeStatuscall.On origin/main code (fix stashed), all four no-token tests fail:
Mutation check: reverting only the RI line (
case isPermissionDenied(err):) failsTestApproveRIExchange_SessionDenialWithoutTokenIsFinal.Four existing tests passed only because of the fall-through. Each is updated:
TestHandler_cancelPurchase_Session_RejectsTerminalStatusand..._RejectsEachNonCancelableStatusused a session with no cancel permission (their own comment says "admin session"). They reached the status guard only through the fall-through. They now grant cancel-any.TestApproveViaSession_RequiresCSRFused a principal that RBAC denied (the issue points to this one). It now usesgrantAdminPurchaserplus a non-zero cost, so it still reaches the CSRF guard.TestHandler_revokePurchase_SessionNoPermissionNoTokennow expects 403 instead of 401.Commands run, all with
GOTOOLCHAIN=go1.26.6 GOWORK=off GOFLAGS='-p=2 -count=1':go build ./...go vetandgo vet -tags integrationon internal/api, internal/purchase and internal/servergo teston internal/api, internal/purchase and internal/server: passgolangci-lint run --new-from-rev=origin/main ./internal/api/...: no issuesFrontend: the dashboard approve/cancel calls already got a 403 on denial. The SPA calls
/purchases/{id}/revoke, not the email revoke endpoint, so nothing in the frontend relied on the old 401.Deliberately left
authorizeSessionApproveRIExchange, sec(money): currency is dropped at every layer boundary, so USD-denominated caps are compared against non-USD amounts [platform part] #361 and the pre-auth status 409 ordering (cli#1749) are separate issues.Closes #173
Summary by CodeRabbit