|
1 | 1 | package org.akhq.utils; |
2 | 2 |
|
| 3 | +import com.google.gson.JsonArray; |
3 | 4 | import com.google.gson.JsonElement; |
4 | 5 | import com.google.gson.JsonObject; |
5 | 6 | import com.google.gson.JsonParser; |
| 7 | +import com.google.gson.JsonPrimitive; |
6 | 8 | import io.micronaut.context.annotation.Requires; |
7 | 9 | import jakarta.inject.Singleton; |
8 | 10 | import lombok.SneakyThrows; |
9 | 11 | import org.akhq.configs.DataMasking; |
10 | | -import org.akhq.configs.JsonMaskingFilter; |
11 | 12 | import org.akhq.models.Record; |
12 | 13 |
|
13 | 14 | import java.util.List; |
14 | 15 | import java.util.Map; |
15 | 16 |
|
16 | 17 | @Singleton |
17 | 18 | @Requires(property = "akhq.security.data-masking.mode", value = "json_mask_by_default") |
18 | | -public class JsonMaskByDefaultMasker implements Masker { |
| 19 | +public class JsonMaskByDefaultMasker extends JsonMasker { |
19 | 20 |
|
20 | | - private final List<JsonMaskingFilter> jsonMaskingFilters; |
21 | | - private final String jsonMaskReplacement; |
| 21 | + 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."; |
| 22 | + 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."; |
22 | 23 |
|
23 | 24 | public JsonMaskByDefaultMasker(DataMasking dataMasking) { |
24 | | - this.jsonMaskingFilters = dataMasking.getJsonFilters(); |
25 | | - this.jsonMaskReplacement = dataMasking.getJsonMaskReplacement(); |
| 25 | + super(dataMasking); |
26 | 26 | } |
27 | 27 |
|
28 | 28 | public Record maskRecord(Record record) { |
| 29 | + if (!isJson(record)) { |
| 30 | + record.setValue(NON_JSON_MESSAGE); |
| 31 | + return record; |
| 32 | + } |
| 33 | + |
29 | 34 | try { |
30 | | - if(isJson(record)) { |
31 | | - return jsonMaskingFilters |
32 | | - .stream() |
33 | | - .filter(jsonMaskingFilter -> record.getTopic().getName().equalsIgnoreCase(jsonMaskingFilter.getTopic())) |
34 | | - .findFirst() |
35 | | - .map(filter -> applyMasking(record, filter.getKeys())) |
36 | | - .orElseGet(() -> applyMasking(record, List.of())); |
37 | | - } else { |
38 | | - 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."); |
39 | | - } |
| 35 | + List<String> keysToUnmask = getKeysForTopic(record.getTopic().getName()); |
| 36 | + return applyMasking(record, keysToUnmask); |
40 | 37 | } catch (Exception e) { |
41 | | - LOG.error("Error masking record at topic {}, partition {}, offset {} due to {}", record.getTopic(), record.getPartition(), record.getOffset(), e.getMessage()); |
42 | | - 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."); |
| 38 | + LOG.error("Error masking record at topic {}, partition {}, offset {} due to {}", |
| 39 | + record.getTopic(), record.getPartition(), record.getOffset(), e.getMessage()); |
| 40 | + record.setValue(ERROR_MESSAGE); |
| 41 | + return record; |
43 | 42 | } |
44 | | - return record; |
45 | 43 | } |
46 | 44 |
|
47 | 45 | @SneakyThrows |
48 | | - private Record applyMasking(Record record, List<String> keys) { |
49 | | - JsonObject jsonElement = JsonParser.parseString(record.getValue()).getAsJsonObject(); |
50 | | - maskAllExcept(jsonElement, keys); |
51 | | - record.setValue(jsonElement.toString()); |
| 46 | + private Record applyMasking(Record record, List<String> keysToUnmask) { |
| 47 | + JsonElement root = JsonParser.parseString(record.getValue()); |
| 48 | + maskJson(root, "", keysToUnmask); |
| 49 | + record.setValue(root.toString()); |
52 | 50 | return record; |
53 | 51 | } |
54 | 52 |
|
55 | | - private void maskAllExcept(JsonObject jsonElement, List<String> keys) { |
56 | | - maskAllExcept("", jsonElement, keys); |
| 53 | + private void maskJson(JsonElement element, String path, List<String> keysToUnmask) { |
| 54 | + if (element.isJsonObject()) { |
| 55 | + maskJsonObject(element.getAsJsonObject(), path, keysToUnmask); |
| 56 | + } else if (element.isJsonArray()) { |
| 57 | + maskJsonArray(element.getAsJsonArray(), path, keysToUnmask); |
| 58 | + } |
57 | 59 | } |
58 | 60 |
|
59 | | - private void maskAllExcept(String currentKey, JsonObject node, List<String> keys) { |
60 | | - if (node.isJsonObject()) { |
61 | | - JsonObject objectNode = node.getAsJsonObject(); |
62 | | - for(Map.Entry<String, JsonElement> entry : objectNode.entrySet()) { |
63 | | - if(entry.getValue().isJsonObject()) { |
64 | | - maskAllExcept(currentKey + entry.getKey() + ".", entry.getValue().getAsJsonObject(), keys); |
65 | | - } else { |
66 | | - if(!keys.contains(currentKey + entry.getKey())) { |
67 | | - objectNode.addProperty(entry.getKey(), jsonMaskReplacement); |
68 | | - } |
69 | | - } |
| 61 | + private void maskJsonObject(JsonObject obj, String path, List<String> keysToUnmask) { |
| 62 | + for (Map.Entry<String, JsonElement> entry : obj.entrySet()) { |
| 63 | + String newPath = path + entry.getKey(); |
| 64 | + JsonElement value = entry.getValue(); |
| 65 | + |
| 66 | + if (shouldMaskPrimitive(value, newPath, keysToUnmask)) { |
| 67 | + entry.setValue(new JsonPrimitive(jsonMaskReplacement)); |
| 68 | + } else if (isNestedStructure(value)) { |
| 69 | + maskJson(value, newPath + ".", keysToUnmask); |
70 | 70 | } |
71 | 71 | } |
72 | 72 | } |
| 73 | + |
| 74 | + private void maskJsonArray(JsonArray array, String path, List<String> keysToUnmask) { |
| 75 | + boolean shouldMask = !keysToUnmask.contains(path.substring(0, path.length() - 1)); |
| 76 | + |
| 77 | + for (int i = 0; i < array.size(); i++) { |
| 78 | + JsonElement arrayElement = array.get(i); |
| 79 | + if (arrayElement.isJsonPrimitive() && shouldMask) { |
| 80 | + array.set(i, new JsonPrimitive(jsonMaskReplacement)); |
| 81 | + } else if (isNestedStructure(arrayElement)) { |
| 82 | + maskJson(arrayElement, path, keysToUnmask); |
| 83 | + } |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + private boolean shouldMaskPrimitive(JsonElement value, String path, List<String> keysToUnmask) { |
| 88 | + return value.isJsonPrimitive() && !keysToUnmask.contains(path); |
| 89 | + } |
| 90 | + |
| 91 | + private boolean isNestedStructure(JsonElement value) { |
| 92 | + return value.isJsonObject() || value.isJsonArray(); |
| 93 | + } |
73 | 94 | } |
0 commit comments