fix(web): never trap an admin on the onboarding email-verification gate (BUNYIP-401) #393

Merged
longjacksonle merged 2 commits from fix/BUNYIP-401-admin-never-trapped-on-unverified-email into main 2026-07-24 16:51:25 +02:00

Closes BUNYIP-401. The chicken-and-egg behind PSA-1's "Verify the onboarding process for the admin".

Problem

bunyip-web/src/handlers/mod.rs::needs_onboarding pinned a named-but-unverified user to /onboarding whenever setup_status.email_enabled == true. The onboarding allowlist excludes the admin surface, and admin_guard runs guard(path = "/admin"), so a gated admin could not reach /admin/email to configure SMTP. When delivery is enabled but not actually working (bad credentials, DMARC/SPF reject, wrong SMTP_FROM, unreachable relay) the verification mail never arrives and the only admin is permanently locked out.

login/register carry no verification gate (the BUNYIP-290 bootstrap-admin promotion already works); the trap was solely the web onboarding gate. Bunyip already exempted email_enabled == false; the missing case is email_enabled == true but undeliverable, where the admin is the one party who must be able to intervene.

Fix

Exempt the admin role from the email-verification arm of the onboarding gate. A name is still required (self-service, no email needed); the dashboard keeps the "unverified" badge + resend control, so an admin can still verify once mail works. Regular (non-admin) users are unchanged. The decision is split into a pure onboarding_needed(names_present, email_verified, is_admin, email_enabled) helper so the full matrix is unit-testable without an AppState.

Acceptance

  • A named admin with an unverified email is not redirected to /onboarding and can reach /admin/email, even when email_enabled = true.
  • Regular (non-admin) users are still gated when email delivery is enabled.
  • An admin still needs a name.
  • Unit tests cover the name/verified/admin/email-enabled matrix (onboarding_gate_matrix, admin_is_never_trapped_on_unverified_email).

Verification

cargo test -p bunyip-web onboarding_gate: 5 passed. cargo fmt --all --check clean, cargo clippy -p bunyip-web clean.

Rollout note

This fix ships in the next bunyip release. Until it is deployed, the immediate operational unblock is to boot bunyip with EMAIL_ENABLED=false (or via the admin escape hatch), let the bootstrap admin in, configure SMTP in /admin/email, then re-enable email.

Closes BUNYIP-401. The chicken-and-egg behind PSA-1's "Verify the onboarding process for the admin". ## Problem `bunyip-web/src/handlers/mod.rs::needs_onboarding` pinned a named-but-unverified user to `/onboarding` whenever `setup_status.email_enabled == true`. The onboarding allowlist excludes the admin surface, and `admin_guard` runs `guard(path = "/admin")`, so a gated admin could not reach `/admin/email` to configure SMTP. When delivery is enabled but not actually working (bad credentials, DMARC/SPF reject, wrong `SMTP_FROM`, unreachable relay) the verification mail never arrives and the only admin is permanently locked out. `login`/`register` carry no verification gate (the BUNYIP-290 bootstrap-admin promotion already works); the trap was solely the web onboarding gate. Bunyip already exempted `email_enabled == false`; the missing case is `email_enabled == true` but undeliverable, where the admin is the one party who must be able to intervene. ## Fix Exempt the admin role from the email-verification arm of the onboarding gate. A name is still required (self-service, no email needed); the dashboard keeps the "unverified" badge + resend control, so an admin can still verify once mail works. Regular (non-admin) users are unchanged. The decision is split into a pure `onboarding_needed(names_present, email_verified, is_admin, email_enabled)` helper so the full matrix is unit-testable without an `AppState`. ## Acceptance - [x] A named admin with an unverified email is not redirected to `/onboarding` and can reach `/admin/email`, even when `email_enabled = true`. - [x] Regular (non-admin) users are still gated when email delivery is enabled. - [x] An admin still needs a name. - [x] Unit tests cover the name/verified/admin/email-enabled matrix (`onboarding_gate_matrix`, `admin_is_never_trapped_on_unverified_email`). ## Verification `cargo test -p bunyip-web onboarding_gate`: 5 passed. `cargo fmt --all --check` clean, `cargo clippy -p bunyip-web` clean. ## Rollout note This fix ships in the next bunyip release. Until it is deployed, the immediate operational unblock is to boot bunyip with `EMAIL_ENABLED=false` (or via the admin escape hatch), let the bootstrap admin in, configure SMTP in `/admin/email`, then re-enable email.
fix(web): never trap an admin on the onboarding email-verification gate (BUNYIP-401)
Some checks failed
Check / fmt + clippy + build + tests (pull_request) Failing after 15s
E2E / Playwright against deployment (pull_request) Successful in 30s
8162bcdd39
needs_onboarding pinned a named-but-unverified user to /onboarding whenever email delivery was enabled (setup_status.email_enabled == true). Because the onboarding allowlist excludes the admin surface and admin_guard runs guard(path = "/admin"), a gated admin could not reach /admin/email to configure SMTP. When delivery is enabled but not actually working (bad credentials, DMARC/SPF reject, wrong SMTP_FROM, unreachable relay), the verification mail never arrives and the only admin is permanently locked out - the chicken-and-egg behind PSA-1's admin onboarding.

login/register carry no verification gate (the BUNYIP-290 bootstrap-admin promotion already works); the trap was solely the web onboarding gate. Bunyip already exempted email_enabled == false; the missing case is email_enabled == true but undeliverable, where the admin is the one party who must be able to intervene.

Exempt the admin role from the email-verification arm of the gate. A name is still required (self-service, no email needed) and the dashboard keeps the unverified badge + resend control, so an admin can still verify once mail works. Regular users are unchanged. Split the decision into a pure onboarding_needed(names_present, email_verified, is_admin, email_enabled) helper so the full matrix is unit-testable without an AppState.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDCg5oWRUicf3pVrUR3CqX
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-07-24 16:43:25 +02:00
Author
Owner

Operational unblock

Uses bunyip's built-in escape hatch: with email_enabled == false, the onboarding gate stops requiring verification, so the admin reaches /admin/email.

Pick the locked-out host - staging = ~/docker/server/c-01/bunyip-api, prod = ~/docker/server/nc-01/bunyip-api.

nushell

on the docker repo (edit the pin), example = staging c-01

cd ~/docker
git switch main; git pull --ff-only
git switch --create chore/bunyip-email-disabled-bootstrap

in server/c-01/bunyip-api/compose-variables.yml set: EMAIL_ENABLED: "false"

git commit --all --message "chore(bunyip): temporarily disable email for admin bootstrap"
git push --set-upstream origin chore/bunyip-email-disabled-bootstrap

merge PR, then on the host (c-01):

cd ~/docker/server/c-01/bunyip-api
git pull --ff-only
docker compose up --detach # restart bunyip-api with email off

Then : sign in → clear the 2FA-setup step (self-service TOTP, no email) → you now lands ungated → open /admin/email and confirm/fix SMTP.

Important caveat - re-enabling can re-trap him

SMTP creds are already in env on both deploys and email_enabled=true, so the real symptom is likely the verification mail not delivering to @niceguyit.biz (SMTP_FROM/relay/SPF/DMARC), not SMTP being unset. That matters because:

  • If you just flip EMAIL_ENABLED back to "true" while is still unverified (mail never reached him), the current deployed v0.7.0 re-traps him. The escape hatch is only durable once one of these is true:
    a. BUNYIP-401 is deployed (admin never needs verification) - the clean fix. Cut a bunyip release after #393 merges. Or
    b. verification mail actually delivers (fix SMTP_FROM/relay so @niceguyit.biz accepts it), you clicks verify, then re-enable.
--- Operational unblock Uses bunyip's built-in escape hatch: with email_enabled == false, the onboarding gate stops requiring verification, so the admin reaches /admin/email. Pick the locked-out host - staging = ~/docker/server/c-01/bunyip-api, prod = ~/docker/server/nc-01/bunyip-api. nushell # on the docker repo (edit the pin), example = staging c-01 cd ~/docker git switch main; git pull --ff-only git switch --create chore/bunyip-email-disabled-bootstrap # in server/c-01/bunyip-api/compose-variables.yml set: EMAIL_ENABLED: "false" git commit --all --message "chore(bunyip): temporarily disable email for admin bootstrap" git push --set-upstream origin chore/bunyip-email-disabled-bootstrap # merge PR, then on the host (c-01): cd ~/docker/server/c-01/bunyip-api git pull --ff-only docker compose up --detach # restart bunyip-api with email off Then : sign in → clear the 2FA-setup step (self-service TOTP, no email) → you now lands ungated → open /admin/email and confirm/fix SMTP. Important caveat - re-enabling can re-trap him SMTP creds are already in env on both deploys and email_enabled=true, so the real symptom is likely the verification mail not delivering to @niceguyit.biz (SMTP_FROM/relay/SPF/DMARC), not SMTP being unset. That matters because: - If you just flip EMAIL_ENABLED back to "true" while is still unverified (mail never reached him), the current deployed v0.7.0 re-traps him. The escape hatch is only durable once one of these is true: a. BUNYIP-401 is deployed (admin never needs verification) - the clean fix. Cut a bunyip release after #393 merges. Or b. verification mail actually delivers (fix SMTP_FROM/relay so @niceguyit.biz accepts it), you clicks verify, then re-enable.
style: wrap onboarding_needed signature to satisfy rustfmt (BUNYIP-401)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 38s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m8s
Create release / Create release from merged PR (pull_request) Has been skipped
9b315cdb64
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDCg5oWRUicf3pVrUR3CqX
longjacksonle deleted branch fix/BUNYIP-401-admin-never-trapped-on-unverified-email 2026-07-24 16:51:26 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!393
No description provided.