diff --git a/src/main/java/dev/blaauwendraad/masker/json/MaskingState.java b/src/main/java/dev/blaauwendraad/masker/json/MaskingState.java index de94ac70..be257376 100644 --- a/src/main/java/dev/blaauwendraad/masker/json/MaskingState.java +++ b/src/main/java/dev/blaauwendraad/masker/json/MaskingState.java @@ -13,6 +13,7 @@ * operation. */ final class MaskingState implements ValueMaskerContext { + private static final int INITIAL_JSONPATH_STACK_CAPACITY = 16; // an initial size of the jsonpath array private final byte[] message; private int currentIndex = 0; private final List replacementOperations = new ArrayList<>(); @@ -23,13 +24,13 @@ final class MaskingState implements ValueMaskerContext { * A stack is implemented with an array of the trie nodes that reference the end of the segment */ private KeyMatcher.TrieNode[] currentJsonPath = null; - private int currentJsonPathIndex = -1; + private int currentJsonPathHeadIndex = -1; private int currentValueStartIndex = -1; public MaskingState(byte[] message, boolean trackJsonPath) { this.message = message; if (trackJsonPath) { - currentJsonPath = new KeyMatcher.TrieNode[100]; + currentJsonPath = new KeyMatcher.TrieNode[INITIAL_JSONPATH_STACK_CAPACITY]; } } @@ -151,8 +152,8 @@ boolean jsonPathEnabled() { */ void expandCurrentJsonPath(@CheckForNull KeyMatcher.TrieNode trieNode) { if (currentJsonPath != null) { - currentJsonPath[++currentJsonPathIndex] = trieNode; - if (currentJsonPathIndex == currentJsonPath.length - 1) { + currentJsonPath[++currentJsonPathHeadIndex] = trieNode; + if (currentJsonPathHeadIndex == currentJsonPath.length - 1) { // resize currentJsonPath = Arrays.copyOf(currentJsonPath, currentJsonPath.length*2); } @@ -164,7 +165,7 @@ void expandCurrentJsonPath(@CheckForNull KeyMatcher.TrieNode trieNode) { */ void backtrackCurrentJsonPath() { if (currentJsonPath != null) { - currentJsonPath[currentJsonPathIndex--] = null; + currentJsonPath[currentJsonPathHeadIndex--] = null; } } @@ -172,8 +173,8 @@ void backtrackCurrentJsonPath() { * Returns the TrieNode that references the end of the latest segment in the current jsonpath */ public KeyMatcher.TrieNode getCurrentJsonPathNode() { - if (currentJsonPath != null && currentJsonPathIndex != -1) { - return currentJsonPath[currentJsonPathIndex]; + if (currentJsonPath != null && currentJsonPathHeadIndex != -1) { + return currentJsonPath[currentJsonPathHeadIndex]; } else { return null; } diff --git a/src/main/java/dev/blaauwendraad/masker/json/path/JsonPathParser.java b/src/main/java/dev/blaauwendraad/masker/json/path/JsonPathParser.java index 950e9eac..e715b68a 100644 --- a/src/main/java/dev/blaauwendraad/masker/json/path/JsonPathParser.java +++ b/src/main/java/dev/blaauwendraad/masker/json/path/JsonPathParser.java @@ -145,9 +145,6 @@ public void checkAmbiguity(Set jsonPaths) { } break; } - if (j == current.segments().length - 1) { // covers cases like a ("$.a.b", "$.a.b.c") combination - throw new IllegalArgumentException(String.format("Ambiguous jsonpath keys. '%s' and '%s' combination is not supported.", current, next)); - } } } } diff --git a/src/test/java/dev/blaauwendraad/masker/json/JsonMaskerTestUtil.java b/src/test/java/dev/blaauwendraad/masker/json/JsonMaskerTestUtil.java index afc56fc3..a84fe1e3 100644 --- a/src/test/java/dev/blaauwendraad/masker/json/JsonMaskerTestUtil.java +++ b/src/test/java/dev/blaauwendraad/masker/json/JsonMaskerTestUtil.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import dev.blaauwendraad.masker.json.config.JsonMaskingConfig; +import dev.blaauwendraad.masker.json.config.KeyMaskingConfig; import java.io.IOException; import java.util.ArrayList; @@ -21,24 +22,17 @@ private JsonMaskerTestUtil() { public static List getJsonMaskerTestInstancesFromFile(String fileName) throws IOException { List testInstances = new ArrayList<>(); - ArrayNode jsonArray = mapper.readValue( - JsonMaskerTestUtil.class.getClassLoader().getResource(fileName), - ArrayNode.class - ); + ArrayNode jsonArray = mapper.readValue(JsonMaskerTestUtil.class.getClassLoader().getResource(fileName), ArrayNode.class); for (JsonNode jsonNode : jsonArray) { - JsonNode jsonMaskingConfig = jsonNode.findValue("maskingConfig"); JsonMaskingConfig.Builder builder = JsonMaskingConfig.builder(); + JsonNode jsonMaskingConfig = jsonNode.findValue("maskingConfig"); if (jsonMaskingConfig != null) { applyConfig(jsonMaskingConfig, builder); } JsonMaskingConfig maskingConfig = builder.build(); var input = jsonNode.get("input").toString(); var expectedOutput = jsonNode.get("expectedOutput").toString(); - testInstances.add(new JsonMaskerTestInstance( - input, - expectedOutput, - new KeyContainsMasker(maskingConfig) - )); + testInstances.add(new JsonMaskerTestInstance(input, expectedOutput, new KeyContainsMasker(maskingConfig))); } return testInstances; } @@ -48,8 +42,20 @@ private static void applyConfig(JsonNode jsonMaskingConfig, JsonMaskingConfig.Bu String key = e.getKey(); JsonNode value = e.getValue(); switch (key) { - case "maskKeys" -> builder.maskKeys(asSet(value, JsonNode::asText)); - case "maskJsonPaths" -> builder.maskJsonPaths(asSet(value, JsonNode::asText)); + case "maskKeys" -> StreamSupport.stream(value.spliterator(), false).forEach(node -> { + if (node.isTextual()) { + builder.maskKeys(Set.of(node.asText())); + } else { + builder.maskKeys(asSet(node.get("keys"), JsonNode::asText), applyKeyConfig(node.get("keyMaskingConfig"))); + } + }); + case "maskJsonPaths" -> StreamSupport.stream(value.spliterator(), false).forEach(node -> { + if (node.isTextual()) { + builder.maskJsonPaths(Set.of(node.asText())); + } else { + builder.maskJsonPaths(asSet(node.get("keys"), JsonNode::asText), applyKeyConfig(node.get("keyMaskingConfig"))); + } + }); case "allowKeys" -> builder.allowKeys(asSet(value, JsonNode::asText)); case "allowJsonPaths" -> builder.allowJsonPaths(asSet(value, JsonNode::asText)); case "caseSensitiveTargetKeys" -> { @@ -78,6 +84,34 @@ private static void applyConfig(JsonNode jsonMaskingConfig, JsonMaskingConfig.Bu }); } + private static KeyMaskingConfig applyKeyConfig(JsonNode jsonNode) { + KeyMaskingConfig.Builder builder = KeyMaskingConfig.builder(); + jsonNode.fields().forEachRemaining(e -> { + String key = e.getKey(); + JsonNode value = e.getValue(); + switch (key) { + case "maskStringsWith" -> builder.maskStringsWith(value.textValue()); + case "maskStringCharactersWith" -> builder.maskStringCharactersWith(value.textValue()); + case "maskNumbersWith" -> { + if (value.isInt()) { + builder.maskNumbersWith(value.intValue()); + } else { + builder.maskNumbersWith(value.textValue()); + } + } + case "maskNumberDigitsWith" -> builder.maskNumberDigitsWith(value.intValue()); + case "maskBooleansWith" -> { + if (value.isBoolean()) { + builder.maskBooleansWith(value.booleanValue()); + } + builder.maskBooleansWith(value.textValue()); + } + default -> throw new IllegalArgumentException("Unknown option " + key); + } + }); + return builder.build(); + } + private static Set asSet(JsonNode value, Function mapper) { return StreamSupport.stream(value.spliterator(), false).map(mapper).collect(Collectors.toSet()); } diff --git a/src/test/java/dev/blaauwendraad/masker/json/path/JsonPathParserTest.java b/src/test/java/dev/blaauwendraad/masker/json/path/JsonPathParserTest.java index 6e71e859..baca9ef2 100644 --- a/src/test/java/dev/blaauwendraad/masker/json/path/JsonPathParserTest.java +++ b/src/test/java/dev/blaauwendraad/masker/json/path/JsonPathParserTest.java @@ -100,10 +100,7 @@ private static Stream> ambiguousJsonPaths() { Set.of("$.a.b.c.f", "$.*.*.*.u"), Set.of("$.*.b.c", "$.q.w.e", "$.*.d.f"), Set.of("$.a.b.c", "$.d.*.*.f", "$.d.*.v.f"), - Set.of("$.a.b.*.*.d", "$.a.b.*.c"), - Set.of("$.a.b.c", "$.a.b.c.*.f"), - Set.of("$.key.bbb.c", "$.key.bbb.c.d"), - Set.of("$.f.e.g", "$.n.*.m", "$", "$.a.b.c.d") + Set.of("$.a.b.*.*.d", "$.a.b.*.c") ); } @@ -115,7 +112,10 @@ private static Stream> notAmbiguousJsonPaths() { Set.of("$.a.b.*.d", "$.a.b.*.c"), Set.of("$.a.b.*.d", "$.a.b.*.c.f"), Set.of("$.ab", "$.a"), - Set.of("$.a.b", "$.a", "$.a!", "$.a.c", "$.a0.i") + Set.of("$.a.b", "$.a", "$.a!", "$.a.c", "$.a0.i"), + Set.of("$.a.b.c", "$.a.b.c.*.f"), + Set.of("$.key.bbb.c", "$.key.bbb.c.d"), + Set.of("$.f.e.g", "$.n.*.m", "$", "$.a.b.c.d") ); } diff --git a/src/test/resources/test-json-path.json b/src/test/resources/test-json-path.json index 5ba16805..125b0998 100644 --- a/src/test/resources/test-json-path.json +++ b/src/test/resources/test-json-path.json @@ -1458,5 +1458,173 @@ }, "otherKey": "***" } + }, + { + "maskingConfig": { + "maskJsonPaths": [ + "$.json.path", + { + "keys": [ + "$.json.path.further.specific", + "$.json.path.array.*.specific" + ], + "keyMaskingConfig": { + "maskStringsWith": "###" + } + } + ] + }, + "input": { + "json": { + "irrelevant": "do not mask", + "path": { + "field": "mask", + "further": { + "field": "mask", + "specific": { + "nested": "mask specifically", + "nestedObject": { + "nestedObjectField": "do not mask" + }, + "otherField": "mask specifically" + } + }, + "otherField": "mask", + "array": [ + { + "field": "mask", + "specific": "mask specifically" + } + ], + "anotherField": "mask", + "objectField": { + "nestedField": "mask", + "ignore": "mask" + } + } + } + }, + "expectedOutput": { + "json": { + "irrelevant": "do not mask", + "path": { + "field": "***", + "further": { + "field": "***", + "specific": { + "nested": "###", + "nestedObject": { + "nestedObjectField": "###" + }, + "otherField": "###" + } + }, + "otherField": "***", + "array": [ + { + "field": "***", + "specific": "###" + } + ], + "anotherField": "***", + "objectField": { + "nestedField": "***", + "ignore": "***" + } + } + } + } + }, + { + "maskingConfig": { + "maskJsonPaths": [ + "$", + { + "keys": [ + "$.specific" + ], + "keyMaskingConfig": { + "maskStringsWith": "###" + } + } + ] + }, + "input": { + "field": "mask", + "specific": "mask" + }, + "expectedOutput": { + "field": "***", + "specific": "###" + } + }, + { + "maskingConfig": { + "allowJsonPaths": [ + "$.json.path", + "$.json.path.further.specific", + "$.json.path.array.*.specific" + ] + }, + "input": { + "json": { + "irrelevant": "mask", + "path": { + "field": "do not mask", + "further": { + "field": "do not mask", + "ignore": { + "nested": "do not mask", + "nestedObject": { + "nestedObjectField": "do not mask" + }, + "otherField": "do not mask" + }, + "otherField": "do not mask", + "array": [ + { + "field": "do not mask", + "ignore": "do not mask" + } + ], + "anotherField": "do not mask", + "objectField": { + "nestedField": "do not mask", + "ignore": "do not mask" + } + } + } + } + }, + "expectedOutput": { + "json": { + "irrelevant": "***", + "path": { + "field": "do not mask", + "further": { + "field": "do not mask", + "ignore": { + "nested": "do not mask", + "nestedObject": { + "nestedObjectField": "do not mask" + }, + "otherField": "do not mask" + }, + "otherField": "do not mask", + "array": [ + { + "field": "do not mask", + "ignore": "do not mask" + } + ], + "anotherField": "do not mask", + "objectField": { + "nestedField": "do not mask", + "ignore": "do not mask" + } + } + } + } + } } ] \ No newline at end of file