AdminAccountCreator.java
/*
* Copyright (C) 2021 B3Partners B.V.
*
* SPDX-License-Identifier: MIT
*/
package org.tailormap.api.security;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ansi.AnsiPropertySource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySourcesPropertyResolver;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StreamUtils;
import org.tailormap.api.persistence.Group;
import org.tailormap.api.persistence.User;
import org.tailormap.api.repository.GroupRepository;
import org.tailormap.api.repository.UserRepository;
@Configuration
@ConditionalOnProperty(name = "tailormap-api.security.admin.create-if-not-exists", havingValue = "true")
public class AdminAccountCreator {
private static final Logger logger =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@Value("${tailormap-api.security.admin.username}")
private String newAdminUsername;
@Value("${tailormap-api.security.admin.hashed-password}")
private String newAdminHashedPassword;
private final UserRepository userRepository;
private final GroupRepository groupRepository;
private final PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
public AdminAccountCreator(UserRepository userRepository, GroupRepository groupRepository) {
this.userRepository = userRepository;
this.groupRepository = groupRepository;
}
@EventListener(ApplicationReadyEvent.class)
@Transactional
public void postConstruct() throws IOException {
logger.trace("Checking whether an admin account exists...");
InternalAdminAuthentication.setInSecurityContext();
try {
if (!userRepository.existsByGroupsNameIn(List.of(Group.ADMIN))) {
if (isNotBlank(newAdminHashedPassword) && newAdminHashedPassword.startsWith("{bcrypt}")) {
createAdmin(newAdminUsername, newAdminHashedPassword);
logger.info(
"New admin account \"{}\" created with hashed password from environment", newAdminUsername);
} else {
// Create a new admin account with a random generated password
String password = UUID.randomUUID().toString();
createAdmin(newAdminUsername, passwordEncoder.encode(password));
// Log generated password
logger.info(getAccountBanner(newAdminUsername, password));
}
}
} finally {
InternalAdminAuthentication.clearSecurityContextAuthentication();
}
}
private void createAdmin(String username, String hashedPassword) {
User u = new User().setUsername(username).setPassword(hashedPassword);
u.getGroups().add(groupRepository.getReferenceById(Group.ADMIN));
userRepository.saveAndFlush(u);
}
private static String getAccountBanner(String username, String password) throws IOException {
String accountBanner = StreamUtils.copyToString(
new ClassPathResource("account-banner.txt").getInputStream(), StandardCharsets.US_ASCII);
MutablePropertySources sources = new MutablePropertySources();
sources.addFirst(new AnsiPropertySource("ansi", true));
sources.addFirst(new MapPropertySource(
"account",
Map.of(
"username", username,
"password", password)));
return new PropertySourcesPropertyResolver(sources).resolvePlaceholders(accountBanner);
}
}