Repository navigation
fix(config/db): seed-group rollbacks, partition-safe 000027 dedupe, fail-closed plan providers, revocation_in_flight clear - #1268
Conversation
…config store - 000059 down: only remove ...0005 when it really is Purchaser. 000057 gave that UUID to Standard Users, so the old down deleted Standard Users and stripped it from every member. Refuse with a clear error rather than leave a Purchaser-only user with no group. 000096 down gets the same up-front refusal instead of a bare users_min_one_group violation. - 000027: dedupe savings_snapshots on (tableoid, ctid). ctid is per partition, so matching on it alone deleted unrelated rows of other partitions on a forced re-run. - DerivePlanProviders: accept the "provider:service" keys 000040 wrote, and return an error instead of an empty provider set when no key names a provider, so both plan-account guards fail closed. - MarkPurchaseRevoked clears revocation_in_flight, as 000072 documents; migration 000146 clears it on rows already revoked. Real-Postgres tests for each, using the shared container. Normalizing the plan-services key delimiter is deferred (see the PR). Refs LeanerCloud#316 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first LeanerCloud#316 commit made DerivePlanProviders fail when no service key had a provider prefix. That refused supported plans: a legacy alias kept under its original key (LeanerCloud#1123's singleton-alias edit path, exercised by TestLegacyServiceB1S1*) has a key like "original-key", and so do fixtures keyed "ec2" with Provider "aws". Each entry's provider now comes from its key prefix ("provider/service" or "provider:service"), falling back to the entry's own Provider field, which is what mergeRequestedServices and plan step resolution use. The check fails closed only when no entry identifies a provider either way. Refs LeanerCloud#316 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>
main's LeanerCloud#1400 added main_integration_test.go with a TestMain that stops the shared PostgreSQL container after the run. This branch added an identical TestMain to helpers_test.go, so after merging main the integration build of the package declared TestMain twice and failed to compile. Keep main's TestMain, which does the same teardown, and restore helpers_test.go to main's version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
CI fix pushed. New head: Root cause. #1400 on main added Fix. Commit Other changes: I merged Checks run locally on the new head:
|
|
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: rB14. 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: rB15. 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 1 (cold, fresh context): VERDICT: APPROVE 2627040 Findings (none blocking):
Claims checked against code and issue #316:
Attacked:
@IAmJSD on the merge-order nit: once 000146 is on main, any of #1239/#1240/#1241/#1253/#1302 that still carries 000140-000145 will be skipped by golang-migrate on every DB already at 146, so whichever lands later needs renumbering above the highest on main. Happy to merge this one last among them, or have it renumbered, whichever you prefer. |
|
Independent review 2 (claude-fleet, cold): APPROVE at 2627040 Blocking items: none. Non-blocking items:
Per-claim table (claim / implemented? / test fails without fix? / evidence):
Migration analysis:
Caller enumeration for DerivePlanProviders (head):
Revocation race analysis:
Commands and results (GOTOOLCHAIN=go1.26.10, credentials nulled, heavy runs under the lock):
Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged. |
…n 000146 -> 000142 golang-migrate only applies versions above the database's current one, so PRs must merge in migration-number order. 000140-000144 had been taken by PRs that are now held on owner decisions (LeanerCloud#1239, LeanerCloud#1241, LeanerCloud#1253). The ready PRs move to 000140-000145 so they can merge ahead of them, and the held ones will take the next free numbers when decided. Rename only: the SQL statements are unchanged, and the test version constants and the comments naming the migration follow the new number. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Renumbered migration 000146 -> 000142 so it can merge ahead of PRs held on owner decisions (#1239, #1241, #1253, which will take the next free numbers when decided). Rename only; SQL unchanged. New head: The migration test's name follows the number too (TestMigration_000146_ClearRevokedInFlight is now TestMigration_000142_ClearRevokedInFlight), as do its fixture purchase IDs (ri-146-* is now ri-142-*). A first, parallel integration run hit testcontainers start failures caused by machine load. A serial re-run of api, config and migrations passed. Merge order for the ready PRs: #1240 (000140) -> #1302 (000141) -> #1268 (000142) -> #1287 (000143) -> #1289 (000144) -> #1116 (000145). The PR body's migration-number paragraph is updated to match. Checks on this head (local): build, vet (plain, integration, e2e), golangci-lint v2.10.1 with and without the integration tag and gocyclo -over 10 on the changed packages, |
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked: I checked each finding against issue #316 and traced both
Ran Body-stale:
Reviewer: rE5. 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: rE6. 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 |
Summary
This PR addresses the four 2026-09-02 audit findings bundled in the issue (
A04-010…A04-013). One part ofA04-012is deferred (normalizing the plan-services key delimiter, see below), so this is a partial fix. I re-checked each finding against current main (a14dc8f2), and all four still reproduced.git log --grepalso turns up commitda731285, which mentions this issue number, but it is an old federation-test commit from before the renumbering and has nothing to do with these findings.Refs #316
Per-finding status
a14dc8f2...0005when it really isPurchaserusers_min_one_groupalready aborts the DO block atomically, so nothing is destroyed, but the error is a bare CHECK violationusers_min_one_groupand restoresusers.role. An emptygroup_idsis then the legitimate pre-000024 state, so failing loud would block a valid rollbackctidacross partitions(tableoid, ctid)DerivePlanProvidersfails openprovider:servicekeys, identifies legacy-keyed entries by theirProviderfield, and errors instead of returning an empty setaws/savings-planskeys were never splitMarkPurchaseRevokedleavesrevocation_in_flightset000142fixes rows already revokedRoot cause and fix
A04-010: seed-group down migrations
000057 seeds
Standard Usersat00000000-0000-5000-8000-000000000005. 000059 tried to seedPurchaserat the same UUID, and itsON CONFLICT DO NOTHINGmade that a no-op (#942). 000064 then put Purchaser at...0007and owns that row from then on. Its own down is a no-op. Even so, 000059's down removed...0005from every user and deleted the group. That destroyed Standard Users, or aborted onusers_min_one_groupif a user had no other group.000059_seed_purchaser_group.down.sql: the membership strip and the delete now run only if the row at...0005is namedPurchaser. If any user would be left with no group, the down raises a clear error instead.system_managedis still dropped. One thing stays as-is: 000059's admin backfill added...0005(Standard Users) to every admin, and those memberships can't be told apart from deliberate ones (documented in the file).000096_seed_ri_exchanger_group.down.sql: the same up-front refusal for RI-Exchanger-only users, inside the existing DO block.A04-011: 000027 dedupe
savings_snapshotsis partitioned by month, andctidis a position inside a single partition's heap. Matching onctidalone meant a duplicate at(0,1)in one partition also deleted the row at(0,1)in every other partition. The CTE now selectstableoidand the DELETE matches on(tableoid, ctid). Duplicates of one(id, timestamp)always sit in the same partition, so thetableoidtie-break added to the ORDER BY never changes which row is kept. This edits an already-applied up migration. It only affects fresh databases and the forced re-run that the file's header describes.A04-012: fail-open provider derivation
DerivePlanProviderscut keys on/and silently skipped anything else. That included theaws:savings-plans-<type>keys 000040 wrote, whichfindServiceKeyininternal/apialready accepts as valid. An empty result means "no restriction" to both guards (Handler.validatePlanAccountProvidersandPostgresStore.validatePlanAccountProvidersTx), so a colon-keyed AWS plan accepted an Azure account. Now each entry's provider is resolved like this:/or:in the key.Providerfield, when the key has no prefix. This covers legacy aliases kept under their original key by fix(api): preserve legacy service aliases on plan update #1123's singleton-alias edit path (e.g."original-key"), and plans keyed"ec2"withProvider: "aws". That is the same field the legacy edit path (mergeRequestedServices) and plan step resolution go by. Main skipped such entries, so a legacy-keyed plan accepted an account of any provider.TestPlanWriteBodiesAssociationCompatibilityrelies on this: it stores an unknown"future"services entry and expects account assignment to keep working.([]string, error). If the plan has services but none of them identifies a provider, it returns an error. The handler maps that to a logged 500 (no plan UUID in the response), and the store returns it as-is.Behavior change from main for mixed plans: a plan with
aws/ec2plus a legacy-keyed Azure entry now derives{aws, azure}instead of{aws}. That matches what the plan actually buys. A key prefix still wins over a conflictingProviderfield, as before.The first commit on this branch failed closed whenever no key had a provider prefix. That broke the supported legacy-key plans (
TestLegacyServiceB1S1RoutedLedger,…RoutedRefusals,…Migration127Replay). The second commit (acd60dd4) adds theProvider-field fallback.A04-013:
revocation_in_flightnever clearedMigration 000072,
PurchaseHistoryRecord.RevocationInFlightand the interface doc all say a successfulMarkPurchaseRevokedresets the flag, but the UPDATE didn't.MarkPurchaseRevokednow setsrevocation_in_flight = falsein the same guarded UPDATE (WHERE revoked_at IS NULL). The new000142_purchase_history_clear_revoked_in_flightclears the flag on rows that are already revoked. It leaves unrevoked flagged rows alone, because those are the finalize sweep's queue. Its down is a documented no-op.Concurrency: concurrent
MarkPurchaseRevokedcalls on one row (the handler's retry loop plus the finalize sweep) serialize on the row lock. The losers re-checkrevoked_at IS NULL, match nothing and take the idempotent path. The integration test runs 8 concurrent marks on one in-flight row and checks that exactly one stamp lands and the flag ends cleared. A lateFlipPurchaseRevocationInFlightfrom a stale concurrent request can still re-set the flag on a row that is already revoked. That was already possible before this PR (see "Out of scope").Migration number
Migration number: 000142 (renumbered from 000146 on 2026-10-26). main is at 000139. The reviewed, ready PRs take 000140-000145 and merge in this order: #1240 (000140) → #1302 (000141) → #1268 (000142) → #1287 (000143) → #1289 (000144) → #1116 (000145). golang-migrate skips a version below the database's current one, so merge this after #1302 and before #1287. #1239 (was 000140-000141), #1241 (was 000143) and #1253 (was 000144) are held on owner decisions and will be renumbered to start at 000146 when decided. The test uses
previousMigrationVersion(t, 142).Verification
All evidence below is local. "Integration" means real PostgreSQL in Docker, using the shared container (
testhelpers.RequireMigratedDatabase). The handler tests are mock-store unit tests. Fail-before runs swapped inorigin/main's production files (and an empty000142body), kept the new tests, and then restored the files.TestMigration_SeedPurchaserGroup_Down/Standard_Users_and_its_members_survive_the_rollback(integration)users_min_one_groupviolation…/Standard_Users_is_not_silently_destroyed(integration)…/a_Purchaser-only_user_stops_the_rollback_with_a_clear_error(integration)…/a_real_Purchaser_at_...0005_is_still_removed(integration, preservation)TestMigration_SeedRIExchangerGroup_Down/an_RI-Exchanger-only_user_stops…(integration)TestMigration_SavingsSnapshotsPK_DedupeStaysInPartition(integration)TestMigration_000142_ClearRevokedInFlight(integration)TestMarkPurchaseRevoked_ClearsRevocationInFlight(integration, incl. 8-way concurrent marks)TestSetPlanAccounts_ProviderCheckFailsClosed(integration, store path: colon key, legacy key rejected and accepted, unidentifiable)TestSetPlanAccounts_ColonKeyedPlanStillChecksProvider,…_UnparseableServiceKeyFailsClosed,…_LegacyKeyedPlanUsesProviderField(unit, mock store)TestDerivePlanProviders*(unit)TestDerivePlanProviders_LegacyKeyUsesProviderField,TestSetPlanAccounts_LegacyKeyedPlanUsesProviderField, the legacy subtests ofTestSetPlanAccounts_ProviderCheckFailsClosedaaeab564, the regression)Regression check for the legacy-key plans, plus every test file that reaches
SetPlanAccountsorcreatePlan(integration, run by name):I grepped
internal/for services fixtures whose keys have no provider prefix ("ec2","original-key","original-unusual-key","one","service"). Every one that reaches the provider check setsProviderand is covered by the runs above. The rest (mergeRequestedServicesunit tests,plan_display_test.go, and the"aws|rds"key inrecommendation_overrides_test.go, which is a different map type) never callDerivePlanProviders.Combined-tree verification. This branch was tested together with the other queued branches on a local, never-pushed tree:
queue/platform@936996ad= origin/maina14dc8f2plus these heads, merged in this order:fix/issue-72andfix/issue-312conflict on one line ininternal/api/handler_dashboard.go(both change thecalculateCommitmentMetricscall). The tree used #72's side with #312's validatedprovidervariable; whichever PR merges second takes that same resolution.Linux, Go 1.26.9,
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true; Docker Postgres via testcontainers:936996ad:go build ./...,go vet ./...,go vet -tags integration ./...and the fullgo test -race -short -count=1 ./....936996ad: golangci-lint v2.10.1 found 0 issues on every changed package, with and without--build-tags integration.tests/e2eis a separate module, so it was vetted under its owne2etag.gocyclo -over 10is silent on the changed non-test files.-tags integration -count=1, 60-minute local timeout because the VM was shared):936996ad, these pass:internal/server,internal/credentials,internal/scheduler,internal/database/postgres/migrations,internal/purchaseandinternal/api.internal/configpasses exceptTestPostgresStoreDB_ResolveStaleRIExchange_ValidatesCloudCheck, which fails the same way on unmodified main (fixed by fix(config): compare resolve checked_at at the database's microsecond precision #1252).26795376, same branches at earlier heads):internal/analytics,internal/auth,internal/commitmentopts,internal/databaseandcmd/lambda/clear-rate-limitall pass. The later fixes don't touch those packages.httpToLambdaRequestsignature change;scripts/setup-git-secrets.shpatterns in a fresh repo with an isolated HOME, found nothing in the changed files, and a planted fake key was flagged.scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.Out of scope / deferred
aws/savings-plansumbrella keys that 000040 never touched. I left it out for three reasons. Open fix(plans): migrate stored pre-#608 Azure/GCP service ids to canonical slugs #1140 (000136_canonical_legacy_plan_service_ids) rewritespurchase_plans.servicestoo and deliberately keeps keys unchanged because fix(api): preserve legacy service aliases on plan update #1123's edit path relies on them. What to do when a plan has bothaws/xandaws:xis an owner call. And the savings-plans identifier cleanup belongs to umbrella Normalize 'savings-plans' vs 'savingsplans' identifier across frontend + backend #7, which the maintainer asked to keep open. The fail-closed fix above removes the security-relevant part without it.FlipPurchaseRevocationInFlighthas norevoked_at IS NULLguard, so a stale concurrent revoke request can re-flag a revoked row. Readers checkrevoked_atfirst, and that request's failed Return clears the flag again. Guarding it would change its not-found error semantics, so I left it unchanged....0005). Rolling back can't tell those memberships from deliberate ones, so they stay.No frontend change needed.
Maintainer: please mirror the issue's triage labels on this PR:
impact/internal,triaged,priority/p3,severity/low,urgency/eventually,effort/m,type/bug.🤖 Generated with Claude Code