diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/admin/SensitiveKeysMerger.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/admin/SensitiveKeysMerger.java new file mode 100644 index 00000000..62cd2af7 --- /dev/null +++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/admin/SensitiveKeysMerger.java @@ -0,0 +1,45 @@ +package com.cameleer3.server.core.admin; + +import java.util.ArrayList; +import java.util.List; +import java.util.TreeSet; + +/** + * Merges global (enforced) sensitive keys with per-app additions. + * Union-only: per-app can add keys, never remove global keys. + * Case-insensitive deduplication, preserves first-seen casing. + */ +public final class SensitiveKeysMerger { + + private SensitiveKeysMerger() {} + + /** + * @param global enforced global keys (null = not configured) + * @param perApp per-app additional keys (null = none) + * @return merged list, or null if both inputs are null + */ + public static List merge(List global, List perApp) { + if (global == null && perApp == null) { + return null; + } + + TreeSet seen = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + List result = new ArrayList<>(); + + if (global != null) { + for (String key : global) { + if (seen.add(key)) { + result.add(key); + } + } + } + if (perApp != null) { + for (String key : perApp) { + if (seen.add(key)) { + result.add(key); + } + } + } + return result; + } +} diff --git a/cameleer3-server-core/src/test/java/com/cameleer3/server/core/admin/SensitiveKeysMergerTest.java b/cameleer3-server-core/src/test/java/com/cameleer3/server/core/admin/SensitiveKeysMergerTest.java new file mode 100644 index 00000000..b758a31a --- /dev/null +++ b/cameleer3-server-core/src/test/java/com/cameleer3/server/core/admin/SensitiveKeysMergerTest.java @@ -0,0 +1,66 @@ +package com.cameleer3.server.core.admin; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class SensitiveKeysMergerTest { + + @Test + void bothNull_returnsNull() { + assertNull(SensitiveKeysMerger.merge(null, null)); + } + + @Test + void globalOnly_returnsGlobal() { + List result = SensitiveKeysMerger.merge( + List.of("Authorization", "Cookie"), null); + assertEquals(List.of("Authorization", "Cookie"), result); + } + + @Test + void perAppOnly_returnsPerApp() { + List result = SensitiveKeysMerger.merge( + null, List.of("X-Internal-*")); + assertEquals(List.of("X-Internal-*"), result); + } + + @Test + void union_mergesWithoutDuplicates() { + List result = SensitiveKeysMerger.merge( + List.of("Authorization", "Cookie"), + List.of("Cookie", "X-Custom")); + assertEquals(List.of("Authorization", "Cookie", "X-Custom"), result); + } + + @Test + void caseInsensitiveDedup_preservesFirstCasing() { + List result = SensitiveKeysMerger.merge( + List.of("Authorization"), + List.of("authorization", "AUTHORIZATION")); + assertEquals(List.of("Authorization"), result); + } + + @Test + void emptyGlobal_returnsEmptyList() { + List result = SensitiveKeysMerger.merge(List.of(), null); + assertEquals(List.of(), result); + } + + @Test + void emptyGlobalWithPerApp_returnsPerApp() { + List result = SensitiveKeysMerger.merge( + List.of(), List.of("X-Custom")); + assertEquals(List.of("X-Custom"), result); + } + + @Test + void globPatterns_preserved() { + List result = SensitiveKeysMerger.merge( + List.of("*password*", "*secret*"), + List.of("X-Internal-*")); + assertEquals(List.of("*password*", "*secret*", "X-Internal-*"), result); + } +}