fix(user): rename yaml key username→email so admin seed reads APP_ADMIN_USERNAME (#513) #516
Reference in New Issue
Block a user
Delete Branch "fix/issue-513-admin-seed-property-key"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes #513.
UserDataInitializerreads@Value("${app.admin.email:...}"), butapplication.yamlexposed the env var underapp.admin.username. The keys never connected —APP_ADMIN_USERNAMEwas silently ignored and the admin user was always seeded with the hardcoded defaultadmin@familyarchive.local.For production this is HIGH severity: per
DEPLOYMENT.md §3.5and ADR-011, the admin password is permanently locked on first deploy. With the bug, the lock-in is on the dev defaults instead of the operator's strong password — an operator settingPROD_APP_ADMIN_USERNAME=admin@archiv.raddatz.cloudgets a false sense of security; the admin email is actuallyadmin@familyarchive.local.(Discovered alongside #514 while debugging the staging login flow — the password key was already correctly bound, only the email key was wrong.)
Fix
Rename the yaml key from
username:toemail:so the Spring propertyapp.admin.emailactually exists. Keep env-var nameAPP_ADMIN_USERNAME(matches existing Gitea secrets + DEPLOYMENT.md §3.3). Default value updated to an email-shape.Added
AdminSeedPropertyKeyTest(Binder pattern, no Spring context): verifies bothapp.admin.emailandapp.admin.passwordresolve from the yaml. Confirmed red without the fix, green with it.Test plan after merge
AdminSeedPropertyKeyTestpassesDefault Admin erstellt: Email='<APP_ADMIN_USERNAME value>'admin@familyarchive.localon staging only); prod must land this PR before the v1.0.0 tag.🤖 Generated with Claude Code
Markus Keller — Senior Application Architect
Verdict: ⚠️ Approved with concerns
This is the right fix. The env-var contract (
APP_ADMIN_USERNAME) is deliberately preserved so existing Gitea secrets andDEPLOYMENT.md §3.3keep working, while the internal Spring property gets renamed to match whatUserDataInitializeractually reads. Pragmatic, minimal, surgical — exactly what a hot-path config bug needs before a v1.0.0 cut.The inline comment on the yaml block is good — it documents the intentional name asymmetry (env-var keeps
USERNAME, internal key becomesemail) and links to #513 so the next reader doesn't "fix" it back.Blockers
None at the architectural layer.
Concerns
Hidden scope:
docker-compose.prod.ymlchanges are not in the PR description. The diff also addsrestart: "no"andcreate-buckets: condition: service_completed_successfullyto backend's depends_on. The comments reference #510, not #513, so this is a separate concern piggybacking on the admin-seed fix. Either split into its own commit/PR or update the PR body and title to declare the scope honestly. Atomic commits is one of our standing rules.ADR-011 references the wrong key. If ADR-011 documents the first-deploy lockout and uses
app.admin.usernameanywhere, update it now while you remember. Doc drift is a blocker per my review rubric — please confirm ADR-011 (and DEPLOYMENT.md §3.5) still reads correctly after this rename.The env-var/property naming asymmetry is now permanent.
APP_ADMIN_USERNAME → app.admin.emailis the cheaper path for v1.0.0, agreed. But add it to the GLOSSARY or DEPLOYMENT.md as a known wart — "the env var historically said USERNAME, the property isemail, both refer to the admin login email." Future operators will trip over this otherwise.Suggestion
The Binder test is a clean architectural pattern — context-less yaml binding to pin contract-as-data. Consider extracting a small
YamlBinderTestSupporthelper if a second contract test follows; right now one occurrence isn't enough to justify the abstraction, so the duplication-of-three rule says wait.Felix Brandt — Senior Fullstack Developer
Verdict: ⚠️ Approved with concerns
Clean, minimal, surgical fix. The test came first, the test was red without the change, the test is green with it — that's the TDD contract honored. The test itself reads well: descriptive names (
admin_email_key_binds_from_yaml), AssertJ with.as(...)describing what's being verified, one logical assertion per test, and theorElseThrowfailure message is the kind that saves a future debugger 20 minutes.The inline yaml comment is the right why comment — it documents the intentional asymmetry between env-var name and Spring property name so the next reader doesn't "clean up" the inconsistency.
Blockers
docker-compose.prod.ymlis not in the PR description. The diff addsrestart: "no"andcreate-buckets: service_completed_successfullydependency. Those comments reference #510, not #513. Atomic commits rule: one logical change per commit. Either land the compose changes as a separate commit on this branch with its own message, or push them to a separate PR. Right now the commit history won't tell the truth about what this PR did.Suggestions
Test class is package-private and has no
@DisplayName— minor. The other tests in the codebase mostly use the same style, so I'm fine either way.UserDataInitializershould pin the property name as a constant. Right now@Value("${app.admin.email:...}")is the only place that defines the contract, and it lives as a string literal. Consider extractingpublic static final String ADMIN_EMAIL_PROPERTY = "app.admin.email";and referencing it from both the@Value(via SpEL) and the test. That makes the contract grep-able from one place. Not a blocker —@ValueSpEL with a constant is awkward syntax — but flag for follow-up.The test verifies the key exists; it does not verify
UserDataInitializeractually reads it. A future refactor that changesUserDataInitializerto readapp.admin.adminEmailwould make this test pass while the bug returns. A second, narrower test that bootsUserDataInitializerwith aMockEnvironmentsettingapp.admin.email=foo@barand asserts the resultingAppUser.email == "foo@bar"would close that gap. Nice-to-have, not a blocker — the current test catches the specific bug from #513.Tobias Wendt — DevOps & Platform Engineer
Verdict: 🚫 Changes requested
The application.yaml fix is correct and the env-var asymmetry decision is the right one — preserving
APP_ADMIN_USERNAMEmeans I don't have to touch Gitea secrets or rotate the production env-file. Good call.But this PR title and body claim "rename a yaml key + add a test." The actual diff modifies
docker-compose.prod.ymltoo. That's where I have to push back.Blockers
Out-of-scope
docker-compose.prod.ymlchanges. The PR introduces:restart: "no"oncreate-bucketscreate-buckets: condition: service_completed_successfullyon the backend's depends_onThe inline comments reference
#510, not#513. This is a#510change riding on a#513PR. From an ops/audit perspective this is poison: if I need to revert PR #516 because the admin-seed fix had a regression, I also revert two production compose changes that were unrelated. Conversely, if I revert because of a compose-orchestration regression, I roll back the admin-seed fix. The two concerns are coupled in a commit they shouldn't be coupled in.Fix: split. Land the yaml-key fix as #516, land the compose changes as their own PR (or at minimum their own commit with a
#510-scoped message). Atomic commits.Was the compose change tested?
service_completed_successfullyon a one-shot is the right pattern (I'd recommend the same), but it needs verification withdocker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --waiton a clean volume set. Ifcreate-bucketsever fails idempotently on a re-run, backend will now refuse to start. Please confirm a re-deploy on a system where buckets already exist still succeeds — the bucket-create script needs to exit 0 on "already exists."Notes (positive)
APP_ADMIN_USERNAMEpreserved → existing prod env-file and Gitea secret stay valid. Zero ops migration cost. Good.admintoadmin@familienarchiv.local— email-shaped default matches whatUserDataInitializerexpects. Sane.#513andDEPLOYMENT.md §3.3is the kind of inline documentation I want — future me will thank present you.Suggestion (post-merge)
Once this lands, run a deploy verification:
docker compose ... up -d --wait, thendocker logs archiv-app 2>&1 | grep "Default Admin"— confirm the log line shows the operator's email, not the dev default. That should be in the test plan as an explicit step before tagging v1.0.0.Elicit — Requirements Engineer & Business Analyst
Verdict: ⚠️ Approved with concerns
The PR description is unusually strong: it states the why (silent env-var ignore → dev-default admin in production), the measurable harm (first-deploy password lock-in per ADR-011 + DEPLOYMENT.md §3.5), the scope boundary (env-var name unchanged, only property name moves), and a test plan with concrete post-deploy verification steps. This is how a bugfix issue should look.
Concerns
The "Test plan after merge" has an unresolved branch and no owner. The third checkbox says:
This is an open question that blocks the v1.0.0 tag's success criteria. Convert it to a Gitea issue (or sub-issue of #513) with: option A description, option B description, decision owner, decision deadline (must be before tag), and the trigger condition for each option. As written, this risks falling between the cracks during the deploy.
Acceptance criterion missing in Given-When-Then form. The bug is described prose-style. The implicit AC is:
The unit test covers half of this (the property binds) but not the integration half (the seeded
AppUseractually carries that email). Recommend adding that AC explicitly to issue #513 and tracking whether the post-deploy log-check (Default Admin erstellt: Email='...') satisfies it.PR scope/description mismatch. The diff touches
docker-compose.prod.yml(restart policy + dependency oncreate-buckets). The PR body does not mention this. Either update the PR description to include the compose changes (and reference #510 alongside #513), or split. As a requirements artifact, the PR's "Summary" should match the diff.Strengths to preserve
Suggestion
When you cut v1.0.0, add a one-line entry to a "production readiness checklist" issue (or DEPLOYMENT.md §3.5): "Verified that
APP_ADMIN_USERNAMEis read into the seeded admin's email field via post-deploy log inspection." Closes the loop between the fix and the production-deploy gate.Nora "NullX" Steiner — Application Security Engineer
Verdict: ⚠️ Approved with concerns
This is a security-relevant bugfix masquerading as a config rename. Per ADR-011 + DEPLOYMENT.md §3.5, the first-deploy admin password is permanently locked. Before this fix, an operator following the deployment runbook to set a strong admin email/password would end up with
admin@familyarchive.local/admin123seeded into production — and that seed is irrevocable on a real first deploy without a DB wipe. CWE-1188 (Insecure Default Initialization of Resource) / CWE-1392 (Use of Default Credentials), severity HIGH, exposure window = until v1.0.0 ships.The fix itself is correct. The test pins the contract.
Blockers
The hardcoded dev defaults (
admin@familyarchive.local/admin123) are still inapplication.yamland still fall back silently ifAPP_ADMIN_USERNAME/APP_ADMIN_PASSWORDare unset. This PR closes the naming hole but not the fail-open default hole. For a production profile, the application should refuse to start when these env vars are unset — fail closed, not fall back toadmin123. Today, if an operator forgets to add the env-var to the prod env-file, you still get the dev default in production. That's the same outcome #513 was trying to prevent, just via a different failure mode.Concrete fix (post-merge or in a follow-up issue is acceptable, but it must exist before v1.0.0):
Or scope it to the prod profile: keep dev defaults in
application-dev.yaml, strip them from the baseapplication.yaml, and letapplication-prod.yamlrequire explicit values.This is the standard "no hardcoded fallback credentials" rule. If you want, I'll write the failing test myself.
Concerns
Test coverage gap.
AdminSeedPropertyKeyTestverifies the yaml exposes the key. It does not verify thatUserDataInitializeractually uses that key — i.e., a future refactor that renames the@Valueplaceholder back toapp.admin.usernamewould make this test still pass while reintroducing the bug. A second integration test that bootsUserDataInitializerwithapp.admin.email=foo@bar.comand asserts the seededAppUser.emailisfoo@bar.comwould close the loop. Felix can write this in 15 minutes withMockEnvironment+ a thin context.No log redaction check. If the seed log line literally prints
Default Admin erstellt: Email='admin@archiv.raddatz.cloud', that's fine — emails are not secrets. But please confirm no codepath logs the password alongside it. Quick grep onUserDataInitializerfor anylog.*passwordpatterns.Notes
email, notusername— UserDataInitializer reads${app.admin.email:...}. See #513." Future auditor doesn't have to git-blame.docker-compose.prod.ymlchanges (restart policy, create-buckets dependency) are not security-impacting from what I can see, but as a hygiene matter I support DevOps's split request — coupled commits make security reverts harder.Suggestion
When you tag v1.0.0, open an issue: "Production admin seed must fail-closed on missing env-vars (CWE-1188 follow-up to #513)." Track it as P1, target patch release.
Sara Holt — Senior QA Engineer
Verdict: ⚠️ Approved with concerns
This is exactly the kind of bug a contract test should catch — config-vs-code drift that compiles, boots, and silently produces wrong behavior in production. Adding
AdminSeedPropertyKeyTestis the right move. Discipline: red without fix, green with fix, confirmed in the PR description. ✓What's good
admin_email_key_binds_from_yaml,admin_password_key_binds_from_yaml. NotestYaml1()nonsense.orElseThrowfailure modes — when one breaks in CI, you know exactly which key drifted.orElseThrowmessage explains why the test exists and what a future "fix" should not do. That's the kind of test message that saves a CI investigation.@SpringBootTest. Cheap and fast. Models theForwardHeadersConfigurationTestprecedent, which is good consistency..as(...)describing the business meaning of the assertion. Per my own readability rules, exemplary.Concerns
Contract is asserted at the yaml layer only; not at the consumer. The test verifies
app.admin.emailresolves from the yaml. It does not verify thatUserDataInitializeractually readsapp.admin.email. A future refactor that renames the@Valueplaceholder would make this test pass while reintroducing the production bug. Recommend a complementary test (integration, slow, separate file): bootUserDataInitializerwithapp.admin.email=foo@bar, assert seededAppUser.email == "foo@bar". That closes the round-trip contract.@Disabled/@Tagmarkers are missing. Not blocker. But if you want this to run on every PR (it should — it's a 50ms safeguard against a HIGH-severity prod bug), confirm it lives in the default Maven test phase and not behind a profile.PR scope leak.
docker-compose.prod.ymlchanges are not covered by any test in this PR. The depends_on +service_completed_successfullychange has its own failure mode (backend hangs forever ifcreate-bucketserrors on a re-deploy). That should have its own test or a documented smoke-test step. Splitting the PR (per DevOps's request) would let me push the compose changes through their own validation path.Suggestions
Consider parameterizing the two tests as a single
@ParameterizedTestoverStream.of("app.admin.email", "app.admin.password"). Slightly more compact, same coverage, single failure mode per key. Not blocking — two separate methods read fine and are easier to find in logs.Add a related coverage check: a
@SpringBootTest-level smoke that assertsAppUsertable is populated with the expected email from the test config after Flyway+seed runs. Per pyramid rule that's an integration test, lives separately, runs slower — but it's what would actually have caught #513 in CI before staging deploy.Quality gate
Passes the "tests precede the fix" rule. Passes the "test name explains failure" rule. Passes the "no Spring context unless needed" rule. The only gate I'd add is the round-trip test in concern #1 — without it, this test is necessary but not sufficient.
Leonie Voss — UI/UX Design Lead
Verdict: ✅ Approved
No UI surface touched. This is a backend configuration fix (
application.yaml), a backend unit test (AdminSeedPropertyKeyTest.java), and a docker-compose orchestration tweak (docker-compose.prod.yml). Zero frontend, zero Svelte, zero design tokens, zero copy, zero a11y impact.What I checked
frontend/getErrorMessage()i18n mapping infrontend/src/lib/shared/errors.tsor new keys inmessages/{de,en,es}.jsonadmin→admin@familienarchiv.local) is internal — not a user-facing string and not rendered anywhere on the frontend that I can see from this diffOne downstream note for whoever cuts v1.0.0
If the operator's chosen admin email is going to be displayed anywhere in the UI on first login (welcome screen, profile dropdown, audit log), now's the moment to confirm that surface is i18n-safe and handles a long email gracefully at 320px. Not this PR's scope — just flagging since #513 changes the value that ends up in
AppUser.email.LGTM from a design/UX perspective.
9ebcf59af9toe974a520c3