Skip to content

Commit

Permalink
Allow overriding masking config for more specific keys for jsonpath (#…
Browse files Browse the repository at this point in the history
…119)

* Allow overriding masking config for more specific keys for jsonpath
  • Loading branch information
donavdey authored Apr 5, 2024
1 parent 82c901b commit 12324ff
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 27 deletions.
15 changes: 8 additions & 7 deletions src/main/java/dev/blaauwendraad/masker/json/MaskingState.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReplacementOperation> replacementOperations = new ArrayList<>();
Expand All @@ -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];
}
}

Expand Down Expand Up @@ -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);
}
Expand All @@ -164,16 +165,16 @@ void expandCurrentJsonPath(@CheckForNull KeyMatcher.TrieNode trieNode) {
*/
void backtrackCurrentJsonPath() {
if (currentJsonPath != null) {
currentJsonPath[currentJsonPathIndex--] = null;
currentJsonPath[currentJsonPathHeadIndex--] = null;
}
}

/**
* 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,6 @@ public void checkAmbiguity(Set<JsonPath> 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));
}
}
}
}
Expand Down
58 changes: 46 additions & 12 deletions src/test/java/dev/blaauwendraad/masker/json/JsonMaskerTestUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,24 +22,17 @@ private JsonMaskerTestUtil() {

public static List<JsonMaskerTestInstance> getJsonMaskerTestInstancesFromFile(String fileName) throws IOException {
List<JsonMaskerTestInstance> 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;
}
Expand All @@ -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" -> {
Expand Down Expand Up @@ -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 <T> Set<T> asSet(JsonNode value, Function<JsonNode, T> mapper) {
return StreamSupport.stream(value.spliterator(), false).map(mapper).collect(Collectors.toSet());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,7 @@ private static Stream<Set<String>> 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")
);
}

Expand All @@ -115,7 +112,10 @@ private static Stream<Set<String>> 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")
);
}

Expand Down
168 changes: 168 additions & 0 deletions src/test/resources/test-json-path.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}
}
}
]

0 comments on commit 12324ff

Please sign in to comment.