feature/run-office-module-without-booking-and-hosting #148
@ -0,0 +1,105 @@
|
|||||||
|
package net.hostsharing.hsadminng.config;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.actuate.endpoint.SanitizableData;
|
||||||
|
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
// HOWTO: exclude sensitive values, like passwords and other secrets, from being show by actuator endpoints:
|
||||||
|
// either use: add your custom keys to management.endpoint.additionalKeysToSanitize,
|
||||||
|
// or, if you need more heuristics, amend this code down here.
|
||||||
|
@Component
|
||||||
|
public class ActuatorSanitizer implements SanitizingFunction {
|
||||||
|
|
||||||
|
private static final String[] REGEX_PARTS = {"*", "$", "^", "+"};
|
||||||
|
|
||||||
|
private static final Set<String> DEFAULT_KEYS_TO_SANITIZE = Set.of(
|
||||||
|
"password", "secret", "token", ".*credentials.*", "vcap_services", "^vcap\\.services.*$", "sun.java.command", "^spring[._]application[._]json$"
|
||||||
|
);
|
||||||
|
|
||||||
|
private static final Set<String> URI_USERINFO_KEYS = Set.of(
|
||||||
|
"uri", "uris", "url", "urls", "address", "addresses"
|
||||||
|
);
|
||||||
|
|
||||||
|
private static final Pattern URI_USERINFO_PATTERN = Pattern.compile("^\\[?[A-Za-z][A-Za-z0-9\\+\\.\\-]+://.+:(.*)@.+$");
|
||||||
|
|
||||||
|
private final List<Pattern> keysToSanitize = new ArrayList<>();
|
||||||
|
|
||||||
|
public ActuatorSanitizer(@Value("${management.endpoint.additionalKeysToSanitize:}") final List<String> additionalKeysToSanitize) {
|
||||||
|
addKeysToSanitize(DEFAULT_KEYS_TO_SANITIZE);
|
||||||
|
addKeysToSanitize(URI_USERINFO_KEYS);
|
||||||
|
addKeysToSanitize(additionalKeysToSanitize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SanitizableData apply(final SanitizableData data) {
|
||||||
|
if (data.getValue() == null) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final Pattern pattern : keysToSanitize) {
|
||||||
|
if (pattern.matcher(data.getKey()).matches()) {
|
||||||
|
if (keyIsUriWithUserInfo(pattern)) {
|
||||||
|
return data.withValue(sanitizeUris(data.getValue().toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.withValue(SanitizableData.SANITIZED_VALUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addKeysToSanitize(final Collection<String> keysToSanitize) {
|
||||||
|
for (final String key : keysToSanitize) {
|
||||||
|
this.keysToSanitize.add(getPattern(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pattern getPattern(final String value) {
|
||||||
|
if (isRegex(value)) {
|
||||||
|
return Pattern.compile(value, Pattern.CASE_INSENSITIVE);
|
||||||
|
}
|
||||||
|
return Pattern.compile(".*" + value + "$", Pattern.CASE_INSENSITIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRegex(final String value) {
|
||||||
|
for (final String part : REGEX_PARTS) {
|
||||||
|
if (value.contains(part)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean keyIsUriWithUserInfo(final Pattern pattern) {
|
||||||
|
for (String uriKey : URI_USERINFO_KEYS) {
|
||||||
|
if (pattern.matcher(uriKey).matches()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object sanitizeUris(final String value) {
|
||||||
|
return Arrays.stream(value.split(",")).map(this::sanitizeUri).collect(Collectors.joining(","));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sanitizeUri(final String value) {
|
||||||
|
final var matcher = URI_USERINFO_PATTERN.matcher(value);
|
||||||
|
final var password = matcher.matches() ? matcher.group(1) : null;
|
||||||
|
if (password != null) {
|
||||||
|
return StringUtils.replace(value, ":" + password + "@", ":" + SanitizableData.SANITIZED_VALUE + "@");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
@ -8,8 +8,23 @@ management:
|
|||||||
endpoints:
|
endpoints:
|
||||||
web:
|
web:
|
||||||
exposure:
|
exposure:
|
||||||
# HOWTO: view _clickable_ Spring Actuator (Micrometer) Metrics endpoints: http://localhost:8081/actuator/metric-links
|
# HOWTO: view _clickable_ Spring Actuator (Micrometer) Metrics endpoints:
|
||||||
include: info, health, metrics, metric-links, mappings, openapi, swaggerui
|
# http://localhost:8081/actuator/metric-links
|
||||||
|
|
||||||
|
# HOWTO: view all configured endpoints of the running application:
|
||||||
|
# http://localhost:8081/actuator/mappings
|
||||||
|
|
||||||
|
# HOWTO: view the effective application configuration properties:
|
||||||
|
# http://localhost:8081/actuator/configprops
|
||||||
|
|
||||||
|
include: info, health, metrics, metric-links, mappings, openapi, swaggerui, configprops, env
|
||||||
|
endpoint:
|
||||||
|
env:
|
||||||
|
# TODO.spec: check this, maybe set to when_authorized?
|
||||||
|
show-values: always
|
||||||
|
configprops:
|
||||||
|
# TODO.spec: check this, maybe set to when_authorized?
|
||||||
|
show-values: always
|
||||||
observations:
|
observations:
|
||||||
annotations:
|
annotations:
|
||||||
enabled: true
|
enabled: true
|
||||||
@ -18,7 +33,7 @@ spring:
|
|||||||
datasource:
|
datasource:
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
password: password
|
password: password
|
||||||
url: jdbc:postgresql://localhost:5432/postgres
|
url: ${HSADMINNG_POSTGRES_JDBC_URL}
|
||||||
username: postgres
|
username: postgres
|
||||||
|
|
||||||
sql:
|
sql:
|
||||||
@ -30,13 +45,13 @@ spring:
|
|||||||
hibernate:
|
hibernate:
|
||||||
dialect: net.hostsharing.hsadminng.config.PostgresCustomDialect
|
dialect: net.hostsharing.hsadminng.config.PostgresCustomDialect
|
||||||
|
|
||||||
|
liquibase:
|
||||||
|
contexts: dev,office
|
||||||
|
|
||||||
# keep this in sync with test/.../application.yml
|
# keep this in sync with test/.../application.yml
|
||||||
springdoc:
|
springdoc:
|
||||||
use-management-port: true
|
use-management-port: true
|
||||||
|
|
||||||
liquibase:
|
|
||||||
contexts: dev
|
|
||||||
|
|
||||||
hsadminng:
|
hsadminng:
|
||||||
postgres:
|
postgres:
|
||||||
leakproof:
|
leakproof:
|
||||||
@ -51,3 +66,33 @@ metrics:
|
|||||||
server:
|
server:
|
||||||
requests: true
|
requests: true
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# the 'dev-all' profile automatically creates the PgSql users and runs only the office module
|
||||||
|
spring:
|
||||||
|
config:
|
||||||
|
activate:
|
||||||
|
on-profile: dev-all
|
||||||
|
liquibase:
|
||||||
|
contexts: dev,office,booking,hosting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# the 'dev-office' profile automatically creates the PgSql users and runs only the office module
|
||||||
|
spring:
|
||||||
|
config:
|
||||||
|
activate:
|
||||||
|
on-profile: dev-office
|
||||||
|
liquibase:
|
||||||
|
contexts: dev,office
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# the 'prod-office' profile expects that the PgSql users are already created and runs only the office module
|
||||||
|
spring:
|
||||||
|
config:
|
||||||
|
activate:
|
||||||
|
on-profile: prod-office
|
||||||
|
liquibase:
|
||||||
|
contexts: office
|
||||||
|
|
||||||
|
@ -0,0 +1,95 @@
|
|||||||
|
package net.hostsharing.hsadminng.config;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.actuate.endpoint.SanitizableData;
|
||||||
|
import org.springframework.core.env.PropertySource;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class ActuatorSanitizerTest {
|
||||||
|
|
||||||
|
private ActuatorSanitizer actuatorSanitizer;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// Initialize with additional keys for testing
|
||||||
|
final var additionalKeys = List.of("customSecret", "^custom[._]regex.*$");
|
||||||
|
actuatorSanitizer = new ActuatorSanitizer(additionalKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSanitizesDefaultKeys() {
|
||||||
|
final var data = createSanitizableData("password", "my-secret-password");
|
||||||
|
final var sanitizedData = actuatorSanitizer.apply(data);
|
||||||
|
|
||||||
|
assertThat(sanitizedData.getValue()).isEqualTo(SanitizableData.SANITIZED_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSanitizesCustomKey() {
|
||||||
|
final var data = createSanitizableData("customSecret", "my-custom-secret");
|
||||||
|
final var sanitizedData = actuatorSanitizer.apply(data);
|
||||||
|
|
||||||
|
assertThat(sanitizedData.getValue()).isEqualTo(SanitizableData.SANITIZED_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSanitizesCustomRegexKey() {
|
||||||
|
final var data = createSanitizableData("custom.regex.key", "my-custom-regex-value");
|
||||||
|
final var sanitizedData = actuatorSanitizer.apply(data);
|
||||||
|
|
||||||
|
assertThat(sanitizedData.getValue()).isEqualTo(SanitizableData.SANITIZED_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSanitizesUriWithUserInfo() {
|
||||||
|
final var data = createSanitizableData("uri", "http://user:password@host.com");
|
||||||
|
final var sanitizedData = actuatorSanitizer.apply(data);
|
||||||
|
|
||||||
|
assertThat(sanitizedData.getValue()).isEqualTo("http://user:******@host.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSanitizeIrrelevantKey() {
|
||||||
|
final var data = createSanitizableData("irrelevantKey", "non-sensitive-value");
|
||||||
|
final var sanitizedData = actuatorSanitizer.apply(data);
|
||||||
|
|
||||||
|
assertThat(sanitizedData.getValue()).isEqualTo("non-sensitive-value");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHandlesNullValue() {
|
||||||
|
final var data = createSanitizableData("password", null);
|
||||||
|
final var sanitizedData = actuatorSanitizer.apply(data);
|
||||||
|
|
||||||
|
assertThat(sanitizedData.getValue()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHandlesMultipleUris() {
|
||||||
|
final var data = createSanitizableData(
|
||||||
|
"uris",
|
||||||
|
"http://user1:password1@host1.com,http://user2:geheim@host2.com,http://user2@host2.com");
|
||||||
|
final var sanitizedData = actuatorSanitizer.apply(data);
|
||||||
|
|
||||||
|
assertThat(sanitizedData.getValue()).isEqualTo(
|
||||||
|
"http://user1:******@host1.com,http://user2:******@host2.com,http://user2@host2.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to create a SanitizableData instance for testing.
|
||||||
|
*/
|
||||||
|
private SanitizableData createSanitizableData(final String key, final String value) {
|
||||||
|
final var dummyPropertySource = new PropertySource<>("testSource") {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getProperty(String name) {
|
||||||
|
return null; // No real property resolution needed for this test
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return new SanitizableData(dummyPropertySource, key, value);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user