From 21343cdf234d67da01de8d4032ed573107b12815 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 11 May 2026 16:36:57 +0200 Subject: [PATCH] =?UTF-8?q?fix(user):=20rename=20yaml=20key=20username?= =?UTF-8?q?=E2=86=92email=20so=20admin=20seed=20reads=20APP=5FADMIN=5FUSER?= =?UTF-8?q?NAME?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #513. UserDataInitializer reads `@Value("${app.admin.email:...}")` but application.yaml mapped APP_ADMIN_USERNAME to `app.admin.username`. The keys never connected — env vars APP_ADMIN_USERNAME and APP_ADMIN_PASSWORD were silently ignored and the admin user got seeded with the hardcoded defaults admin@familyarchive.local / admin123. For production this is HIGH severity: DEPLOYMENT.md §3.5 documents the admin password as permanently locked on first deploy. The bug locked the lock-in to dev defaults, not to whatever an operator set in PROD_APP_ADMIN_PASSWORD. Rename yaml key from `username:` to `email:` so the Spring property `app.admin.email` actually exists. Keep env-var name APP_ADMIN_USERNAME (matches the already-set Gitea secrets and DEPLOYMENT.md §3.3). Default value updated to an email-shape. Added AdminSeedPropertyKeyTest (Binder pattern, no Spring context): verifies both `app.admin.email` and `app.admin.password` resolve from the yaml. Confirmed red without the fix, green with it. Co-Authored-By: Claude Opus 4.7 --- backend/src/main/resources/application.yaml | 6 +- .../user/AdminSeedPropertyKeyTest.java | 68 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 backend/src/test/java/org/raddatz/familienarchiv/user/AdminSeedPropertyKeyTest.java diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index 6e12b9f6..179ed09e 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -69,7 +69,11 @@ app: from: ${APP_MAIL_FROM:noreply@familienarchiv.local} admin: - username: ${APP_ADMIN_USERNAME:admin} + # Key must be `email`, not `username` — UserDataInitializer reads + # `${app.admin.email:...}`. The env-var name stays APP_ADMIN_USERNAME + # to match the existing Gitea secrets and DEPLOYMENT.md §3.3. + # See #513. + email: ${APP_ADMIN_USERNAME:admin@familienarchiv.local} password: ${APP_ADMIN_PASSWORD:admin123} import: diff --git a/backend/src/test/java/org/raddatz/familienarchiv/user/AdminSeedPropertyKeyTest.java b/backend/src/test/java/org/raddatz/familienarchiv/user/AdminSeedPropertyKeyTest.java new file mode 100644 index 00000000..99157bc7 --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/user/AdminSeedPropertyKeyTest.java @@ -0,0 +1,68 @@ +package org.raddatz.familienarchiv.user; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.io.ClassPathResource; + +import java.util.Properties; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Pins the admin-seed property key contract. {@code UserDataInitializer} reads + * {@code @Value("${app.admin.email:...}")} and {@code @Value("${app.admin.password:...}")}. + * The yaml MUST expose those exact keys, not e.g. {@code app.admin.username}, or + * the env vars {@code APP_ADMIN_USERNAME} / {@code APP_ADMIN_PASSWORD} are + * silently ignored and the admin user gets seeded with the hardcoded defaults. + * + *

Discovered as a HIGH bug during the production-deploy bootstrap (#513): on + * first deploy the prod admin password is permanently locked to whatever ends + * up in the database, so a key-name mismatch would lock prod to the dev defaults + * {@code admin@familyarchive.local} / {@code admin123}. + * + *

No Spring context — Binder reads application.yaml directly. + */ +class AdminSeedPropertyKeyTest { + + @Test + void admin_email_key_binds_from_yaml() { + Binder binder = binderFromApplicationYaml(); + + String email = binder.bind("app.admin.email", String.class) + .orElseThrow(() -> new AssertionError( + "app.admin.email is missing from application.yaml. " + + "UserDataInitializer reads this exact key; if the yaml uses " + + "a different name (e.g. 'username'), the env var " + + "APP_ADMIN_USERNAME is silently ignored.")); + + assertThat(email) + .as("app.admin.email must resolve from APP_ADMIN_USERNAME or its default") + .isNotBlank(); + } + + @Test + void admin_password_key_binds_from_yaml() { + Binder binder = binderFromApplicationYaml(); + + String password = binder.bind("app.admin.password", String.class) + .orElseThrow(() -> new AssertionError( + "app.admin.password is missing from application.yaml. " + + "UserDataInitializer reads this exact key.")); + + assertThat(password) + .as("app.admin.password must resolve from APP_ADMIN_PASSWORD or its default") + .isNotBlank(); + } + + private Binder binderFromApplicationYaml() { + YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean(); + yaml.setResources(new ClassPathResource("application.yaml")); + Properties props = yaml.getObject(); + assertThat(props).as("application.yaml must be on the classpath").isNotNull(); + return new Binder(ConfigurationPropertySources.from( + new PropertiesPropertySource("application", props))); + } +}