SecurityDataInitializer.java
package com.taxonomy.security.config;
import com.taxonomy.security.model.AppRole;
import com.taxonomy.security.model.AppUser;
import com.taxonomy.security.repository.RoleRepository;
import com.taxonomy.security.repository.UserRepository;
import com.taxonomy.security.service.BootstrapAdminCredentialStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Set;
/** Seeds local roles and an explicitly configured or one-time administrator. */
@Component
@Profile("!keycloak")
@Order(Ordered.LOWEST_PRECEDENCE)
public class SecurityDataInitializer implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(SecurityDataInitializer.class);
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private final RoleRepository roleRepository;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final BootstrapAdminCredentialStore bootstrapCredentialStore;
private final String configuredAdminPassword;
private final boolean requirePasswordChange;
public SecurityDataInitializer(RoleRepository roleRepository,
UserRepository userRepository,
PasswordEncoder passwordEncoder,
BootstrapAdminCredentialStore bootstrapCredentialStore,
@Value("${taxonomy.admin-password:}") String configuredAdminPassword,
@Value("${taxonomy.security.require-password-change:true}")
boolean requirePasswordChange) {
this.roleRepository = roleRepository;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.bootstrapCredentialStore = bootstrapCredentialStore;
this.configuredAdminPassword = configuredAdminPassword;
this.requirePasswordChange = requirePasswordChange;
}
@Override
@Transactional
public void run(ApplicationArguments args) {
AppRole roleUser = findOrCreateRole("ROLE_USER");
AppRole roleArchitect = findOrCreateRole("ROLE_ARCHITECT");
AppRole roleAdmin = findOrCreateRole("ROLE_ADMIN");
AppUser admin = userRepository.findByUsername("admin").orElse(null);
if (admin == null) {
boolean generatedBootstrapPassword = configuredAdminPassword == null
|| configuredAdminPassword.isBlank();
String effectivePassword = generatedBootstrapPassword
? generateBootstrapPassword()
: configuredAdminPassword;
admin = new AppUser();
admin.setUsername("admin");
admin.setPasswordHash(passwordEncoder.encode(effectivePassword));
admin.setEnabled(true);
admin.setDisplayName("Administrator");
admin.setRoles(Set.of(roleUser, roleArchitect, roleAdmin));
admin.setMustChangePassword(generatedBootstrapPassword || requirePasswordChange);
if (generatedBootstrapPassword) {
bootstrapCredentialStore.publish(effectivePassword);
removePublishedCredentialAfterRollback();
}
try {
userRepository.save(admin);
} catch (RuntimeException exception) {
if (generatedBootstrapPassword) {
bootstrapCredentialStore.deletePublishedCredential();
}
throw exception;
}
if (!generatedBootstrapPassword) {
log.info("Created configured administrator account "
+ "(passwordChangeRequired={}).",
requirePasswordChange);
}
} else if (!admin.isMustChangePassword()
&& passwordEncoder.matches("admin", admin.getPasswordHash())) {
// One-time migration for accounts created by historic releases with
// the removed built-in credential. Do not re-lock normal accounts on
// every application restart.
admin.setMustChangePassword(true);
userRepository.save(admin);
log.warn("Detected a legacy administrator credential and required replacement.");
}
}
private void removePublishedCredentialAfterRollback() {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
return;
}
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
if (status != TransactionSynchronization.STATUS_COMMITTED) {
bootstrapCredentialStore.deletePublishedCredential();
}
}
});
}
private AppRole findOrCreateRole(String name) {
return roleRepository.findByName(name)
.orElseGet(() -> roleRepository.save(new AppRole(name)));
}
private static String generateBootstrapPassword() {
byte[] entropy = new byte[24];
SECURE_RANDOM.nextBytes(entropy);
return Base64.getUrlEncoder().withoutPadding().encodeToString(entropy);
}
}