Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ public static class KeystoreConfig {
String keystoreLocation;
String keystorePassword;
}

@Data
public static class Masking {
Type type;
Expand All @@ -136,6 +135,7 @@ public static class Masking {
String replacement; //used when type=REPLACE
String topicKeysPattern;
String topicValuesPattern;
Boolean enableNestedPaths = false; // New field to enable nested path support

public enum Type {
REMOVE, MASK, REPLACE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.provectus.kafka.ui.config.ClustersProperties;
import com.provectus.kafka.ui.exception.ValidationException;
import java.util.List;
import java.util.regex.Pattern;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
Expand All @@ -12,17 +13,62 @@ static FieldsSelector create(ClustersProperties.Masking property) {
if (StringUtils.hasText(property.getFieldsNamePattern()) && !CollectionUtils.isEmpty(property.getFields())) {
throw new ValidationException("You can't provide both fieldNames & fieldsNamePattern for masking");
}

boolean nestedPathsEnabled = Boolean.TRUE.equals(property.getEnableNestedPaths());

if (StringUtils.hasText(property.getFieldsNamePattern())) {
Pattern pattern = Pattern.compile(property.getFieldsNamePattern());
return f -> pattern.matcher(f).matches();
return new FieldsSelector() {
@Override
public boolean shouldBeMasked(String fieldName) {
return pattern.matcher(fieldName).matches();
}

@Override
public boolean shouldBeMasked(List<String> fieldPath) {
if (!nestedPathsEnabled) {
return shouldBeMasked(fieldPath.get(fieldPath.size() - 1));
}
String path = String.join(".", fieldPath);
return pattern.matcher(path).matches();
}
};
}

if (!CollectionUtils.isEmpty(property.getFields())) {
return f -> property.getFields().contains(f);
return new FieldsSelector() {
@Override
public boolean shouldBeMasked(String fieldName) {
return property.getFields().contains(fieldName);
}

@Override
public boolean shouldBeMasked(List<String> fieldPath) {
if (!nestedPathsEnabled) {
return shouldBeMasked(fieldPath.get(fieldPath.size() - 1));
}
String path = String.join(".", fieldPath);
return property.getFields().contains(path);
}
};
}

//no pattern, no field names - mean all fields should be masked
return fieldName -> true;
return new FieldsSelector() {
@Override
public boolean shouldBeMasked(String fieldName) {
return true;
}

@Override
public boolean shouldBeMasked(List<String> fieldPath) {
return true;
}
};
}

boolean shouldBeMasked(String fieldName);

boolean shouldBeMasked(List<String> fieldPath);

}
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,35 @@ private static UnaryOperator<String> createMasker(List<String> maskingChars) {
return sb.toString();
};
}

private JsonNode maskWithFieldsCheck(JsonNode node) {
return maskWithFieldsCheck(node, new java.util.ArrayList<>());
}

private JsonNode maskWithFieldsCheck(JsonNode node, java.util.List<String> path) {
if (node.isObject()) {
ObjectNode obj = ((ObjectNode) node).objectNode();
node.fields().forEachRemaining(f -> {
String fieldName = f.getKey();
JsonNode fieldVal = f.getValue();
if (fieldShouldBeMasked(fieldName)) {

java.util.List<String> currentPath = new java.util.ArrayList<>(path);
currentPath.add(fieldName);

if (fieldShouldBeMasked(fieldName) || fieldShouldBeMasked(currentPath)) {
obj.set(fieldName, maskNodeRecursively(fieldVal));
} else {
obj.set(fieldName, maskWithFieldsCheck(fieldVal));
obj.set(fieldName, maskWithFieldsCheck(fieldVal, currentPath));
}
});
return obj;
} else if (node.isArray()) {
ArrayNode arr = ((ArrayNode) node).arrayNode(node.size());
node.elements().forEachRemaining(e -> arr.add(maskWithFieldsCheck(e)));
int index = 0;
for (JsonNode element : node) {
java.util.List<String> currentPath = new java.util.ArrayList<>(path);
currentPath.add(String.valueOf(index++));
arr.add(maskWithFieldsCheck(element, currentPath));
}
return arr;
}
return node;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.databind.node.ContainerNode;
import com.provectus.kafka.ui.config.ClustersProperties;
import java.util.List;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
Expand Down Expand Up @@ -34,6 +35,10 @@ protected boolean fieldShouldBeMasked(String fieldName) {
return fieldsSelector.shouldBeMasked(fieldName);
}

protected boolean fieldShouldBeMasked(List<String> fieldPath) {
return fieldsSelector.shouldBeMasked(fieldPath);
}

public abstract ContainerNode<?> applyToJsonContainer(ContainerNode<?> node);

public abstract String applyToString(String str);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,33 @@ public String applyToString(String str) {
public ContainerNode<?> applyToJsonContainer(ContainerNode<?> node) {
return (ContainerNode<?>) removeFields(node);
}

private JsonNode removeFields(JsonNode node) {
return removeFields(node, new java.util.ArrayList<>());
}

private JsonNode removeFields(JsonNode node, java.util.List<String> path) {
if (node.isObject()) {
ObjectNode obj = ((ObjectNode) node).objectNode();
node.fields().forEachRemaining(f -> {
String fieldName = f.getKey();
JsonNode fieldVal = f.getValue();
if (!fieldShouldBeMasked(fieldName)) {
obj.set(fieldName, removeFields(fieldVal));

java.util.List<String> currentPath = new java.util.ArrayList<>(path);
currentPath.add(fieldName);

if (!fieldShouldBeMasked(fieldName) && !fieldShouldBeMasked(currentPath)) {
obj.set(fieldName, removeFields(fieldVal, currentPath));
}
});
return obj;
} else if (node.isArray()) {
var arr = ((ArrayNode) node).arrayNode(node.size());
node.elements().forEachRemaining(e -> arr.add(removeFields(e)));
int index = 0;
for (JsonNode element : node) {
java.util.List<String> currentPath = new java.util.ArrayList<>(path);
currentPath.add(String.valueOf(index++));
arr.add(removeFields(element, currentPath));
}
return arr;
}
return node;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,35 @@ public String applyToString(String str) {
public ContainerNode<?> applyToJsonContainer(ContainerNode<?> node) {
return (ContainerNode<?>) replaceWithFieldsCheck(node);
}

private JsonNode replaceWithFieldsCheck(JsonNode node) {
return replaceWithFieldsCheck(node, new java.util.ArrayList<>());
}

private JsonNode replaceWithFieldsCheck(JsonNode node, java.util.List<String> path) {
if (node.isObject()) {
ObjectNode obj = ((ObjectNode) node).objectNode();
node.fields().forEachRemaining(f -> {
String fieldName = f.getKey();
JsonNode fieldVal = f.getValue();
if (fieldShouldBeMasked(fieldName)) {

java.util.List<String> currentPath = new java.util.ArrayList<>(path);
currentPath.add(fieldName);

if (fieldShouldBeMasked(fieldName) || fieldShouldBeMasked(currentPath)) {
obj.set(fieldName, replaceRecursive(fieldVal));
} else {
obj.set(fieldName, replaceWithFieldsCheck(fieldVal));
obj.set(fieldName, replaceWithFieldsCheck(fieldVal, currentPath));
}
});
return obj;
} else if (node.isArray()) {
ArrayNode arr = ((ArrayNode) node).arrayNode(node.size());
node.elements().forEachRemaining(e -> arr.add(replaceWithFieldsCheck(e)));
int index = 0;
for (JsonNode element : node) {
java.util.List<String> currentPath = new java.util.ArrayList<>(path);
currentPath.add(String.valueOf(index++));
arr.add(replaceWithFieldsCheck(element, currentPath));
}
return arr;
}
// if it is not an object or array - we have nothing to replace here
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,65 @@ void throwsExceptionIfBothFieldListAndPatternProvided() {
.isInstanceOf(ValidationException.class);
}

@Test
void selectsNestedFieldsWhenEnabledWithFieldNames() {
var properties = new ClustersProperties.Masking();
properties.setFields(List.of("user.email", "user.address.street"));
properties.setEnableNestedPaths(true);

var selector = FieldsSelector.create(properties);

// Test single field names (backward compatibility)
assertThat(selector.shouldBeMasked("email")).isFalse();
assertThat(selector.shouldBeMasked("street")).isFalse();

// Test nested paths
assertThat(selector.shouldBeMasked(List.of("user", "email"))).isTrue();
assertThat(selector.shouldBeMasked(List.of("user", "address", "street"))).isTrue();
assertThat(selector.shouldBeMasked(List.of("user", "name"))).isFalse();
assertThat(selector.shouldBeMasked(List.of("other", "email"))).isFalse();
}

@Test
void selectsNestedFieldsWhenEnabledWithPattern() {
var properties = new ClustersProperties.Masking();
properties.setFieldsNamePattern("user\\..*|.*\\.secret");
properties.setEnableNestedPaths(true);

var selector = FieldsSelector.create(properties);

// Test nested paths with pattern
assertThat(selector.shouldBeMasked(List.of("user", "email"))).isTrue();
assertThat(selector.shouldBeMasked(List.of("user", "address", "street"))).isTrue();
assertThat(selector.shouldBeMasked(List.of("config", "secret"))).isTrue();
assertThat(selector.shouldBeMasked(List.of("other", "public"))).isFalse();
}

@Test
void fallsBackToFieldNameWhenNestedPathsDisabled() {
var properties = new ClustersProperties.Masking();
properties.setFields(List.of("user.email", "street"));
properties.setEnableNestedPaths(false);

var selector = FieldsSelector.create(properties);

// Should only match the last part of the path when nested paths are disabled
assertThat(selector.shouldBeMasked(List.of("user", "email"))).isFalse(); // Only matches "email", not "user.email"
assertThat(selector.shouldBeMasked(List.of("address", "street"))).isTrue(); // Matches "street"
assertThat(selector.shouldBeMasked("street")).isTrue(); // Direct field name matching still works
}

@Test
void defaultNestedPathsBehaviorIsFalse() {
var properties = new ClustersProperties.Masking();
properties.setFields(List.of("user.email"));
// enableNestedPaths not set, should default to false

var selector = FieldsSelector.create(properties);

// Should behave as if nested paths are disabled
assertThat(selector.shouldBeMasked(List.of("user", "email"))).isFalse();
assertThat(selector.shouldBeMasked("email")).isFalse();
}

}
Loading
Loading