Repository navigation
fix(api/purchases): drop removed session.Role shortcut in execute-direct gate (closes #940) - #941
Conversation
…ect gate (closes #940) PR #912 (#907) removed the Role field from api.Session, leaving authorizeSessionExecuteDirect with a dead `if session.Role == "admin"` reference that prevented the base branch from compiling. Remove the three-line shortcut and update the gate-logic doc comment to match the sibling authorizeSessionApprove/authorizeSessionCancel pattern: admins pass via the execute-any HasPermissionAPI check because the Administrators group carries the {admin,*} wildcard permission. Also fix all test Session struct literals that still set Role (now an unknown field), update the admin-role-shortcut test to properly stub the HasPermissionAPI path, and add three regression tests: - AdminGroupViaExecuteAny: confirms admin users still pass via execute-any - NoGrant: confirms non-admin without execute-any/own gets 403 - NilAuth: confirms nil auth component returns 500 (fail-closed)
📝 WalkthroughWalkthroughThis PR refactors purchase direct-execution authorization by replacing session-role-based admin shortcuts with an API key sentinel check ( ChangesAdmin Authorization Bypass Refactor
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
internal/api/handler_purchases_test.go (1)
3219-3274: ⚡ Quick winAdd a regression test for the
apiKeyAdminUserIDbypass.These additions cover the execute-any path, but not the actual stateless admin API-key short-circuit introduced in
authorizeSessionExecuteDirect. A future regression could break that sentinel branch while this suite still passes throughHasPermissionAPI("execute-any").Proposed test
+func TestHandler_authorizeSessionExecuteDirect_AdminAPIKeySentinel(t *testing.T) { + ctx := context.Background() + session := &Session{UserID: apiKeyAdminUserID} + + handler := &Handler{auth: new(MockAuthService)} + err := handler.authorizeSessionExecuteDirect(ctx, session, "") + + require.NoError(t, err) +}🤖 Prompt for 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. In `@internal/api/handler_purchases_test.go` around lines 3219 - 3274, Add a regression test that verifies the stateless admin API-key short-circuit: create a session whose UserID is the apiKeyAdminUserID constant, construct a Handler with auth == nil, call authorizeSessionExecuteDirect(ctx, session, anyCreatorID) and assert no error (i.e. admin API key bypasses HasPermissionAPI); reference the apiKeyAdminUserID, Handler, Session, and authorizeSessionExecuteDirect symbols when adding this test (mirror style of existing TestHandler_authorizeSessionExecuteDirect_* tests).
🤖 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.
Nitpick comments:
In `@internal/api/handler_purchases_test.go`:
- Around line 3219-3274: Add a regression test that verifies the stateless admin
API-key short-circuit: create a session whose UserID is the apiKeyAdminUserID
constant, construct a Handler with auth == nil, call
authorizeSessionExecuteDirect(ctx, session, anyCreatorID) and assert no error
(i.e. admin API key bypasses HasPermissionAPI); reference the apiKeyAdminUserID,
Handler, Session, and authorizeSessionExecuteDirect symbols when adding this
test (mirror style of existing TestHandler_authorizeSessionExecuteDirect_*
tests).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 99841b6f-71ec-4b92-91a5-ad16b1c18e30
📒 Files selected for processing (2)
internal/api/handler_purchases.gointernal/api/handler_purchases_test.go
…000059 (base collision from #803+#924) The base feat/multicloud-web-frontend merged two 000058 migrations from the recent wave, which the check-migration-conflicts pre-commit hook rejects: - 000058_purchase_executions_direct_execute_audit (#803, schema change) - 000058_seed_purchaser_group (#924, data seed) Keep the schema-change audit migration at 000058 and renumber the seed migration to the next free slot, 000059 (base jumps 000058 -> 000063, so 000059-000062 are open). golang-migrate discovers migrations by filename glob, so no embed list needs updating; only two self-references were adjusted: - auth/types.go GroupPurchaser doc comment pointing at the filename - the self-referencing error message inside the up.sql Safe to renumber without applied-state reconciliation: the base has not compiled since the #803/#907 break, so neither 000058 migration has been applied to any database yet.
|
Pushed a second base-regression fix to this PR (commit d7a65fa). The Renumbered the seed migration to Safe to renumber without applied-state reconciliation: the base hasn't compiled since the #803/#907 break, so neither @coderabbitai review |
|
🧠 Learnings used✅ Action performedReview finished.
|
PR #941 merged 000059_seed_purchaser_group onto the base, colliding with this PR's 000059_purchase_history_revocation. Renumber to the next free slot vs the updated base (000060; base highest is 000063). Update the header comments in the up/down SQL and the migration-number reference in store_postgres.go to match. No schema change.
PR #941 merged 000059_seed_purchaser_group onto the base, colliding with this PR's 000059_purchase_history_revocation. Renumber to the next free slot vs the updated base (000060; base highest is 000063). Update the header comments in the up/down SQL and the migration-number reference in store_postgres.go to match. No schema change.
Base feat/multicloud-web-frontend now carries 000059_seed_purchaser_group (merged via PR #941) and 000063_purchase_history_monthly_cost_nullable, so the revocation migration must move above the highest base migration to keep the sequence monotonic and avoid collisions on the merge ref. Move both the up and down files to 000064 and update the in-code reference comment in store_postgres.queryPurchaseHistory.
PR #941 merged 000059_seed_purchaser_group onto the base, colliding with this PR's 000059_purchase_history_revocation. Renumber to the next free slot vs the updated base (000060; base highest is 000063). Update the header comments in the up/down SQL and the migration-number reference in store_postgres.go to match. No schema change.
Base feat/multicloud-web-frontend now carries 000059_seed_purchaser_group (merged via PR #941) and 000063_purchase_history_monthly_cost_nullable, so the revocation migration must move above the highest base migration to keep the sequence monotonic and avoid collisions on the merge ref. Move both the up and down files to 000064 and update the in-code reference comment in store_postgres.queryPurchaseHistory.
PR #941 merged 000059_seed_purchaser_group onto the base, colliding with this PR's 000059_purchase_history_revocation. Renumber to the next free slot vs the updated base (000060; base highest is 000063). Update the header comments in the up/down SQL and the migration-number reference in store_postgres.go to match. No schema change.
Base feat/multicloud-web-frontend now carries 000059_seed_purchaser_group (merged via PR #941) and 000063_purchase_history_monthly_cost_nullable, so the revocation migration must move above the highest base migration to keep the sequence monotonic and avoid collisions on the merge ref. Move both the up and down files to 000064 and update the in-code reference comment in store_postgres.queryPurchaseHistory.
Summary
This PR restores
feat/multicloud-web-frontendto a buildable + pre-commit-clean state by fixing two independent base regressions from the recent merge wave.Fix 1 — base does not compile (
session.Roleundefined)Rolefromapi.Session(type Session struct { UserID string; Email string }), leavingauthorizeSessionExecuteDirectwith a deadif session.Role == "admin" { return nil }reference.internal/api/handler_purchases.go:520:13: session.Role undefined-- the base branch did not compile.authorizeSessionApprove/authorizeSessionCancel: admin users pass via the execute-anyHasPermissionAPIcheck because the Administrators group carries the{admin, *}wildcard permission (exactly like the sibling gates).Role: "admin"/"user"fields from four existingSessionstruct literals in tests (now unknown fields caught bygo vet), and update the test that assumed the admin-role short-circuit to properly stub theHasPermissionAPIcall path.Fix 2 — duplicate migration number 000058 (
check-migration-conflictshook failing)The base merged two
000058migrations from the recent wave, which thecheck-migration-conflictspre-commit hook rejects:000058_purchase_executions_direct_execute_audit.{up,down}.sql(feat(api,recs): execute-any/own permission gate on direct execute (closes #289) #803, schema change)000058_seed_purchaser_group.{up,down}.sql(feat(auth): add Purchaser group + carve execute/approve-any/retry-any out of admin wildcard (closes #923) #924, data seed)Fix: keep the schema-change audit migration at
000058and renumber the seed migration to the next free slot,000059(base jumps000058 -> 000063, so000059-000062are open).golang-migratediscovers migrations by filename glob, so no embed list needs updating; only two self-references were adjusted: theGroupPurchaserdoc comment ininternal/auth/types.goand the self-referencing error message string inside the up.sql.Renumber safety: deploys have been failing because the base did not compile, so neither
000058migration has been applied to any database yet. Renumbering before the next successful deploy is clean -- no applied-state reconciliation needed.Regression tests added
TestHandler_authorizeSessionExecuteDirect_AdminGroupViaExecuteAny{admin,*}wildcard is permitted viaHasPermissionAPI(ExecuteAny)-- no deadRoleshortcutTestHandler_authorizeSessionExecuteDirect_NoGrantTestHandler_authorizeSessionExecuteDirect_NilAuthnilauth component returns 500 (fail-closed perfeedback_fail_closed_middleware.md)Test plan
go build github.com/LeanerCloud/CUDly/internal/...succeedsgo vet ./internal/api/... ./internal/auth/...passesgo test ./internal/api/... -count=1-- 1413 passed, 0 failedpre-commit run check-migration-conflicts --all-files-- Passed (no dup)Summary by CodeRabbit
Bug Fixes
Tests