Skip to content

fix(#480): hard-delete the seed artefacts instead of parking them in the Trash - #484

Merged
guycorbaz merged 2 commits into
mainfrom
fix/480-seed-gate-hard-delete
Sep 10, 2026
Merged

guycorbaz merged 2 commits into
mainfrom
fix/480-seed-gate-hard-delete

Conversation

@guycorbaz

Copy link
Copy Markdown
Owner

Closes #480.

The residue

The #173 seed gate soft-deleted the admin / librarian rows whose password_hash still matched the documented seed value. That was enough to fire the wizard and to stop admin/admin from logging in, but it left two things behind:

  1. The seeded rows stayed recoverable for the 30 days before auto-purge — one UPDATE users SET deleted_at = NULL away. The Trash panel's Restore button is that UPDATE. Once [CR] The Trash panel's Restore button points at a route that does not exist #478 wires the missing route, an administrator who sees admin and librarian in the Trash and restores them — reasonably, believing a predecessor deleted them — gets back a live administrator whose password is published in this repository's own SECURITY.md.
  2. The seeded sessions row was never touched. Its token is published in CLAUDE.md, in the E2E helpers and in the git history. Inert today on two counts, which is one count more than it should need.

This is the change #480 says must land before #478.

The change

seed_gate::apply_with now runs one transaction:

  • delete the sessions — by the published token, and by user_id of any seeded row (fk_sessions_user has no CASCADE, so this ordering is required, not cosmetic);
  • delete the user rows outright.

admin_audit.user_id and api_keys.created_by are ON DELETE SET NULL (#69 / #70), so audit history survives detached — an integration test pins that.

Two deliberate choices:

  • No deleted_at filter. An instance upgrading from v1.18.0 or earlier carries rows its old gate soft-deleted; the first boot on this version purges them too.
  • The hash guard is unchanged. An operator who rotated the seeded password keeps their row, live. The published session token goes regardless of whose row it points at — the value itself is public.

One consequence worth stating: an operator who has been running with MYBIBLI_SEED_DEV_USERS=1 and then unsets it no longer gets the seeded admin back from the Trash. That is the point of the issue; the wizard fires and they create a real admin.

Coverage

Rust — tests/seed_gate.rs moves from "both rows are soft-deleted" to "both rows are gone", plus: the published token is removed; a second session of a seeded user goes with the user; the audit row is detached, not cascaded; rows an older version soft-deleted are purged on the next boot; and the rotated-password and idempotence properties still hold.

Browser — the e2e-wizard CI lane no longer wipes users and sessions with raw SQL. docker-compose.wizard.yml unsets MYBIBLI_SEED_DEV_USERS, so the gate itself clears them during the boot the lane already waits for. Wiping by hand would have hidden a broken gate; letting the gate do it makes that lane the end-to-end proof that a fresh production install starts clean. The wizard smoke spec then asserts what an operator actually sees: an empty Trash panel, and the published session cookie no longer authenticating (/admin bounces to /login).

Docs (README.md, .env.example, CLAUDE.md) say hard-delete where they said soft-delete.

Testing

Run locally against the wizard stack and the test MariaDB:

  • cargo clippy --all-targets -- -D warnings — clean.
  • cargo test --lib — 1171 passed.
  • cargo test --tests --no-fail-fast — 31 binaries, 204 tests, all green.
  • Wizard stack boot log: removed_count=2, sessions_removed=1; sessions empty, only SYSTEM left in users.
  • MYBIBLI_SETUP_E2E=1 npx playwright test specs/journeys/setup-wizard.spec.ts — passed against that stack.

🤖 Generated with Claude Code

https://claude.ai/code/session_01D5cV9QRqQAFpiNKZwZfoNX

guycorbaz and others added 2 commits September 10, 2026 21:09
The seed gate soft-deleted the `admin` / `librarian` rows whose hash
still matched the documented seed value. That made the wizard fire and
stopped `admin/admin` from logging in, but left two residues: the seed
`password_hash` stayed recoverable for the 30 days before auto-purge,
one `UPDATE users SET deleted_at = NULL` away — which is exactly what
the Trash panel's Restore button does — and the seeded `sessions` row,
whose token is published in CLAUDE.md and the git history, was never
touched at all.

The gate now deletes both outright, in one transaction: sessions first
(`fk_sessions_user` has no CASCADE), then the user rows. `admin_audit`
and `api_keys` are ON DELETE SET NULL, so their rows survive detached.
The predicate no longer filters on `deleted_at`, so an instance
upgrading from v1.18.0 or earlier purges the rows its old gate had
soft-deleted.

The hash guard is unchanged: an operator who rotated the seeded
password keeps their row.

Refs #480

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5cV9QRqQAFpiNKZwZfoNX
…e browser

Integration coverage (`tests/seed_gate.rs`) moves from "both rows are
soft-deleted" to "both rows are gone", and adds: the published session
token is removed; a second session of a seeded user goes with the user
(the FK that would otherwise abort the DELETE); the audit trail is
detached rather than cascaded away; rows an older version soft-deleted
are purged on the next boot.

For the browser side, the `e2e-wizard` CI lane no longer wipes `users`
and `sessions` with raw SQL. The wizard compose override unsets
MYBIBLI_SEED_DEV_USERS, so the gate itself clears them at boot — which
turns that lane into the end-to-end proof that a fresh production
install starts clean. The wizard smoke spec then asserts what an
operator would see: an empty Trash panel, and the published session
cookie no longer authenticating.

README, .env.example and CLAUDE.md say hard-delete where they said
soft-delete.

Refs #480

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5cV9QRqQAFpiNKZwZfoNX
@guycorbaz
guycorbaz marked this pull request as ready for review September 10, 2026 19:25
@guycorbaz
guycorbaz merged commit 67a6c70 into main Sep 10, 2026
8 checks passed
@guycorbaz
guycorbaz deleted the fix/480-seed-gate-hard-delete branch September 10, 2026 19:25
guycorbaz added a commit that referenced this pull request Sep 11, 2026
Cut the v1.19.0 release documentation: version bump in Cargo.toml /
Cargo.lock, and every version-bearing surface required by Foundation
Rule 19 brought to 1.19.0.

- Manual EN/FR: title-page version, install snippets, a "What's new in
  1.19.0" section in chapter 8 (the Restore button that never worked,
  the seeded accounts leaving no trace, the bounded cover decode, and
  an explicit "nothing to do on upgrade"); the release-notes section
  now states that the PDFs are attached to the release rather than
  promising it once CI is wired. PDFs rebuilt.
- README: status line, live-install label, image-size badge, current-
  release paragraph.
- ROADMAP: current-stable header, a v1.19.0 shipped section, and the
  "merged but not released" block retired now that it has shipped.
- docs/dockerhub-overview.md: tags list.
- website/: index (nav badge, hero, JSON-LD softwareVersion, the hero
  paragraph rewritten around this release), about (nav badge), roadmap
  (meta descriptions, JSON-LD, nav badge, both narrative paragraphs),
  sitemap lastmod — stale since May, and the surface Rule 19 names as
  the easiest to forget.
- sprint-status.yaml: last_updated header.

No source change. The release carries #478, #480 and #479 (merged in
#485, #484 and #486), the supply-chain CI work (#481), the network-
posture documentation (#482), the community-health files (#477) and the
documentation audit (#487). No migration.


Claude-Session: https://claude.ai/code/session_01D5cV9QRqQAFpiNKZwZfoNX

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[CR] Seed gate leaves the seeded session row and recoverable seed credentials in the database

1 participant