feat(audit): stamp actor on execution + RI-exchange state transitions (closes #1009) - #1011
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR adds audit actor attribution to cloud commitment state transitions. Three store methods now accept an ChangesAudit actor stamping on state transitions
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
internal/config/store_postgres.go (2)
1753-1768:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRI exchange terminal transitions still leave stale transition metadata.
This stamps
pending -> processing/cancelled, butCompleteRIExchangeandFailRIExchangebelow still changestatuswithout refreshingtransitioned_by/transitioned_at, andinternal/api/handler_ri_exchange.gocalls both on the system-driven completion/failure paths (Lines 1137-1181). A completed or failed row will therefore keep the earlier approval stamp, or no stamp at all, instead of the actual terminal transition time. Those UPDATEs should null the actor and settransitioned_at = NOW()as well. Based on PR objectives, system-initiated transitions should still stamp transition time even when the actor is nil.🤖 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/config/store_postgres.go` around lines 1753 - 1768, CompleteRIExchange and FailRIExchange currently update status without touching transitioned_by/transitioned_at, leaving stale or missing transition stamps; update their UPDATE statements (the ones in PostgresStore methods CompleteRIExchange and FailRIExchange) to set transitioned_by = NULL (or the actor param when provided) and transitioned_at = NOW() and adjust query parameters accordingly so system-driven terminal transitions record transitioned_at even when actor is nil. Ensure the UPDATE mirrors the TransitionRIExchangeStatus behavior for stamping transition metadata while preserving existing status, error, completed_at, and other returned fields.
827-847:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift
CancelExecutionAtomicstill bypasses the new transition audit fields.
TransitionExecutionStatusnow stampstransitioned_by/transitioned_at, but the normal purchase-cancel flow ininternal/api/handler_purchases.go(Lines 761-770) still goes throughCancelExecutionAtomic, whose UPDATE only touchesstatus,cancelled_by, andupdated_at. That leaves history/email cancellations with NULL transition audit metadata, so execution cancellation is only fixed for the planned-purchase path. ExtendCancelExecutionAtomicwith the same nullable UUID actor stamp and thread it through its callers. Based on PR objectives, execution state transitions are supposed to be auditable end-to-end.🤖 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/config/store_postgres.go` around lines 827 - 847, CancelExecutionAtomic currently updates only status, cancelled_by and updated_at, leaving transitioned_by/transitioned_at NULL; change CancelExecutionAtomic signature to accept an actor *string (nullable UUID) and update its SQL to set transitioned_by = $X and transitioned_at = NOW() (mirroring TransitionExecutionStatus), return the same execution fields as queryExecutions if applicable, and thread the new actor parameter through all callers (including the normal purchase cancel flow in internal/api/handler_purchases.go) so the actor value is supplied when cancellations occur; ensure parameter ordering in the UPDATE call matches the new placeholder positions and adjust any invocation sites to compile.
🧹 Nitpick comments (1)
internal/api/handler_per_account_perms_test.go (1)
962-963: ⚡ Quick winAssert the actor value, not just its presence.
This is a human-initiated transition path, but the test currently accepts any actor value. Match
actortopermsScopedUserIDto actually verify propagation.Proposed test tightening
- mockStore.On("TransitionExecutionStatus", ctx, executionID, mock.Anything, "paused", mock.Anything). + mockStore.On("TransitionExecutionStatus", ctx, executionID, mock.Anything, "paused", + mock.MatchedBy(func(actor *string) bool { + return actor != nil && *actor == permsScopedUserID + }), + ). Return(transitoned, nil) @@ - mockStore.AssertCalled(t, "TransitionExecutionStatus", ctx, executionID, mock.Anything, "paused", mock.Anything) + mockStore.AssertCalled(t, "TransitionExecutionStatus", ctx, executionID, mock.Anything, "paused", + mock.MatchedBy(func(actor *string) bool { + return actor != nil && *actor == permsScopedUserID + }), + )Also applies to: 982-982
🤖 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_per_account_perms_test.go` around lines 962 - 963, The mock expectation for mockStore.TransitionExecutionStatus currently accepts any actor value; tighten it to assert the actor equals permsScopedUserID so the test verifies the human-initiated actor is propagated. Update the mock.On call for TransitionExecutionStatus (and the similar occurrence around the other expectation) to use mock.MatchedBy or the specific value permsScopedUserID instead of mock.Anything for the actor parameter, keeping ctx, executionID and the "paused" status the same and returning transitoned, nil.
🤖 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/api/coverage_extras_test.go`:
- Around line 405-423: The test
TestHandler_TransitionRegistrationStatus_ActorStamped is calling
mockStore.TransitionRegistrationStatus directly which bypasses the handler
logic; change the test to exercise the real approve/reject handler (the function
under test that calls TransitionRegistrationStatus) instead of the store method
so the actor-stamping path is validated end-to-end. Set up the same mock
expectation on mockStore.On("TransitionRegistrationStatus", ctx, reg, "pending",
mock.MatchedBy(...)) and then invoke the handler entrypoint (e.g., the HTTP
handler or the handler method that processes registration approvals/rejections)
with a request/context containing the reviewer actor ID (use
validUUIDPtrOrNil(reg.ReviewedBy) or inject the actor into context/header as the
handler expects), then assert no error and mockStore.AssertExpectations(t).
In `@internal/purchase/approvals.go`:
- Around line 147-149: The call to m.config.TransitionExecutionStatus currently
always passes nil for the transitionedBy parameter which clears actor
attribution; change the approval flow so that when the approval is
human/session-authenticated you pass the session user's UUID into
TransitionExecutionStatus (instead of nil) while keeping nil for
token/SQS/system paths—locate the session approval branch that calls
TransitionExecutionStatus (the same method referenced as
m.config.TransitionExecutionStatus with executionID and status args) and thread
the session user ID through to that function call so transitioned_by is
persisted for human-initiated approvals.
---
Outside diff comments:
In `@internal/config/store_postgres.go`:
- Around line 1753-1768: CompleteRIExchange and FailRIExchange currently update
status without touching transitioned_by/transitioned_at, leaving stale or
missing transition stamps; update their UPDATE statements (the ones in
PostgresStore methods CompleteRIExchange and FailRIExchange) to set
transitioned_by = NULL (or the actor param when provided) and transitioned_at =
NOW() and adjust query parameters accordingly so system-driven terminal
transitions record transitioned_at even when actor is nil. Ensure the UPDATE
mirrors the TransitionRIExchangeStatus behavior for stamping transition metadata
while preserving existing status, error, completed_at, and other returned
fields.
- Around line 827-847: CancelExecutionAtomic currently updates only status,
cancelled_by and updated_at, leaving transitioned_by/transitioned_at NULL;
change CancelExecutionAtomic signature to accept an actor *string (nullable
UUID) and update its SQL to set transitioned_by = $X and transitioned_at = NOW()
(mirroring TransitionExecutionStatus), return the same execution fields as
queryExecutions if applicable, and thread the new actor parameter through all
callers (including the normal purchase cancel flow in
internal/api/handler_purchases.go) so the actor value is supplied when
cancellations occur; ensure parameter ordering in the UPDATE call matches the
new placeholder positions and adjust any invocation sites to compile.
---
Nitpick comments:
In `@internal/api/handler_per_account_perms_test.go`:
- Around line 962-963: The mock expectation for
mockStore.TransitionExecutionStatus currently accepts any actor value; tighten
it to assert the actor equals permsScopedUserID so the test verifies the
human-initiated actor is propagated. Update the mock.On call for
TransitionExecutionStatus (and the similar occurrence around the other
expectation) to use mock.MatchedBy or the specific value permsScopedUserID
instead of mock.Anything for the actor parameter, keeping ctx, executionID and
the "paused" status the same and returning transitoned, nil.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0aa755e5-8068-4f4a-a053-3b18de5eb405
📒 Files selected for processing (33)
internal/analytics/collector_test.gointernal/api/coverage_extras_test.gointernal/api/handler_history.gointernal/api/handler_history_test.gointernal/api/handler_per_account_perms_test.gointernal/api/handler_purchases.gointernal/api/handler_purchases_test.gointernal/api/handler_registrations.gointernal/api/handler_ri_exchange.gointernal/api/handler_ri_exchange_test.gointernal/api/handler_test.gointernal/api/mocks_test.gointernal/api/router_660_permission_flips_test.gointernal/api/router_handlers_test.gointernal/api/validation.gointernal/config/interfaces.gointernal/config/store_postgres.gointernal/config/store_postgres_nil_db_test.gointernal/config/store_postgres_pgxmock_test.gointernal/config/store_postgres_registrations.gointernal/database/postgres/migrations/000066_audit_actor_stamps.down.sqlinternal/database/postgres/migrations/000066_audit_actor_stamps.up.sqlinternal/mocks/stores.gointernal/purchase/approvals.gointernal/purchase/approvals_test.gointernal/purchase/coverage_extra_test.gointernal/purchase/manager.gointernal/purchase/manager_test.gointernal/purchase/mocks_test.gointernal/purchase/reaper.gointernal/purchase/reaper_test.gointernal/scheduler/scheduler_test.gointernal/server/test_helpers_test.go
|
Rebase onto Conflict in
Autopilot agent cannot resolve this safely without risking a regression on the #1032 fix. A human needs to inspect the current state of CR findings (2 actionable) remain unaddressed pending conflict resolution:
Generated by Claude Code |
f468037 to
2f35c19
Compare
…ration Address CodeRabbit review on PR #1011 and resolve the rebase onto feat/multicloud-web-frontend. CR finding 1 (approvals.go): ApproveAndExecute always passed nil as the transition actor, so transitioned_by was NULL even for human session approvals, undercutting the audit objective. Add a transitionedBy *string parameter threaded into TransitionExecutionStatus. Session callers (approvePurchaseViaSession, directExecutePurchase) now pass the session user UUID via validUUIDPtrOrNil(&session.UserID); the token/SQS path (ApproveExecution) keeps nil. Update the PurchaseManagerInterface in both api and server, plus the mocks. CR finding 2 (coverage_extras_test.go): the registration actor-stamping tests called the store mock directly, so they could go false-green if the handler stopped passing the actor. Drive both through the real rejectRegistration handler: the UUID-session path asserts transitioned_by equals the session UUID, the admin-API-key path asserts nil. Rebase fixups: resolve handler_history conflicts (base moved stale-expiry into expireStaleExecutionsAsync; thread the nil actor into that call and its tests). Renumber migration 000066 -> 000068 to avoid collision with the base branch's 000066/000067. Update purchase/api test expectations to the 5-arg TransitionExecutionStatus signature. Add a manager-level regression test asserting a non-nil transitionedBy is threaded for human approvals, and rework the async stale-expire actor-nil test to block on a done channel. Pre-existing base-branch failure TestAuthServiceAdapter_ValidateCSRFToken (fails on the clean base tip, unrelated to this change) is out of scope. Closes #1009
|
Rebased onto Finding dispositions1.
These now fail if the handler ever stops threading the actor. 2.
Rebase fixups
Verification
@coderabbitai full review |
|
The rebase fixups (async stale-expiry actor threading, migration renumber to ✏️ Learnings added
🧠 Learnings used✅ Action performedFull review finished. |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
…closes #1009) Add transitioned_by/transitioned_at columns to purchase_executions, ri_exchange_history, and account_registrations (migration 000066). Thread an optional actor *string through all three TransitionStatus methods so human-initiated transitions record the session user UUID while system-initiated paths (reaper, scheduler, token-based approval) pass nil. Guard non-UUID reviewer IDs (e.g. "admin-api-key") via validUUIDPtrOrNil before using as a FK value. Mirrors the CancelExecutionAtomic(actor) pattern from #804.
…ration Address CodeRabbit review on PR #1011 and resolve the rebase onto feat/multicloud-web-frontend. CR finding 1 (approvals.go): ApproveAndExecute always passed nil as the transition actor, so transitioned_by was NULL even for human session approvals, undercutting the audit objective. Add a transitionedBy *string parameter threaded into TransitionExecutionStatus. Session callers (approvePurchaseViaSession, directExecutePurchase) now pass the session user UUID via validUUIDPtrOrNil(&session.UserID); the token/SQS path (ApproveExecution) keeps nil. Update the PurchaseManagerInterface in both api and server, plus the mocks. CR finding 2 (coverage_extras_test.go): the registration actor-stamping tests called the store mock directly, so they could go false-green if the handler stopped passing the actor. Drive both through the real rejectRegistration handler: the UUID-session path asserts transitioned_by equals the session UUID, the admin-API-key path asserts nil. Rebase fixups: resolve handler_history conflicts (base moved stale-expiry into expireStaleExecutionsAsync; thread the nil actor into that call and its tests). Renumber migration 000066 -> 000068 to avoid collision with the base branch's 000066/000067. Update purchase/api test expectations to the 5-arg TransitionExecutionStatus signature. Add a manager-level regression test asserting a non-nil transitionedBy is threaded for human approvals, and rework the async stale-expire actor-nil test to block on a done channel. Pre-existing base-branch failure TestAuthServiceAdapter_ValidateCSRFToken (fails on the clean base tip, unrelated to this change) is out of scope. Closes #1009
cb5a984 to
6b41ae6
Compare
Rebasing #297 onto feat/multicloud-web-frontend surfaced a migration-number collision: the base sequence already extends past 000061 (tops at 000073), and 000074/000075 are taken by in-flight PRs #1011 and #1094. Move the per-recipient mute migration to the next free slot, 000076, for both the .up.sql and .down.sql, so the merge-ref check-migration-conflicts hook stays green. Update the matching "migration 000061" reference in the config.StoreInterface doc comment.
Summary
transitioned_by UUID NULL REFERENCES users(id) ON DELETE SET NULLandtransitioned_at TIMESTAMPTZ NULLtopurchase_executions,ri_exchange_history, andaccount_registrations(migration 000066).TransitionExecutionStatus,TransitionRIExchangeStatus, andTransitionRegistrationStatuswith a trailingactor *stringparameter: session UUID for human-initiated transitions,nilfor system paths (reaper, scheduler, token approvals).validUUIDPtrOrNilhelper ininternal/api/validation.goto guard non-UUID reviewer IDs (e.g."admin-api-key") before FK insertion. Mirrors theCancelExecutionAtomic(actor)pattern from feat(purchases): in-app revocation within free-cancel window (closes #290) #804.Test plan
go test ./internal/api/... ./internal/config/... ./internal/purchase/... ./internal/analytics/... ./internal/scheduler/... ./internal/server/...-- all pass (2861 passed)TestHandler_pausePlannedPurchase_ActorStamped-- session UUID threaded as actorTestHandler_expireIfStale_SystemActorIsNil-- system expiry path passes nilTestApproveRIExchange_SessionActorStampedandTestRejectRIExchange_TokenPathActorIsNil-- RI exchange actor pathsTestValidUUIDPtrOrNil_*(3 tests) -- validation helper edge casesTestHandler_TransitionRegistrationStatus_ActorStampedandTestHandler_TransitionRegistrationStatus_NonUUIDActorIsNil-- registration actor pathsIF NOT EXISTS/IF EXISTSguards)Closes #1009
Summary by CodeRabbit
Release Notes