M7 N2 Logging-Sensitivität hart validiert und produktiv abgesichert
This commit is contained in:
@@ -108,7 +108,7 @@ public class PropertiesConfigurationPortAdapter implements ConfigurationPort {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private StartConfiguration buildStartConfiguration(Properties props, String apiKey) {
|
private StartConfiguration buildStartConfiguration(Properties props, String apiKey) {
|
||||||
boolean logAiSensitive = Boolean.parseBoolean(getOptionalProperty(props, "log.ai.sensitive", "false"));
|
boolean logAiSensitive = parseAiContentSensitivity(props);
|
||||||
return new StartConfiguration(
|
return new StartConfiguration(
|
||||||
Paths.get(getRequiredProperty(props, "source.folder")),
|
Paths.get(getRequiredProperty(props, "source.folder")),
|
||||||
Paths.get(getRequiredProperty(props, "target.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);
|
throw new ConfigurationLoadingException("Invalid URI value for property: " + value, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the {@code log.ai.sensitive} configuration property with strict validation.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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).");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -379,6 +379,167 @@ class PropertiesConfigurationPortAdapterTest {
|
|||||||
"log.ai.sensitive must be parsed as true when explicitly set to 'true'");
|
"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 {
|
private Path createConfigFile(String resourceName) throws Exception {
|
||||||
Path sourceResource = Path.of("src/test/resources", resourceName);
|
Path sourceResource = Path.of("src/test/resources", resourceName);
|
||||||
Path targetConfigFile = tempDir.resolve("application.properties");
|
Path targetConfigFile = tempDir.resolve("application.properties");
|
||||||
|
|||||||
Reference in New Issue
Block a user