feat(seed): template-declared owns scope and password_env (PSA-56 part 1) #345

Merged
nrupard merged 4 commits from feat/PSA-56-owns-scope-password-env into main 2026-07-06 19:31:57 +02:00
Owner

The two loader generalizations that let the Playwright E2E accounts - which are @a8n.run (not the reserved demo domain) with an env-injected password - be expressed as a seed template. This is the pure, gate-verifiable foundation; wiring reset to owns, the committed e2e.json, and retiring bunyip-e2e-bootstrap follow in part 2 (the E2E-gate-touching bits).

What's here (all pure, unit-tested)

  • SeedOwns + SeedFile.owns - a file declares the email domains and explicit emails it owns. covers() decides membership (domain match anchored on @, or an exact email, both case-insensitive). effective_owns() defaults to the reserved seed domain when a file omits the block, so existing files keep working. Validation now requires every user and feedback-author email to be covered by the file's own scope, so a reset can always reclaim exactly what a file created and no file can reclaim another's rows.
  • SeedUser.password_env - a user may name an env var to source its password from at load time (secret_env, _FILE-aware, trimmed before hashing), so a secret never lands in the committed file. Precedence: password_env, then a literal password, then default_password. An empty password_env is rejected.
  • The demo-msp generator declares its owns (the reserved domain) explicitly; demo-msp.json regenerated. The export mapper stamps owns = the domain it was scoped to, so a round trip validates and reset reclaims the same set.

Tests

owns.covers (domain + explicit email, case-insensitive, no sibling-domain over-match); an E2E-style file owning two explicit @a8n.run emails validates; a user outside owns is rejected; an empty password_env is rejected.

Verification

just check-container green (fmt + clippy -D warnings + cargo test --workspace --all-targets; seed lib 58 tests incl. the 4 new). password_env resolution and the export owns stamp are compile-checked; DB behaviour runs against a dev database.

Part 2 (follow-up)

Reset scoped to owns (repo methods + seed reset), a committed e2e.json (owns the two @a8n.run emails, password_env: BUNYIP_E2E_TEST_USER_PASSWORD), converting bunyip-e2e-bootstrap to load it through the loader, and keeping the E2E CI gate green. That is where AC 3-6 land.

AC status (this PR)

Covered: owns-scope declaration + validation (AC 1 foundation), password_env sourcing + validation (AC 2), owns/password_env tests (AC 7 foundation). Open in part 2: e2e.json (AC 3), bin conversion (AC 4), guards + cleanup semantics (AC 5), the live E2E gate (AC 6), reset-scoping DB behaviour.

#PSA-56

The two loader generalizations that let the Playwright E2E accounts - which are `@a8n.run` (not the reserved demo domain) with an env-injected password - be expressed as a seed template. This is the pure, gate-verifiable foundation; wiring reset to `owns`, the committed `e2e.json`, and retiring `bunyip-e2e-bootstrap` follow in part 2 (the E2E-gate-touching bits). ## What's here (all pure, unit-tested) - **`SeedOwns` + `SeedFile.owns`** - a file declares the email domains and explicit emails it owns. `covers()` decides membership (domain match anchored on `@`, or an exact email, both case-insensitive). `effective_owns()` defaults to the reserved seed domain when a file omits the block, so existing files keep working. Validation now requires every user and feedback-author email to be covered by the file's own scope, so a reset can always reclaim exactly what a file created and no file can reclaim another's rows. - **`SeedUser.password_env`** - a user may name an env var to source its password from at load time (`secret_env`, `_FILE`-aware, trimmed before hashing), so a secret never lands in the committed file. Precedence: `password_env`, then a literal `password`, then `default_password`. An empty `password_env` is rejected. - The demo-msp generator declares its `owns` (the reserved domain) explicitly; `demo-msp.json` regenerated. The export mapper stamps `owns` = the domain it was scoped to, so a round trip validates and reset reclaims the same set. ## Tests `owns.covers` (domain + explicit email, case-insensitive, no sibling-domain over-match); an E2E-style file owning two explicit `@a8n.run` emails validates; a user outside `owns` is rejected; an empty `password_env` is rejected. ## Verification `just check-container` green (fmt + clippy `-D warnings` + `cargo test --workspace --all-targets`; seed lib 58 tests incl. the 4 new). `password_env` resolution and the export `owns` stamp are compile-checked; DB behaviour runs against a dev database. ## Part 2 (follow-up) Reset scoped to `owns` (repo methods + `seed reset`), a committed `e2e.json` (owns the two `@a8n.run` emails, `password_env: BUNYIP_E2E_TEST_USER_PASSWORD`), converting `bunyip-e2e-bootstrap` to load it through the loader, and keeping the E2E CI gate green. That is where AC 3-6 land. ## AC status (this PR) Covered: `owns`-scope declaration + validation (AC 1 foundation), `password_env` sourcing + validation (AC 2), `owns`/`password_env` tests (AC 7 foundation). Open in part 2: `e2e.json` (AC 3), bin conversion (AC 4), guards + cleanup semantics (AC 5), the live E2E gate (AC 6), reset-scoping DB behaviour. #PSA-56
feat(seed): template-declared owns scope and password_env (PSA-56 part 1)
Some checks failed
E2E / Playwright against deployment (pull_request) Successful in 25s
Check / fmt + clippy + build + tests (pull_request) Has been cancelled
3b97d6d6f1
The two loader generalizations that let the E2E accounts (which are @a8n.run, not under the reserved demo domain, with an env-injected password) be expressed as a seed template. This is the pure foundation; wiring reset to `owns`, the committed `e2e.json`, and retiring bunyip-e2e-bootstrap follow in part 2.

- `SeedOwns` + `SeedFile.owns`: a file declares the email domains and explicit emails it owns. `covers()` decides membership (domain match anchored on `@`, or an exact email, both case-insensitive). `effective_owns()` defaults to the reserved seed domain when a file omits the block, so existing files keep working. Validation now requires every user and feedback-author email to be covered by the file's own scope (replacing the single hardcoded reserved-domain check), so a reset can always reclaim exactly what a file created and one file can never reclaim another's rows.
- `SeedUser.password_env`: a user may name an environment variable to source its password from at load time (secret_env, `_FILE`-aware, trimmed before hashing), so a secret never lands in the committed file. Precedence: password_env, then a literal password, then default_password. Validation rejects an empty password_env.
- The demo-msp generator now declares its `owns` (the reserved domain) explicitly; demo-msp.json regenerated. The export mapper stamps `owns` = the domain it was scoped to, so a round trip validates and reset reclaims the same set.

Tests: `owns.covers` (domain + explicit email, case-insensitive, no sibling-domain over-match); an E2E-style file owning two explicit @a8n.run emails validates; a user outside `owns` is rejected; an empty password_env is rejected.

Verified: just check-container green (fmt + clippy -D warnings + cargo test --workspace --all-targets; seed lib 58 tests incl. the 4 new). password_env resolution and the export `owns` stamp are compile-checked; their DB behaviour runs against a dev database.

Part 2 (follow-up): reset scoped to `owns` (repo methods + `seed reset`), a committed `e2e.json` (owns the two @a8n.run emails, password_env BUNYIP_E2E_TEST_USER_PASSWORD), converting bunyip-e2e-bootstrap to load it, and the E2E CI gate.

#PSA-56
docs(seed): update the reserved-domain wording for the owns scope
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 24s
Check / fmt + clippy + build + tests (pull_request) Successful in 9m17s
0cad1e8779
Review follow-up. Three doc comments still described the pre-PSA-56 rule that every seeded email must sit under the reserved SEED_EMAIL_DOMAIN. Validation now keys on the file's declared `owns` scope, so the module header, SeedFeedback.email, and is_seed_email docs are updated to match, and the module header now calls out that `owns` is a deliberate relaxation of the old hardcoded invariant (a file may own a real domain, e.g. the E2E accounts own @a8n.run), so reset safety rests on the non-production guard plus trusted, correctly-scoped files.

#PSA-56
feat(seed): reset scoped to owns + retire bunyip-e2e-bootstrap to a template (PSA-56 part 2)
Some checks failed
E2E / Playwright against deployment (pull_request) Successful in 25s
Check / fmt + clippy + build + tests (pull_request) Has been cancelled
a86a319cb7
Completes PSA-56 (AC 3-6): the E2E accounts are now produced by loading a committed template through the file-driven loader, with no account rows in bin code.

- reset scoped to `owns`: `seed::reset(pool, &owns)` deletes exactly the users and feedback the scope covers - whole-domain delete for the reserved domain, exact-email delete for explicit emails. Guardrail (`domain_reset_allowed`): a whole-domain reset is refused for any non-reserved domain, so a file owning a real domain can never delete every account under it; real domains must be reset via explicit `owns.emails`. New `FeedbackRepository::delete_by_email` for the exact-email path. The loader's idempotent feedback-clear now scopes to the file's `owns` too (via the same guardrailed helper).
- `seed reset [file]`: reset a file's declared `owns`, or the reserved domain when no file is given.
- Committed `bunyip-api/seed/e2e.json`: the two accounts (subscriber + admin), `owns` scoped to their two exact `@a8n.run` emails (never a whole domain), password sourced from `password_env: BUNYIP_E2E_TEST_USER_PASSWORD`.
- bunyip-e2e-bootstrap now `include_str!`s e2e.json and drives it through `seed::load` (seed / refresh) and `seed::reset` (`--cleanup`). No `ACCOUNTS`, no raw upsert. The bin name, flags (`--cleanup` / `--dry-run`), guards (`BUNYIP_E2E_BOOTSTRAP_ALLOW` + non-production `ENVIRONMENT`), and env (`BUNYIP_E2E_TEST_USER_PASSWORD`) are all preserved, so the CI invocation and the Playwright gate's contract are unchanged.

Behavior note: the loader now sets each account's membership to none/free explicitly (the template carries no membership block), where the old raw upsert left the DB column defaults. The accounts are login-only in the suite, so this should be transparent; the E2E gate on this PR is the authoritative check of the bin conversion.

Tests: `domain_reset_allowed` only for the reserved domain; the committed e2e.json parses, both accounts source the password from the env var, and its `owns` is email-scoped (no domains), so a reset can only ever reclaim those two accounts.

Verified: just check-container green (fmt + clippy -D warnings + cargo test --workspace --all-targets; seed lib 59 tests + the e2e template test). reset/loader DB behaviour is compile-checked; the live Playwright E2E gate (AC 6) runs on this PR.

#PSA-56
fix(seed): reject empty/unset seed passwords and non-reserved owns.domains
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 25s
Check / fmt + clippy + build + tests (pull_request) Successful in 8m33s
Create release / Create release from merged PR (pull_request) Has been skipped
355fcafe03
Review follow-ups on PSA-56 part 2.

- The loader now rejects an empty resolved password and an unset `password_env` var, restoring the guard the old bunyip-e2e-bootstrap had (it bailed on an empty BUNYIP_E2E_TEST_USER_PASSWORD). Without this, a set-but-empty secret would silently seed the E2E accounts with an empty-password hash. Password resolution is factored into `resolve_password` and the two new errors (`EmptyPassword`, `PasswordEnvUnset`) name the failure precisely (a `password_env`-unset failure no longer surfaces as the misleading "no password / default_password").
- Validation now rejects a non-reserved domain in `owns.domains`: a real domain must be scoped via `owns.emails`, so a whole-domain reset can never reclaim every account under it. Previously such a file validated but then failed at load when the guardrail fired; enforcing it at parse makes validate/load consistent and fails fast.

Tests: `resolve_password` accepts a literal / default and rejects empty, missing, and an unset env var; validation rejects `owns.domains: ["a8n.run"]`.

Verified: just check-container green (fmt + clippy -D warnings + cargo test --workspace --all-targets; seed lib 61 tests).

#PSA-56
nrupard scheduled this pull request to auto merge when all checks succeed 2026-07-06 19:21:40 +02:00
nrupard deleted branch feat/PSA-56-owns-scope-password-env 2026-07-06 19:31:58 +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!345
No description provided.