fix(sandbox): repair the silently-failing nightly sandbox cleanup and lock down its RPCs (#1449)
* fix(sandbox): repair the silently-failing nightly sandbox cleanup and lock down its RPCs
The daily cleanup cron has deleted nothing for months: cleanup_sandbox_user
died on the journal-line immutability trigger for every user (the seed posts
vouchers since spring), and cleanup_expired_sandbox_users swallowed each
failure as a WARNING while reporting success. 658 expired sandbox users plus
21 orphaned anonymous users had accumulated in prod auth.users.
- cleanup_sandbox_user sets the sanctioned gnubok.allow_delete flag plus a
new transaction-local gnubok.sandbox_cleanup flag, only after verifying
is_sandbox; write_audit_log, audit_log_immutable (DELETE only, per-row
sandbox re-check), enforce_dimension_registry_guards (DELETE only) and
enforce_pending_operations_no_delete (DELETE only) respect it
- clears salary_runs voucher-link FKs and purges the sandbox company's
audit rows before the auth.users cascade
- cleanup_expired_sandbox_users returns {cleaned, failed, orphans_removed},
additionally sweeps expired anonymous users that never got a
company_settings row, and takes an optional p_limit for bounded batches;
the cron route logs failures at error level and accepts both return shapes
- both RPCs lose their default PUBLIC EXECUTE grant (anon and authenticated
could call them via PostgREST) and are now service_role-only
- validated by replaying the full delete chain against prod inside aborted
transactions (21 users sampled across all seed eras, zero failures) and a
committed staging run; pg-real suite + cron route unit tests added
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sandbox): per-row sandbox re-verification in teardown guards, is_anonymous column guard
Resolution pass for PR #1449 review findings and the pg-real CI failure:
- Swedish accounting review: enforce_dimension_registry_guards and
enforce_pending_operations_no_delete now re-verify per row that
OLD.company_id belongs to a sandbox company (same pattern as
audit_log_immutable) instead of trusting the gnubok.sandbox_cleanup flag
alone. Because that re-check needs company_settings to still exist,
cleanup_sandbox_user deletes pending_operations and dimensions explicitly
before the auth.users cascade.
- pg-real CI: auth.users.is_anonymous does not exist in the CI
supabase/postgres image (or on older self-hosted stacks); the orphan sweep
in cleanup_expired_sandbox_users is now guarded on the column's existence,
and the pg test skips the orphan assertions on such stacks.
Re-validated on staging end-to-end: {cleaned: 5, failed: 0,
orphans_removed: 1}, fresh users and non-sandbox rows untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sandbox): make company_settings.is_sandbox write-once, prove orphan sweep fails loudly
Round-2 review findings (Swedish accounting review on PR #1449):
- Every teardown bypass trusts company_settings.is_sandbox, and RLS lets an
owner update their own settings row via PostgREST, so a real company that
flipped the flag would become eligible for full deletion by the nightly
cron. New trigger makes the flag write-once (no application path updates
it; a future sandbox-to-real conversion would ship its own migration).
- New pg test pins the reviewer's remaining concern: an anonymous user who
somehow has bookkeeping but no company_settings row is NOT silently
deleted by the orphan sweep; the unbypasseed immutability triggers make
the deletion fail loudly into the summary's failed count.
Validated on staging: flip blocked in both directions, unrelated
company_settings updates unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sandbox): guard is_sandbox provenance at INSERT, make orphan sweep exclusions explicit
Round-3 review hardening, approved by Emil:
- is_sandbox = true can now only be created by an anonymous-user JWT (the
sandbox seed's actor), service_role, or a direct database session. A
regular authenticated user could previously insert their settings row
pre-flagged and have the nightly cron destroy their real books, which
BFL 7 kap. forbids even self-inflicted. Claims are read from the
request.jwt.* GUCs directly so the check behaves identically on hosted,
self-hosted, and the CI auth shim.
- The orphan sweep now explicitly excludes anonymous users attached to any
companies or company_members row, instead of relying on downstream
immutability triggers throwing (emergent safety) to protect half-seeded
users.
- pg tests updated accordingly: blocked/allowed provenance paths, and the
half-seeded user is proven unreachable rather than merely failing loudly.
Validated on staging: authed insert blocked, anonymous-claim insert
allowed, half-seeded user untouched, sweep summary failed=0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sandbox): all-rows sandbox check, cleared bypass flags, tighter insert guard
CodeRabbit review pass on PR #1449 (its first non-rate-limited run):
- cleanup_sandbox_user now requires EVERY company_settings row of the user
to be sandbox-flagged, not an arbitrary single row: a hypothetical
mixed-company user would otherwise have their real company's rows reached
by the user-scoped deletes.
- Both bypass flags are cleared before the RPC returns, so later work in
the same transaction (the expired loop's next iterations, the orphan
sweep) never runs with them still armed.
- The is_sandbox insert guard now treats ANY PostgREST claims context
(claims json without a role claim included) as guarded, instead of
falling open when the role claim is absent.
- The flag-leak pg test now runs inside an explicit transaction (the old
version could not observe transaction-local GUCs at all), and a new test
covers the mixed sandbox/real user refusal.
Declined: replacing the em dashes inside the two replicated Swedish
exception messages; they are byte-identical copies of the strings already
deployed by migration 20260702084500 and changing them would alter live
user-facing errors out of scope.
Validated on staging: mixed user refused, flags cleared post-teardown,
role-less claims blocked.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6458180bf5
commit
f7f3a31f8e
@@ -821,3 +821,4 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
|
||||
[2026-08-06] Bucket A defaults pass commits the /pending Godkänn pill directly for low/medium risk and keeps the ConfirmationDialog only for high risk: the Granskning row already states source, title, risk and offers Detaljer, so the dialog's second Godkänn restated the row (the audit's expert lens called double-Godkänn the thing professionals do not tolerate). The chat-side "Godkänn alla N" was DEFERRED, not built: ApprovalCard owns its whole state machine internally (commit fetch, account-activation retry, typed high-risk confirm) and a bulk commit from AgentChat would leave committed cards rendering as pending; that is assistant-redesign seam 8.8 (approval batching) and needs the state lifted, not a button.
|
||||
[2026-08-06] SIE-export period default left unchanged despite the choice-audit finding: FiscalYearSelector with includeAllOption=false already auto-selects the newest started period once loaded, so the "opens with nothing selected" claim is only fetch latency. FyPicker gained preferLatestEnded for helårsmoms instead, which also skips the shared per-company localStorage scope on that surface: a filing page defaulting to the current (unfilable) year because Balansräkningen was last viewed there is the one wrong default.
|
||||
[2026-08-06] Review-workflow triage on the Bucket A branch (13 confirmed findings): fixed 10, incl. the branch-killing one (setActiveCompany's cookie write throws in Server Component render, so the /select-company auto-forward silently never fired: the cookie set is now best-effort because the gnubok-company-id cookie is write-only compat nothing reads). Batch "Ingen moms" now goes over the wire as 'exempt' instead of collapsing to undefined, which had an explicit no-VAT choice booking the derived 25%; the same pre-existing collapse in QuickReviewDialog/CategoryExpandedDialog is left for a follow-up. Skipped by choice: generalizing AiFilledIndicator for history provenance (the note's copy already names the source) and converting BulkBookInboxDialog's hardcoded-Swedish option lists to i18n (whole-file migration, not this branch's divergence). Monthly momsdeklaration default is deadline-aware (M-2 until the 12th/17th, M-1 after; over-40M always M-1) mirroring deadline-config, not just calendar-ended.
|
||||
[2026-08-07] Sandbox cleanup repaired via a dedicated gnubok.sandbox_cleanup transaction-local flag (respected by write_audit_log, audit_log_immutable, enforce_dimension_registry_guards, enforce_pending_operations_no_delete) instead of tombstoning sandbox users like the real account-deletion flow: sandbox data is synthetic demo content, not rakenskapsinformation, so full deletion is the correct GDPR/BFL posture and the audit-log DELETE bypass re-verifies per row that the company is a sandbox before letting anything through. Also revoked the default PUBLIC EXECUTE both cleanup RPCs had carried on prod since March (anon could call them via PostgREST), and validated the whole delete chain by replaying it against prod inside aborted transactions (21 sampled users across all seed eras) plus a committed staging run.
|
||||
|
||||
Reference in New Issue
Block a user