ActuatorSecurityConfiguration.java
/*
* Copyright (C) 2023 B3Partners B.V.
*
* SPDX-License-Identifier: MIT
*/
package org.tailormap.api.security;
import java.lang.invoke.MethodHandles;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.transaction.annotation.Transactional;
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
@Order(1)
public class ActuatorSecurityConfiguration {
private static final Logger logger =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@Value("${management.endpoints.web.base-path}")
private String basePath;
@Value("${tailormap-api.management.hashed-password}")
private String hashedPassword;
private final UserRepository userRepository;
private final GroupRepository groupRepository;
public ActuatorSecurityConfiguration(UserRepository userRepository, GroupRepository groupRepository) {
this.userRepository = userRepository;
this.groupRepository = groupRepository;
}
@EventListener(ApplicationReadyEvent.class)
@Transactional
@DependsOn("tailormap-database-initialization")
public void createActuatorAccount() {
if (StringUtils.isBlank(hashedPassword)) {
return;
}
InternalAdminAuthentication.setInSecurityContext();
try {
// Use the group/authority name as account name
User account = userRepository.findById(Group.ACTUATOR).orElse(null);
if (account != null) {
String msg;
if (hashedPassword.equals(account.getPassword())) {
msg = "with the hashed password in";
} else {
msg = "with a different password from";
}
logger.info(
"Actuator account already exists {} the MANAGEMENT_HASHED_ACCOUNT environment variable", msg);
} else {
if (!hashedPassword.startsWith("{bcrypt}")) {
logger.error("Invalid password hash, must start with {bcrypt}");
} else {
account = new User().setUsername(Group.ACTUATOR).setPassword(hashedPassword);
account.getGroups()
.add(groupRepository.findById(Group.ACTUATOR).orElseThrow());
userRepository.save(account);
logger.info("Created {} account with hashed password for management", Group.ACTUATOR);
}
}
} finally {
InternalAdminAuthentication.clearSecurityContextAuthentication();
}
}
@Bean
public SecurityFilterChain actuatorFilterChain(HttpSecurity http, CookieCsrfTokenRepository csrfTokenRepository)
throws Exception {
http.csrf(csrf -> csrf.csrfTokenRepository(csrfTokenRepository))
.securityMatcher(basePath + "/**")
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(basePath + "/health/**")
.permitAll()
.requestMatchers(basePath + "/info")
.permitAll()
.requestMatchers(basePath + "/**")
.access((authentication, context) -> {
String remoteAddr = context.getRequest().getRemoteAddr();
if (remoteAddr.startsWith("10.")
|| remoteAddr.startsWith("192.168.")
|| remoteAddr.startsWith("172.")
|| remoteAddr.startsWith("127.")) {
return new AuthorizationDecision(true);
}
if (authentication.get() == null) {
return new AuthorizationDecision(false);
}
return new AuthorizationDecision(authentication.get().getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals(Group.ADMIN)
|| a.getAuthority().equals(Group.ACTUATOR)));
}))
.httpBasic(Customizer.withDefaults())
.addFilterAfter(
/* debug logging user making the request */ new AuditInterceptor(),
AnonymousAuthenticationFilter.class);
return http.build();
}
}