Repository navigation
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>
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_flightset000146fixes 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 new000146_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
000146. Main ends at000134. Open PRs use000135–000143, our unopenedfix/issue-47takes000144, and a sibling unopened branch (fix/issue-339) already has000145. If those land after this one, 000146 has to be applied last or renumbered. The test usespreviousMigrationVersion(t, 146).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 empty000146body), 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_000146_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