From 788f6110d4c1f15bea8a8ac875c806c7d69b8ef1 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Wed, 8 Apr 2026 06:10:49 +0200 Subject: [PATCH] =?UTF-8?q?M7=20N2=20Logging-Sensitivit=C3=A4t=20hart=20va?= =?UTF-8?q?lidiert=20und=20produktiv=20abgesichert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PropertiesConfigurationPortAdapter.java | 38 ++++- ...ropertiesConfigurationPortAdapterTest.java | 161 ++++++++++++++++++ 2 files changed, 198 insertions(+), 1 deletion(-) diff --git a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/PropertiesConfigurationPortAdapter.java b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/PropertiesConfigurationPortAdapter.java index 6418b37..b337e2a 100644 --- a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/PropertiesConfigurationPortAdapter.java +++ b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/PropertiesConfigurationPortAdapter.java @@ -108,7 +108,7 @@ public class PropertiesConfigurationPortAdapter implements ConfigurationPort { } private StartConfiguration buildStartConfiguration(Properties props, String apiKey) { - boolean logAiSensitive = Boolean.parseBoolean(getOptionalProperty(props, "log.ai.sensitive", "false")); + boolean logAiSensitive = parseAiContentSensitivity(props); return new StartConfiguration( Paths.get(getRequiredProperty(props, "source.folder")), Paths.get(getRequiredProperty(props, "target.folder")), @@ -176,4 +176,40 @@ public class PropertiesConfigurationPortAdapter implements ConfigurationPort { throw new ConfigurationLoadingException("Invalid URI value for property: " + value, e); } } + + /** + * Parses the {@code log.ai.sensitive} configuration property with strict validation. + *

+ * This property controls whether sensitive AI-generated content (raw response, reasoning) + * may be written to log files. It must be either the literal string "true" or "false" + * (case-insensitive). Any other value is rejected as an invalid startup configuration. + *

+ * The default value (when the property is absent) is {@code false}, which is the safe default. + * + * @return {@code true} if the property is explicitly set to "true", {@code false} otherwise + * @throws ConfigurationLoadingException if the property is present but contains an invalid value + */ + private boolean parseAiContentSensitivity(Properties props) { + String value = props.getProperty("log.ai.sensitive"); + + // If absent, return safe default + if (value == null) { + return false; + } + + String trimmedValue = value.trim().toLowerCase(); + + // Only accept literal "true" or "false" + if ("true".equals(trimmedValue)) { + return true; + } else if ("false".equals(trimmedValue)) { + return false; + } else { + // Reject any other value as invalid configuration + throw new ConfigurationLoadingException( + "Invalid value for log.ai.sensitive: '" + value + "'. " + + "Must be either 'true' or 'false' (case-insensitive). " + + "Default is 'false' (sensitive content not logged)."); + } + } } \ No newline at end of file diff --git a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/PropertiesConfigurationPortAdapterTest.java b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/PropertiesConfigurationPortAdapterTest.java index 7fac74d..7224425 100644 --- a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/PropertiesConfigurationPortAdapterTest.java +++ b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/PropertiesConfigurationPortAdapterTest.java @@ -379,6 +379,167 @@ class PropertiesConfigurationPortAdapterTest { "log.ai.sensitive must be parsed as true when explicitly set to 'true'"); } + @Test + void loadConfiguration_logAiSensitiveParsedFalseWhenExplicitlySet() throws Exception { + Path configFile = createInlineConfig( + "source.folder=/tmp/source\n" + + "target.folder=/tmp/target\n" + + "sqlite.file=/tmp/db.sqlite\n" + + "api.baseUrl=https://api.example.com\n" + + "api.model=gpt-4\n" + + "api.timeoutSeconds=30\n" + + "max.retries.transient=3\n" + + "max.pages=100\n" + + "max.text.characters=50000\n" + + "prompt.template.file=/tmp/prompt.txt\n" + + "api.key=test-key\n" + + "log.ai.sensitive=false\n" + ); + + PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile); + + var config = adapter.loadConfiguration(); + + assertFalse(config.logAiSensitive(), + "log.ai.sensitive must be parsed as false when explicitly set to 'false'"); + } + + @Test + void loadConfiguration_logAiSensitiveHandlesCaseInsensitiveTrue() throws Exception { + Path configFile = createInlineConfig( + "source.folder=/tmp/source\n" + + "target.folder=/tmp/target\n" + + "sqlite.file=/tmp/db.sqlite\n" + + "api.baseUrl=https://api.example.com\n" + + "api.model=gpt-4\n" + + "api.timeoutSeconds=30\n" + + "max.retries.transient=3\n" + + "max.pages=100\n" + + "max.text.characters=50000\n" + + "prompt.template.file=/tmp/prompt.txt\n" + + "api.key=test-key\n" + + "log.ai.sensitive=TRUE\n" + ); + + PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile); + + var config = adapter.loadConfiguration(); + + assertTrue(config.logAiSensitive(), + "log.ai.sensitive must handle case-insensitive 'TRUE'"); + } + + @Test + void loadConfiguration_logAiSensitiveHandlesCaseInsensitiveFalse() throws Exception { + Path configFile = createInlineConfig( + "source.folder=/tmp/source\n" + + "target.folder=/tmp/target\n" + + "sqlite.file=/tmp/db.sqlite\n" + + "api.baseUrl=https://api.example.com\n" + + "api.model=gpt-4\n" + + "api.timeoutSeconds=30\n" + + "max.retries.transient=3\n" + + "max.pages=100\n" + + "max.text.characters=50000\n" + + "prompt.template.file=/tmp/prompt.txt\n" + + "api.key=test-key\n" + + "log.ai.sensitive=FALSE\n" + ); + + PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile); + + var config = adapter.loadConfiguration(); + + assertFalse(config.logAiSensitive(), + "log.ai.sensitive must handle case-insensitive 'FALSE'"); + } + + @Test + void loadConfiguration_throwsConfigurationLoadingExceptionForInvalidLogAiSensitive() throws Exception { + Path configFile = createInlineConfig( + "source.folder=/tmp/source\n" + + "target.folder=/tmp/target\n" + + "sqlite.file=/tmp/db.sqlite\n" + + "api.baseUrl=https://api.example.com\n" + + "api.model=gpt-4\n" + + "api.timeoutSeconds=30\n" + + "max.retries.transient=3\n" + + "max.pages=100\n" + + "max.text.characters=50000\n" + + "prompt.template.file=/tmp/prompt.txt\n" + + "api.key=test-key\n" + + "log.ai.sensitive=maybe\n" + ); + + PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile); + + ConfigurationLoadingException exception = assertThrows( + ConfigurationLoadingException.class, + () -> adapter.loadConfiguration() + ); + + assertTrue(exception.getMessage().contains("Invalid value for log.ai.sensitive"), + "Invalid log.ai.sensitive value should throw ConfigurationLoadingException"); + assertTrue(exception.getMessage().contains("'maybe'"), + "Error message should include the invalid value"); + } + + @Test + void loadConfiguration_throwsConfigurationLoadingExceptionForInvalidLogAiSensitiveYes() throws Exception { + Path configFile = createInlineConfig( + "source.folder=/tmp/source\n" + + "target.folder=/tmp/target\n" + + "sqlite.file=/tmp/db.sqlite\n" + + "api.baseUrl=https://api.example.com\n" + + "api.model=gpt-4\n" + + "api.timeoutSeconds=30\n" + + "max.retries.transient=3\n" + + "max.pages=100\n" + + "max.text.characters=50000\n" + + "prompt.template.file=/tmp/prompt.txt\n" + + "api.key=test-key\n" + + "log.ai.sensitive=yes\n" + ); + + PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile); + + ConfigurationLoadingException exception = assertThrows( + ConfigurationLoadingException.class, + () -> adapter.loadConfiguration() + ); + + assertTrue(exception.getMessage().contains("Invalid value for log.ai.sensitive"), + "Invalid log.ai.sensitive value 'yes' should throw ConfigurationLoadingException"); + } + + @Test + void loadConfiguration_throwsConfigurationLoadingExceptionForInvalidLogAiSensitive1() throws Exception { + Path configFile = createInlineConfig( + "source.folder=/tmp/source\n" + + "target.folder=/tmp/target\n" + + "sqlite.file=/tmp/db.sqlite\n" + + "api.baseUrl=https://api.example.com\n" + + "api.model=gpt-4\n" + + "api.timeoutSeconds=30\n" + + "max.retries.transient=3\n" + + "max.pages=100\n" + + "max.text.characters=50000\n" + + "prompt.template.file=/tmp/prompt.txt\n" + + "api.key=test-key\n" + + "log.ai.sensitive=1\n" + ); + + PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile); + + ConfigurationLoadingException exception = assertThrows( + ConfigurationLoadingException.class, + () -> adapter.loadConfiguration() + ); + + assertTrue(exception.getMessage().contains("Invalid value for log.ai.sensitive"), + "Invalid log.ai.sensitive value '1' should throw ConfigurationLoadingException"); + } + private Path createConfigFile(String resourceName) throws Exception { Path sourceResource = Path.of("src/test/resources", resourceName); Path targetConfigFile = tempDir.resolve("application.properties");