Repository navigation
fix(registrations): leave reviewed_by NULL for admin-API-key reviews - #1247
Conversation
Approving or rejecting a registration with the admin API key stamped the non-UUID sentinel "admin-api-key" into account_registrations.reviewed_by, a UUID foreign key to users. Postgres rejected the UPDATE with SQLSTATE 22P02 and the request returned 500, so admin-key reviews never worked. setReviewMetadata now stores only a real user UUID in reviewed_by (the same validUUIDPtrOrNil rule transitioned_by already uses) and logs the non-user principal so the audit trail is kept. reviewed_at is still set. Adds real-Postgres tests through HandleRequest: admin-key approve and reject succeed with reviewed_by NULL, a session reviewer is still stamped, and concurrent admin-key approvals yield one 200, the rest 409, and a single cloud account. Closes LeanerCloud#1234 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in main's CI fixes (LeanerCloud#1236, LeanerCloud#1252) and CI changes since, so this PR gets a fresh CI run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale:
Reviewer: rB21. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale:
Reviewer: rB23. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
Local verification at |
|
Merged by IAmJSD's agent at IAmJSD's request (IAmJSD is acting owner while the maintainers are away), under the bar IAmJSD set: 2 approving independent review(s) on |
Root cause
setReviewMetadata(internal/api/handler_registrations.go) copiedsession.UserIDintoreg.ReviewedBywithout checking it. For an admin-API-key requestrequireAdminreturns the sentinel sessionUserID = "admin-api-key". The store then wrote that string intoaccount_registrations.reviewed_by, which isUUID REFERENCES users(id)(migration 000017), in bothTransitionRegistrationStatusandUpdateAccountRegistration. Postgres rejected it with SQLSTATE 22P02, so every approve or reject made with the admin key returned 500 and left the rowpending.transitioned_byalready went throughvalidUUIDPtrOrNil(#1009), butreviewed_bydid not.Fix
setReviewMetadatanow stores only a real user UUID inreviewed_by, using the samevalidUUIDPtrOrNilrule. For the admin keyreviewed_bystays NULL,reviewed_atis still set, and a log line records the non-user principal and the decision so the audit trail is kept, as the issue suggests. No schema change and no migration.Concurrency: the status transition is still the existing
UPDATE ... WHERE id = $1 AND status = 'pending', so double approval stays prevented. Admin-key approvals could not succeed before this fix, so that race path was never reachable until now. It is covered by a test below.Closes #1234
Verification
All commands were run in the worktree with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null, because this machine's default AWS profile is invalid.New real-Postgres integration tests (testcontainers, through
Handler.HandleRequestso the router and the admin-key principal middleware run too), ininternal/api/handler_registrations_api_key_integration_test.go:TestRegistrationReview_AdminAPIKey_Postgres/approve:x-api-keyapprove returns 200. The row isapproved,reviewed_byandtransitioned_byare NULL,reviewed_atis set, andcloud_account_idis linked, so the follow-upUpdateAccountRegistrationalso succeeds.TestRegistrationReview_AdminAPIKey_Postgres/reject: returns 200. The row isrejectedwithreviewed_byNULL,reviewed_atset and the rejection reason stored.TestRegistrationReview_SessionReviewerStamped_Postgres: a real session reviewer is still written toreviewed_byandtransitioned_by. This catches a fix that nulls the column for everyone.TestRegistrationReview_AdminAPIKey_ConcurrentApprove_Postgres: 8 concurrent admin-key approvals of one registration give exactly one 200 and seven 409s, and create exactly one cloud account.Unit (mock) test:
TestHandler_TransitionRegistrationStatus_NonUUIDActorIsNilnow also assertsreg.ReviewedBy == nilandreg.ReviewedAt != nil.Fail before the fix (handler change reverted, tests kept):
Pass after the fix:
All of this evidence comes from local Postgres via testcontainers plus mocked auth. Nothing was tested against a deployed environment or a real admin API key.
Out of scope / notes
UpdateAccountRegistration/TransitionRegistrationStatus) still writes whateverReviewedByit receives. The FK-safety rule lives in the handler, which is where the session sentinel is known, and matches howtransitioned_byandcreated_by_user_idare handled. I didn't add a guard at the store level that would silently drop bad values.reviewed_byinGET /api/registrations/:id, because the field isomitempty. The frontend declaresreviewed_byinfrontend/src/api/registrations.tsbut never displays it, so no UI change is needed. Showing "reviewed by the admin API key" in the UI would need a separate column, which the issue left optional.Maintainers: issue #1234 currently has no triage labels, so I had none to mirror. Please triage the issue and copy its labels to this PR.
🤖 Generated with Claude Code