feature/run-office-module-without-booking-and-hosting #148

Merged
hsh-michaelhoennig merged 19 commits from feature/run-office-module-without-booking-and-hosting into master 2025-01-21 14:36:53 +01:00
3 changed files with 251 additions and 6 deletions
Showing only changes of commit 2d1b5ce046 - Show all commits

View File

@ -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;
}
}

View File

@ -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

View File

@ -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);
}
}