fix(user): fail-closed when admin seed would use dev defaults outside dev/test/e2e
Some checks failed
Some checks failed
Addresses Nora's review concern on #513/#516. The previous fix only made env-vars take effect — it did NOT close the fail-open default path. If an operator forgets APP_ADMIN_USERNAME / APP_ADMIN_PASSWORD on first prod boot, the seeded admin is the well-known `admin@familienarchiv.local` / `admin123` and is permanently locked (UserDataInitializer only seeds when the row is missing). Refuse to seed outside dev/test/e2e profiles when either credential matches the documented default. The startup fails fast with a clear message pointing at the env-var names and the permanence trap. Also adds Markus/Felix/Sara's "pin the Java side" coverage: a reflection test on the @Value placeholder catches a future rename of `${app.admin.email:...}` back to `${app.admin.username:...}`, which would otherwise pass the yaml-side test but silently break the binding. Tests: - AdminSeedFailClosedTest pins fail-closed for non-local profiles and verifies the dev/test/e2e bypass. - AdminSeedPropertyKeyTest now also asserts the @Value placeholder string on UserDataInitializer.adminEmail/adminPassword. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit was merged in pull request #516.
This commit is contained in:
@@ -20,6 +20,7 @@ import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
import java.time.LocalDate;
|
||||
@@ -31,19 +32,39 @@ import java.util.Set;
|
||||
@DependsOn("flyway")
|
||||
public class UserDataInitializer {
|
||||
|
||||
@Value("${app.admin.email:admin@familyarchive.local}")
|
||||
static final String DEFAULT_ADMIN_EMAIL = "admin@familienarchiv.local";
|
||||
static final String DEFAULT_ADMIN_PASSWORD = "admin123";
|
||||
|
||||
@Value("${app.admin.email:" + DEFAULT_ADMIN_EMAIL + "}")
|
||||
private String adminEmail;
|
||||
|
||||
@Value("${app.admin.password:admin123}")
|
||||
@Value("${app.admin.password:" + DEFAULT_ADMIN_PASSWORD + "}")
|
||||
private String adminPassword;
|
||||
|
||||
private final AppUserRepository userRepository;
|
||||
private final UserGroupRepository groupRepository;
|
||||
private final Environment environment;
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner initAdminUser(PasswordEncoder passwordEncoder) {
|
||||
return args -> {
|
||||
if (userRepository.findByEmail(adminEmail).isEmpty()) {
|
||||
// Fail-closed in production: refuse to seed with the well-known
|
||||
// defaults. Otherwise an operator who forgets APP_ADMIN_USERNAME
|
||||
// / APP_ADMIN_PASSWORD locks production to admin@…/admin123 PERMANENTLY
|
||||
// (UserDataInitializer only seeds when the row is missing — see #513).
|
||||
// Allowed in dev/test/e2e because those run without secrets configured.
|
||||
boolean isLocalProfile = environment.matchesProfiles("dev", "test", "e2e");
|
||||
if (!isLocalProfile
|
||||
&& (DEFAULT_ADMIN_EMAIL.equals(adminEmail)
|
||||
|| DEFAULT_ADMIN_PASSWORD.equals(adminPassword))) {
|
||||
throw new IllegalStateException(
|
||||
"Refusing to seed admin user with default credentials outside "
|
||||
+ "the dev/test/e2e profiles. Set APP_ADMIN_USERNAME and "
|
||||
+ "APP_ADMIN_PASSWORD to non-default values before first boot — "
|
||||
+ "this lock-in is permanent."
|
||||
);
|
||||
}
|
||||
log.info("Kein Admin-User '{}' gefunden. Erstelle Default-Admin...", adminEmail);
|
||||
|
||||
UserGroup adminGroup = UserGroup.builder()
|
||||
|
||||
Reference in New Issue
Block a user