diff --git a/src/main/java/org/akhq/utils/JsonMaskByDefaultMasker.java b/src/main/java/org/akhq/utils/JsonMaskByDefaultMasker.java index b93ff5c46..ae2c2f27b 100644 --- a/src/main/java/org/akhq/utils/JsonMaskByDefaultMasker.java +++ b/src/main/java/org/akhq/utils/JsonMaskByDefaultMasker.java @@ -1,8 +1,6 @@ package org.akhq.utils; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.google.gson.*; import io.micronaut.context.annotation.Requires; import jakarta.inject.Singleton; import lombok.SneakyThrows; @@ -10,64 +8,116 @@ import org.akhq.configs.JsonMaskingFilter; import org.akhq.models.Record; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Singleton @Requires(property = "akhq.security.data-masking.mode", value = "json_mask_by_default") public class JsonMaskByDefaultMasker implements Masker { - private final List jsonMaskingFilters; + private final Map> topicToKeysMap; private final String jsonMaskReplacement; + private static final String NON_JSON_MESSAGE = "This record is unable to be masked as it is not a structured object. This record is unavailable to view due to safety measures from json_mask_by_default to not leak sensitive data."; + private static final String ERROR_MESSAGE = "An exception occurred during an attempt to mask this record. This record is unavailable to view due to safety measures from json_mask_by_default to not leak sensitive data. Please contact akhq administrator."; public JsonMaskByDefaultMasker(DataMasking dataMasking) { - this.jsonMaskingFilters = dataMasking.getJsonFilters(); this.jsonMaskReplacement = dataMasking.getJsonMaskReplacement(); + this.topicToKeysMap = buildTopicKeysMap(dataMasking); + } + + private Map> buildTopicKeysMap(DataMasking dataMasking) { + return dataMasking.getJsonFilters().stream() + .collect(Collectors.toMap( + JsonMaskingFilter::getTopic, + JsonMaskingFilter::getKeys, + (a, b) -> a, + HashMap::new + )); } public Record maskRecord(Record record) { + if (!isJson(record)) { + return createNonJsonRecord(record); + } + try { - if(isJson(record)) { - return jsonMaskingFilters - .stream() - .filter(jsonMaskingFilter -> record.getTopic().getName().equalsIgnoreCase(jsonMaskingFilter.getTopic())) - .findFirst() - .map(filter -> applyMasking(record, filter.getKeys())) - .orElseGet(() -> applyMasking(record, List.of())); - } else { - record.setValue("This record is unable to be masked as it is not a structured object. This record is unavailable to view due to safety measures from json_mask_by_default to not leak sensitive data. Please contact akhq administrator."); - } + List unmaskedKeys = getUnmaskedKeysForTopic(record.getTopic().getName()); + return applyMasking(record, unmaskedKeys); } catch (Exception e) { - LOG.error("Error masking record at topic {}, partition {}, offset {} due to {}", record.getTopic(), record.getPartition(), record.getOffset(), e.getMessage()); - record.setValue("An exception occurred during an attempt to mask this record. This record is unavailable to view due to safety measures from json_mask_by_default to not leak sensitive data. Please contact akhq administrator."); + logMaskingError(record, e); + return createErrorRecord(record); } + } + + private List getUnmaskedKeysForTopic(String topic) { + return topicToKeysMap.getOrDefault(topic.toLowerCase(), Collections.emptyList()); + } + + private Record createNonJsonRecord(Record record) { + record.setValue(NON_JSON_MESSAGE); + return record; + } + + private Record createErrorRecord(Record record) { + record.setValue(ERROR_MESSAGE); return record; } + private void logMaskingError(Record record, Exception e) { + LOG.error("Error masking record at topic {}, partition {}, offset {} due to {}", + record.getTopic(), record.getPartition(), record.getOffset(), e.getMessage()); + } + @SneakyThrows - private Record applyMasking(Record record, List keys) { - JsonObject jsonElement = JsonParser.parseString(record.getValue()).getAsJsonObject(); - maskAllExcept(jsonElement, keys); - record.setValue(jsonElement.toString()); + private Record applyMasking(Record record, List unmaskedKeys) { + JsonObject root = JsonParser.parseString(record.getValue()).getAsJsonObject(); + maskJson(root, "", unmaskedKeys); + record.setValue(root.toString()); return record; } - private void maskAllExcept(JsonObject jsonElement, List keys) { - maskAllExcept("", jsonElement, keys); + private void maskJson(JsonElement element, String path, List unmaskedKeys) { + if (element.isJsonObject()) { + maskJsonObject(element.getAsJsonObject(), path, unmaskedKeys); + } else if (element.isJsonArray()) { + maskJsonArray(element.getAsJsonArray(), path, unmaskedKeys); + } + } + + private void maskJsonObject(JsonObject obj, String path, List unmaskedKeys) { + for (Map.Entry entry : obj.entrySet()) { + String newPath = path + entry.getKey(); + JsonElement value = entry.getValue(); + + if (shouldMaskPrimitive(value, newPath, unmaskedKeys)) { + entry.setValue(new JsonPrimitive(jsonMaskReplacement)); + } else if (isNestedStructure(value)) { + maskJson(value, newPath + ".", unmaskedKeys); + } + } } - private void maskAllExcept(String currentKey, JsonObject node, List keys) { - if (node.isJsonObject()) { - JsonObject objectNode = node.getAsJsonObject(); - for(Map.Entry entry : objectNode.entrySet()) { - if(entry.getValue().isJsonObject()) { - maskAllExcept(currentKey + entry.getKey() + ".", entry.getValue().getAsJsonObject(), keys); - } else { - if(!keys.contains(currentKey + entry.getKey())) { - objectNode.addProperty(entry.getKey(), jsonMaskReplacement); - } - } + private void maskJsonArray(JsonArray array, String path, List unmaskedKeys) { + boolean shouldMask = !unmaskedKeys.contains(path.substring(0, path.length() - 1)); + + for (int i = 0; i < array.size(); i++) { + JsonElement arrayElement = array.get(i); + if (arrayElement.isJsonPrimitive() && shouldMask) { + array.set(i, new JsonPrimitive(jsonMaskReplacement)); + } else if (isNestedStructure(arrayElement)) { + maskJson(arrayElement, path, unmaskedKeys); } } } + + private boolean shouldMaskPrimitive(JsonElement value, String path, List unmaskedKeys) { + return value.isJsonPrimitive() && !unmaskedKeys.contains(path); + } + + private boolean isNestedStructure(JsonElement value) { + return value.isJsonObject() || value.isJsonArray(); + } } diff --git a/src/main/java/org/akhq/utils/JsonShowByDefaultMasker.java b/src/main/java/org/akhq/utils/JsonShowByDefaultMasker.java index b384af661..2951f0dce 100644 --- a/src/main/java/org/akhq/utils/JsonShowByDefaultMasker.java +++ b/src/main/java/org/akhq/utils/JsonShowByDefaultMasker.java @@ -1,8 +1,6 @@ package org.akhq.utils; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.google.gson.*; import io.micronaut.context.annotation.Requires; import jakarta.inject.Singleton; import lombok.SneakyThrows; @@ -10,56 +8,115 @@ import org.akhq.configs.JsonMaskingFilter; import org.akhq.models.Record; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Singleton @Requires(property = "akhq.security.data-masking.mode", value = "json_show_by_default") public class JsonShowByDefaultMasker implements Masker { - private final List jsonMaskingFilters; + private final Map> topicToKeysMap; private final String jsonMaskReplacement; + private static final String ERROR_MESSAGE = "Error masking record"; public JsonShowByDefaultMasker(DataMasking dataMasking) { - this.jsonMaskingFilters = dataMasking.getJsonFilters(); this.jsonMaskReplacement = dataMasking.getJsonMaskReplacement(); + this.topicToKeysMap = buildTopicKeysMap(dataMasking); + } + + private Map> buildTopicKeysMap(DataMasking dataMasking) { + return dataMasking.getJsonFilters().stream() + .collect(Collectors.toMap( + JsonMaskingFilter::getTopic, + JsonMaskingFilter::getKeys, + (a, b) -> a, + HashMap::new + )); } public Record maskRecord(Record record) { try { - if(isJson(record)) { - return jsonMaskingFilters - .stream() - .filter(jsonMaskingFilter -> record.getTopic().getName().equalsIgnoreCase(jsonMaskingFilter.getTopic())) - .findFirst() - .map(filter -> applyMasking(record, filter.getKeys())) - .orElse(record); + if (!isJson(record)) { + return record; } + return maskJsonRecord(record); } catch (Exception e) { - LOG.error("Error masking record", e); + LOG.error(ERROR_MESSAGE, e); + return record; } - return record; + } + + private Record maskJsonRecord(Record record) { + String topic = record.getTopic().getName().toLowerCase(); + List maskedKeys = topicToKeysMap.get(topic); + return maskedKeys != null ? applyMasking(record, maskedKeys) : record; } @SneakyThrows - private Record applyMasking(Record record, List keys) { - JsonObject jsonElement = JsonParser.parseString(record.getValue()).getAsJsonObject(); - for(String key : keys) { - maskField(jsonElement, key.split("\\."), 0); - } - record.setValue(jsonElement.toString()); + private Record applyMasking(Record record, List maskedKeys) { + JsonObject root = JsonParser.parseString(record.getValue()).getAsJsonObject(); + String[][] pathArrays = preProcessPaths(maskedKeys); + maskPaths(root, pathArrays); + record.setValue(root.toString()); return record; } - private void maskField(JsonObject node, String[] keys, int index) { - if (index == keys.length - 1) { - if (node.has(keys[index])) { - node.addProperty(keys[index], jsonMaskReplacement); - } + private String[][] preProcessPaths(List maskedKeys) { + return maskedKeys.stream() + .map(key -> key.split("\\.")) + .toArray(String[][]::new); + } + + private void maskPaths(JsonObject root, String[][] pathArrays) { + for (String[] path : pathArrays) { + maskJson(root, path, 0); + } + } + + private void maskJson(JsonElement element, String[] path, int index) { + if (index == path.length) return; + + String currentKey = path[index]; + if (element.isJsonObject()) { + handleJsonObject(element.getAsJsonObject(), path, index, currentKey); + } else if (element.isJsonArray()) { + handleJsonArray(element.getAsJsonArray(), path, index); + } + } + + private void handleJsonObject(JsonObject obj, String[] path, int index, String currentKey) { + if (!obj.has(currentKey)) return; + + if (index == path.length - 1) { + maskTargetElement(obj, currentKey); } else { - JsonElement childNode = node.get(keys[index]); - if (childNode != null && childNode.isJsonObject()) { - maskField(childNode.getAsJsonObject(), keys, index + 1); + maskJson(obj.get(currentKey), path, index + 1); + } + } + + private void handleJsonArray(JsonArray array, String[] path, int index) { + for (int i = 0; i < array.size(); i++) { + JsonElement arrayElement = array.get(i); + if (arrayElement.isJsonObject()) { + maskJson(arrayElement, path, index); } } } -} \ No newline at end of file + + private void maskTargetElement(JsonObject obj, String currentKey) { + JsonElement target = obj.get(currentKey); + if (target.isJsonArray()) { + maskArrayElement(target.getAsJsonArray()); + } else { + obj.addProperty(currentKey, jsonMaskReplacement); + } + } + + private void maskArrayElement(JsonArray array) { + for (int i = 0; i < array.size(); i++) { + array.set(i, new JsonPrimitive(jsonMaskReplacement)); + } + } +} diff --git a/src/test/java/org/akhq/utils/JsonMaskByDefaultMaskerTest.java b/src/test/java/org/akhq/utils/JsonMaskByDefaultMaskerTest.java index edff0288a..1ebaf7662 100644 --- a/src/test/java/org/akhq/utils/JsonMaskByDefaultMaskerTest.java +++ b/src/test/java/org/akhq/utils/JsonMaskByDefaultMaskerTest.java @@ -4,183 +4,85 @@ import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import org.akhq.models.Record; +import lombok.Getter; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.mockito.Mockito; -import static org.junit.jupiter.api.Assertions.*; +import static org.akhq.utils.MaskerTestHelper.sampleRecord; +import static org.junit.jupiter.api.Assertions.assertEquals; +@Getter @MicronautTest(environments = "json-mask-by-default-data-masking") -class JsonMaskByDefaultMaskerTest extends MaskerTestHelper { +class JsonMaskByDefaultMaskerTest implements JsonMaskerTest { @Inject - Masker masker; + public JsonMaskByDefaultMasker masker; @Test - void shouldUseJsonMaskByDefaultMasker() { - assertInstanceOf(JsonMaskByDefaultMasker.class, masker); - } - - @Test - void shouldMaskRecordValue() { - Record record = sampleRecord( - "users", - "some-key", - sampleValue() - ); - - Record maskedRecord = masker.maskRecord(record); - - assertEquals( - "{\"specialId\":123,\"status\":\"ACTIVE\",\"name\":\"xxxx\",\"dateOfBirth\":\"xxxx\",\"address\":{\"firstLine\":\"xxxx\",\"town\":\"xxxx\",\"country\":\"United Kingdom\"},\"metadata\":{\"trusted\":true,\"rating\":\"10\",\"notes\":\"xxxx\"}}", - maskedRecord.getValue() - ); - } - - @Test - void forUndefinedTopicShouldDefaultMaskAllValues() { - Record record = sampleRecord( - "different-topic", - "some-key", - sampleValue() - ); - - Record maskedRecord = masker.maskRecord(record); - - assertEquals( - "{\"specialId\":\"xxxx\",\"status\":\"xxxx\",\"name\":\"xxxx\",\"dateOfBirth\":\"xxxx\",\"address\":{\"firstLine\":\"xxxx\",\"town\":\"xxxx\",\"country\":\"xxxx\"},\"metadata\":{\"trusted\":\"xxxx\",\"rating\":\"xxxx\",\"notes\":\"xxxx\"}}", - maskedRecord.getValue() - ); - } - - @Test - void forTombstoneShouldReturnItself() { - Record record = sampleRecord( - "users", - "some-key", - null - ); - - Record maskedRecord = masker.maskRecord(record); - - assertEquals( - record, - maskedRecord - ); - } - - @Test - void forNonJsonValueShouldReturnItself() { + public void forNonJsonValueShouldReturnDisclaimer() { Record record = sampleRecord( "users", "some-key", "not a valid json" ); - Record maskedRecord = masker.maskRecord(record); + Record maskedRecord = getMasker().maskRecord(record); assertEquals( "This record is unable to be masked as it is not a structured object. " + "This record is unavailable to view due to safety measures from json_mask_by_default to not leak " + - "sensitive data. Please contact akhq administrator.", + "sensitive data.", maskedRecord.getValue() ); } @Test - void forNonJsonValueThatLooksLikeJsonValueShouldReturnDisclaimer() { + public void forNonJsonValueThatLooksLikeJsonValueShouldReturnDisclaimer() { Record record = sampleRecord( "users", "some-key", "{not a valid json}" ); - Record maskedRecord = masker.maskRecord(record); + Record maskedRecord = getMasker().maskRecord(record); assertEquals( "This record is unable to be masked as it is not a structured object. " + "This record is unavailable to view due to safety measures from json_mask_by_default to not leak " + - "sensitive data. Please contact akhq administrator.", + "sensitive data.", maskedRecord.getValue() ); } @Test - void ifJsonParsingThrowsExceptionShouldReturnFalse() { - String sampleStringToParse = sampleValue(); + public void ifJsonParsingThrowsExceptionShouldReturnFalse() { Record record = sampleRecord( "different-topic", "some-key", - sampleStringToParse + SAMPLE_VALUE ); try (MockedStatic mockStatic = Mockito.mockStatic(JsonParser.class)) { - mockStatic.when(() -> JsonParser.parseString(sampleStringToParse)).thenThrow(new RuntimeException("Bad exception!")); - Record record1 = masker.maskRecord(record); + mockStatic.when(() -> JsonParser.parseString(SAMPLE_VALUE)).thenThrow(new RuntimeException("Bad exception!")); + Record record1 = getMasker().maskRecord(record); assertEquals("An exception occurred during an attempt to mask this record. This record is unavailable to view due to safety measures from json_mask_by_default to not leak sensitive data. Please contact akhq administrator.", record1.getValue()); } } @Test - void ifRecordHasMultiLevelNestedValuesShouldBeProcessedCorrectly() { + public void forUndefinedTopicShouldDefaultMaskAllValues() { Record record = sampleRecord( - "users", + "different-topic", "some-key", - sampleValueWithMultilevelNestedValues() + SAMPLE_VALUE ); - Record maskedRecord = masker.maskRecord(record); + Record maskedRecord = getMasker().maskRecord(record); assertEquals( - """ - {"specialId":123,"status":"ACTIVE","name":"xxxx","dateOfBirth":"xxxx","address":{"firstLine":"xxxx","town":"xxxx","country":"United Kingdom"},"metadata":{"trusted":true,"rating":"10","notes":"xxxx","other":{"shouldBeUnmasked":"Example multi-level-nested-value","shouldBeMasked":"xxxx"}}}""", + "{\"specialId\":\"xxxx\",\"status\":\"xxxx\",\"name\":\"xxxx\",\"dateOfBirth\":\"xxxx\",\"address\":{\"firstLine\":\"xxxx\",\"town\":\"xxxx\",\"country\":\"xxxx\"},\"metadata\":{\"trusted\":\"xxxx\",\"rating\":\"xxxx\",\"notes\":\"xxxx\"}}", maskedRecord.getValue() ); } - - private String sampleValue() { - return """ - { - "specialId": 123, - "status": "ACTIVE", - "name": "John Smith", - "dateOfBirth": "01-01-1991", - "address": { - "firstLine": "123 Example Avenue", - "town": "Faketown", - "country": "United Kingdom" - }, - "metadata": { - "trusted": true, - "rating": "10", - "notes": "All in good order" - } - } - """; - } - - private String sampleValueWithMultilevelNestedValues() { - return """ - { - "specialId": 123, - "status": "ACTIVE", - "name": "John Smith", - "dateOfBirth": "01-01-1991", - "address": { - "firstLine": "123 Example Avenue", - "town": "Faketown", - "country": "United Kingdom" - }, - "metadata": { - "trusted": true, - "rating": "10", - "notes": "All in good order", - "other": { - "shouldBeUnmasked": "Example multi-level-nested-value", - "shouldBeMasked": "Example multi-level-nested-value" - } - } - } - """; - } } diff --git a/src/test/java/org/akhq/utils/JsonMaskerTest.java b/src/test/java/org/akhq/utils/JsonMaskerTest.java new file mode 100644 index 000000000..8416517d7 --- /dev/null +++ b/src/test/java/org/akhq/utils/JsonMaskerTest.java @@ -0,0 +1,150 @@ +package org.akhq.utils; + +import org.akhq.models.Record; +import org.junit.jupiter.api.Test; + +import static org.akhq.utils.MaskerTestHelper.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +interface JsonMaskerTest { + + String SAMPLE_VALUE = """ + { + "specialId": 123, + "status": "ACTIVE", + "name": "John Smith", + "dateOfBirth": "01-01-1991", + "address": { + "firstLine": "123 Example Avenue", + "town": "Faketown", + "country": "United Kingdom" + }, + "metadata": { + "trusted": true, + "rating": "10", + "notes": "All in good order" + } + } + """; + + String SAMPLE_VALUE_WITH_MULTI_LEVEL_NESTED_OBJECTS = """ + { + "specialId": 123, + "status": "ACTIVE", + "name": "John Smith", + "dateOfBirth": "01-01-1991", + "address": { + "firstLine": "123 Example Avenue", + "town": "Faketown", + "country": "United Kingdom" + }, + "metadata": { + "trusted": true, + "rating": "10", + "notes": "All in good order", + "other": { + "shouldBeUnmasked": "Example multi-level-nested-value", + "shouldBeMasked": "Example multi-level-nested-value" + } + } + } + """; + + String SAMPLE_VALUE_WITH_ARRAYS = """ + { + "specialId": 123, + "status": "ACTIVE", + "name": "John Smith", + "dateOfBirth": "01-01-1991", + "address": [ + { + "firstLine": "123 Example Avenue", + "town": "Faketown", + "country": "United Kingdom" + }, + { + "firstLine": "Old Address", + "town": "Previoustown", + "country": "United Kingdom" + } + ], + "metadata": { + "trusted": true, + "rating": "10", + "notes": "All in good order", + "other": { + "shouldBeUnmasked": "Example multi-level-nested-value", + "shouldBeMasked": "Example multi-level-nested-value", + "arrayToMask": [ "one", "two", "three" ], + "arrayToShow": [ "one", "two", "three" ] + } + } + } + """; + + Masker getMasker(); + + @Test + default void shouldMaskRecordValue() { + Record record = sampleRecord( + "users", + "some-key", + SAMPLE_VALUE + ); + + Record maskedRecord = getMasker().maskRecord(record); + + assertEquals( + "{\"specialId\":123,\"status\":\"ACTIVE\",\"name\":\"xxxx\",\"dateOfBirth\":\"xxxx\",\"address\":{\"firstLine\":\"xxxx\",\"town\":\"xxxx\",\"country\":\"United Kingdom\"},\"metadata\":{\"trusted\":true,\"rating\":\"10\",\"notes\":\"All in good order\"}}", + maskedRecord.getValue() + ); + } + + @Test + default void forTombstoneShouldReturnItself() { + Record record = sampleRecord( + "users", + "some-key", + null + ); + + Record maskedRecord = getMasker().maskRecord(record); + + assertEquals( + record, + maskedRecord + ); + } + + @Test + default void ifRecordHasMultiLevelNestedValuesShouldBeProcessedCorrectly() { + Record record = sampleRecord( + "users", + "some-key", + SAMPLE_VALUE_WITH_MULTI_LEVEL_NESTED_OBJECTS + ); + + Record maskedRecord = getMasker().maskRecord(record); + + assertEquals( + """ + {"specialId":123,"status":"ACTIVE","name":"xxxx","dateOfBirth":"xxxx","address":{"firstLine":"xxxx","town":"xxxx","country":"United Kingdom"},"metadata":{"trusted":true,"rating":"10","notes":"All in good order","other":{"shouldBeUnmasked":"Example multi-level-nested-value","shouldBeMasked":"xxxx"}}}""", + maskedRecord.getValue() + ); + } + + @Test + default void ifRecordUsesArrays_handlingOfFieldsWorksAsExpected() { + Record record = sampleRecord( + "users", + "some-key", + SAMPLE_VALUE_WITH_ARRAYS + ); + Record maskedRecord = getMasker().maskRecord(record); + assertEquals( + """ + {"specialId":123,"status":"ACTIVE","name":"xxxx","dateOfBirth":"xxxx","address":[{"firstLine":"xxxx","town":"xxxx","country":"United Kingdom"},{"firstLine":"xxxx","town":"xxxx","country":"United Kingdom"}],"metadata":{"trusted":true,"rating":"10","notes":"All in good order","other":{"shouldBeUnmasked":"Example multi-level-nested-value","shouldBeMasked":"xxxx","arrayToMask":["xxxx","xxxx","xxxx"],"arrayToShow":["one","two","three"]}}}""", + maskedRecord.getValue() + ); + } +} diff --git a/src/test/java/org/akhq/utils/JsonShowByDefaultMaskerTest.java b/src/test/java/org/akhq/utils/JsonShowByDefaultMaskerTest.java index f64454cf3..b626da9aa 100644 --- a/src/test/java/org/akhq/utils/JsonShowByDefaultMaskerTest.java +++ b/src/test/java/org/akhq/utils/JsonShowByDefaultMaskerTest.java @@ -2,163 +2,34 @@ import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; +import lombok.Getter; import org.akhq.models.Record; import org.junit.jupiter.api.Test; +import static org.akhq.utils.MaskerTestHelper.sampleRecord; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; +@Getter @MicronautTest(environments = "json-show-by-default-data-masking") -class JsonShowByDefaultMaskerTest extends MaskerTestHelper { +class JsonShowByDefaultMaskerTest implements JsonMaskerTest { @Inject - Masker masker; + JsonShowByDefaultMasker masker; @Test - void shouldUseJsonShowByDefaultMasker() { - assertInstanceOf(JsonShowByDefaultMasker.class, masker); - } - - @Test - void shouldMaskRecordValue() { - Record record = sampleRecord( - "users", - "some-key", - sampleValue() - ); - - Record maskedRecord = masker.maskRecord(record); - - assertEquals( - "{\"specialId\":123,\"status\":\"ACTIVE\",\"name\":\"xxxx\",\"dateOfBirth\":\"xxxx\",\"address\":{\"firstLine\":\"xxxx\",\"town\":\"xxxx\",\"country\":\"United Kingdom\"},\"metadata\":{\"trusted\":true,\"rating\":\"10\",\"notes\":\"All in good order\"}}", - maskedRecord.getValue() - ); - } - - @Test - void shouldDoNothingForUndefinedTopic() { + public void forUndefinedTopicShouldDefaultShowAllValues() { Record record = sampleRecord( "different-topic", "some-key", - sampleValue() - ); - - Record maskedRecord = masker.maskRecord(record); - - assertEquals( - sampleValue(), - maskedRecord.getValue() - ); - } - - @Test - void forTombstoneShouldReturnItself() { - Record record = sampleRecord( - "users", - "some-key", - null - ); - - Record maskedRecord = masker.maskRecord(record); - - assertEquals( - record, - maskedRecord - ); - } - - @Test - void forNonJsonValueShouldReturnItself() { - Record record = sampleRecord( - "users", - "some-key", - "not a valid json" + SAMPLE_VALUE ); - Record maskedRecord = masker.maskRecord(record); + Record maskedRecord = getMasker().maskRecord(record); assertEquals( - record, - maskedRecord - ); - } - - @Test - void forNonJsonValueThatLooksLikeJsonValueShouldReturnItself() { - Record record = sampleRecord( - "users", - "some-key", - "{not a valid json}" - ); - - Record maskedRecord = masker.maskRecord(record); - - assertEquals( - record, - maskedRecord - ); - } - - @Test - void ifRecordHasMultiLevelNestedValuesShouldBeProcessedCorrectly() { - Record record = sampleRecord( - "users", - "some-key", - sampleValueWithMultilevelNestedValues() - ); - - Record maskedRecord = masker.maskRecord(record); - - assertEquals( - """ - {"specialId":123,"status":"ACTIVE","name":"xxxx","dateOfBirth":"xxxx","address":{"firstLine":"xxxx","town":"xxxx","country":"United Kingdom"},"metadata":{"trusted":true,"rating":"10","notes":"All in good order","other":{"shouldBeUnmasked":"Example multi-level-nested-value","shouldBeMasked":"xxxx"}}}""", + SAMPLE_VALUE, maskedRecord.getValue() ); } - private String sampleValue() { - return """ - { - "specialId": 123, - "status": "ACTIVE", - "name": "John Smith", - "dateOfBirth": "01-01-1991", - "address": { - "firstLine": "123 Example Avenue", - "town": "Faketown", - "country": "United Kingdom" - }, - "metadata": { - "trusted": true, - "rating": "10", - "notes": "All in good order" - } - } - """; - } - - private String sampleValueWithMultilevelNestedValues() { - return """ - { - "specialId": 123, - "status": "ACTIVE", - "name": "John Smith", - "dateOfBirth": "01-01-1991", - "address": { - "firstLine": "123 Example Avenue", - "town": "Faketown", - "country": "United Kingdom" - }, - "metadata": { - "trusted": true, - "rating": "10", - "notes": "All in good order", - "other": { - "shouldBeUnmasked": "Example multi-level-nested-value", - "shouldBeMasked": "Example multi-level-nested-value" - } - } - } - """; - } } diff --git a/src/test/java/org/akhq/utils/MaskerTestHelper.java b/src/test/java/org/akhq/utils/MaskerTestHelper.java index 5453278d3..544c91c2e 100644 --- a/src/test/java/org/akhq/utils/MaskerTestHelper.java +++ b/src/test/java/org/akhq/utils/MaskerTestHelper.java @@ -6,7 +6,7 @@ import java.util.List; -public class MaskerTestHelper { +class MaskerTestHelper { static Record sampleRecord(String topicName, String key, diff --git a/src/test/resources/application-json-mask-by-default-data-masking.yml b/src/test/resources/application-json-mask-by-default-data-masking.yml index 5a4a8d21d..52d457146 100644 --- a/src/test/resources/application-json-mask-by-default-data-masking.yml +++ b/src/test/resources/application-json-mask-by-default-data-masking.yml @@ -11,4 +11,6 @@ akhq: - address.country - metadata.trusted - metadata.rating + - metadata.notes - metadata.other.shouldBeUnmasked + - metadata.other.arrayToShow diff --git a/src/test/resources/application-json-show-by-default-data-masking.yml b/src/test/resources/application-json-show-by-default-data-masking.yml index d917c5898..556ea4792 100644 --- a/src/test/resources/application-json-show-by-default-data-masking.yml +++ b/src/test/resources/application-json-show-by-default-data-masking.yml @@ -11,3 +11,4 @@ akhq: - address.firstLine - address.town - metadata.other.shouldBeMasked + - metadata.other.arrayToMask