Repository: provectus/kafka-ui
Issue: Topics - Data masking: Support regexp for fields
Labels: type/enhancement
good first issue
scope/backend
status/accepted

<!--

Don't forget to check for existing issues/discussions regarding your proposal. We might already have it.
https://github.com/provectus/kafka-ui/issues
https://github.com/provectus/kafka-ui/discussions

-->

**Describe the bug**
<!--(A clear and concise description of what the bug is.)-->
Masking (REMOVE | REPLACE | MASK) configuration for the field names in message key/value does not support regular expressions, e.g.:

```
      KAFKA_CLUSTERS_0_MASKING_0_FIELDS_0: "personal.data.*"
      KAFKA_CLUSTERS_0_MASKING_0_FIELDS_1: ".*password.*"
```

So the value and key patter could be set like below and apply to all the topics within a cluster

```
KAFKA_CLUSTERS_0_MASKING_0_TOPICKEYSPATTERN: ".*"
      KAFKA_CLUSTERS_0_MASKING_0_TOPICVALUESPATTERN: ".*"
```


**Set up**
<!--
How do you run the app? Please provide as much info as possible:
1. App version (docker image version or check commit hash in the top left corner in UI)
2. Helm chart version, if you use one
3. Any IAAC configs

We might close the issue without further explanation if you don't provide such information.
-->


**Steps to Reproduce**
<!-- We'd like you to provide an example setup (via docker-compose, helm, etc.) 
to reproduce the problem, especially with a complex setups. -->
Steps to reproduce the behavior:

1. Add an environment variable to set up the field value for masking, using the RegEx.
2. Install the Kafka UI
**Actual behavior**
Masking will apply only the field name exactly matching the field, including the '.' and '*' chars.

**Expected behavior**
<!--
(A clear and concise description of what you expected to happen)
-->
Ability to use regular expressions to the field names as well, for something obviously confidential, or fields with the same prefix or suffix


Issue comments:
This would be a nice feature. In a wild cluster you can't specify the exact field name you could want to mask or replace. Having just a kind of blacklisted words list that are searched in field name could be a great feature and easy to use.
It would be great if this feature can also be used to provide a regex that says "exclude all fields except certain ones I care about". That way, it would function more so as an allowlist. 

Pull request review comments:

Patch that resolved the issue:
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java
index 1d5cc5393c2..64ec894cd55 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ClustersProperties.java
@@ -131,8 +131,9 @@ public static class KeystoreConfig {
   @Data
   public static class Masking {
     Type type;
-    List<String> fields; //if null or empty list - policy will be applied to all fields
-    List<String> pattern; //used when type=MASK
+    List<String> fields;
+    String fieldsNamePattern;
+    List<String> maskingCharsReplacement; //used when type=MASK
     String replacement; //used when type=REPLACE
     String topicKeysPattern;
     String topicValuesPattern;
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/DataMasking.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/DataMasking.java
index 78e74f33321..ad1c2da31e5 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/DataMasking.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/DataMasking.java
@@ -44,7 +44,7 @@ boolean shouldBeApplied(String topic, Serde.Target target) {
   public static DataMasking create(@Nullable List<ClustersProperties.Masking> config) {
     return new DataMasking(
         Optional.ofNullable(config).orElse(List.of()).stream().map(property -> {
-          Preconditions.checkNotNull(property.getType(), "masking type not specifed");
+          Preconditions.checkNotNull(property.getType(), "masking type not specified");
           Preconditions.checkArgument(
               StringUtils.isNotEmpty(property.getTopicKeysPattern())
                   || StringUtils.isNotEmpty(property.getTopicValuesPattern()),
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/FieldsSelector.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/FieldsSelector.java
new file mode 100644
index 00000000000..99563943984
--- /dev/null
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/FieldsSelector.java
@@ -0,0 +1,28 @@
+package com.provectus.kafka.ui.service.masking.policies;
+
+import com.provectus.kafka.ui.config.ClustersProperties;
+import com.provectus.kafka.ui.exception.ValidationException;
+import java.util.regex.Pattern;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+interface FieldsSelector {
+
+  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");
+    }
+    if (StringUtils.hasText(property.getFieldsNamePattern())) {
+      Pattern pattern = Pattern.compile(property.getFieldsNamePattern());
+      return f -> pattern.matcher(f).matches();
+    }
+    if (!CollectionUtils.isEmpty(property.getFields())) {
+      return f -> property.getFields().contains(f);
+    }
+    //no pattern, no field names - mean all fields should be masked
+    return fieldName -> true;
+  }
+
+  boolean shouldBeMasked(String fieldName);
+
+}
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Mask.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Mask.java
index dbbc5d131a7..e6a469f2c03 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Mask.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Mask.java
@@ -15,8 +15,8 @@ class Mask extends MaskingPolicy {
 
   private final UnaryOperator<String> masker;
 
-  Mask(List<String> fieldNames, List<String> maskingChars) {
-    super(fieldNames);
+  Mask(FieldsSelector fieldsSelector, List<String> maskingChars) {
+    super(fieldsSelector);
     this.masker = createMasker(maskingChars);
   }
 
@@ -38,22 +38,13 @@ private static UnaryOperator<String> createMasker(List<String> maskingChars) {
       for (int i = 0; i < input.length(); i++) {
         int cp = input.codePointAt(i);
         switch (Character.getType(cp)) {
-          case Character.SPACE_SEPARATOR:
-          case Character.LINE_SEPARATOR:
-          case Character.PARAGRAPH_SEPARATOR:
-            sb.appendCodePoint(cp); // keeping separators as-is
-            break;
-          case Character.UPPERCASE_LETTER:
-            sb.append(maskingChars.get(0));
-            break;
-          case Character.LOWERCASE_LETTER:
-            sb.append(maskingChars.get(1));
-            break;
-          case Character.DECIMAL_DIGIT_NUMBER:
-            sb.append(maskingChars.get(2));
-            break;
-          default:
-            sb.append(maskingChars.get(3));
+          case Character.SPACE_SEPARATOR,
+              Character.LINE_SEPARATOR,
+              Character.PARAGRAPH_SEPARATOR -> sb.appendCodePoint(cp); // keeping separators as-is
+          case Character.UPPERCASE_LETTER -> sb.append(maskingChars.get(0));
+          case Character.LOWERCASE_LETTER -> sb.append(maskingChars.get(1));
+          case Character.DECIMAL_DIGIT_NUMBER -> sb.append(maskingChars.get(2));
+          default -> sb.append(maskingChars.get(3));
         }
       }
       return sb.toString();
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/MaskingPolicy.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/MaskingPolicy.java
index 7a753382102..9b80da0cb18 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/MaskingPolicy.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/MaskingPolicy.java
@@ -2,46 +2,36 @@
 
 import com.fasterxml.jackson.databind.node.ContainerNode;
 import com.provectus.kafka.ui.config.ClustersProperties;
-import java.util.List;
 import lombok.RequiredArgsConstructor;
 
 @RequiredArgsConstructor
 public abstract class MaskingPolicy {
 
-
   public static MaskingPolicy create(ClustersProperties.Masking property) {
-    List<String> fields = property.getFields() == null
-        ? List.of() // empty list means that policy will be applied to all fields
-        : property.getFields();
-    switch (property.getType()) {
-      case REMOVE:
-        return new Remove(fields);
-      case REPLACE:
-        return new Replace(
-            fields,
-            property.getReplacement() == null
-                ? Replace.DEFAULT_REPLACEMENT
-                : property.getReplacement()
-        );
-      case MASK:
-        return new Mask(
-            fields,
-            property.getPattern() == null
-                ? Mask.DEFAULT_PATTERN
-                : property.getPattern()
-        );
-      default:
-        throw new IllegalStateException("Unknown policy type: " + property.getType());
-    }
+    FieldsSelector fieldsSelector = FieldsSelector.create(property);
+    return switch (property.getType()) {
+      case REMOVE -> new Remove(fieldsSelector);
+      case REPLACE -> new Replace(
+          fieldsSelector,
+          property.getReplacement() == null
+              ? Replace.DEFAULT_REPLACEMENT
+              : property.getReplacement()
+      );
+      case MASK -> new Mask(
+          fieldsSelector,
+          property.getMaskingCharsReplacement() == null
+              ? Mask.DEFAULT_PATTERN
+              : property.getMaskingCharsReplacement()
+      );
+    };
   }
 
   //----------------------------------------------------------------
 
-  // empty list means policy will be applied to all fields
-  private final List<String> fieldNames;
+  private final FieldsSelector fieldsSelector;
 
   protected boolean fieldShouldBeMasked(String fieldName) {
-    return fieldNames.isEmpty() || fieldNames.contains(fieldName);
+    return fieldsSelector.shouldBeMasked(fieldName);
   }
 
   public abstract ContainerNode<?> applyToJsonContainer(ContainerNode<?> node);
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Remove.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Remove.java
index eb38b0d3e3a..cc5cdd14159 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Remove.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Remove.java
@@ -4,12 +4,12 @@
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ContainerNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
-import java.util.List;
+
 
 class Remove extends MaskingPolicy {
 
-  Remove(List<String> fieldNames) {
-    super(fieldNames);
+  Remove(FieldsSelector fieldsSelector) {
+    super(fieldsSelector);
   }
 
   @Override
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Replace.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Replace.java
index 3af645cb111..1cf91793d22 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Replace.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/masking/policies/Replace.java
@@ -6,7 +6,6 @@
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.fasterxml.jackson.databind.node.TextNode;
 import com.google.common.base.Preconditions;
-import java.util.List;
 
 class Replace extends MaskingPolicy {
 
@@ -14,8 +13,8 @@ class Replace extends MaskingPolicy {
 
   private final String replacement;
 
-  Replace(List<String> fieldNames, String replacementString) {
-    super(fieldNames);
+  Replace(FieldsSelector fieldsSelector, String replacementString) {
+    super(fieldsSelector);
     this.replacement = Preconditions.checkNotNull(replacementString);
   }
 
diff --git a/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml b/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml
index 78c7cf3bf52..2bafb05faaa 100644
--- a/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml
+++ b/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml
@@ -3632,7 +3632,9 @@ components:
                               type: array
                               items:
                                 type: string
-                            pattern:
+                            fieldsNamePattern:
+                              type: string
+                            maskingCharsReplacement:
                               type: array
                               items:
                                 type: string


Test patch:
diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/FieldsSelectorTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/FieldsSelectorTest.java
new file mode 100644
index 00000000000..497a9365d75
--- /dev/null
+++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/FieldsSelectorTest.java
@@ -0,0 +1,53 @@
+package com.provectus.kafka.ui.service.masking.policies;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import com.provectus.kafka.ui.config.ClustersProperties;
+import com.provectus.kafka.ui.exception.ValidationException;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class FieldsSelectorTest {
+
+  @Test
+  void selectsFieldsDueToProvidedPattern() {
+    var properties = new ClustersProperties.Masking();
+    properties.setFieldsNamePattern("f1|f2");
+
+    var selector = FieldsSelector.create(properties);
+    assertThat(selector.shouldBeMasked("f1")).isTrue();
+    assertThat(selector.shouldBeMasked("f2")).isTrue();
+    assertThat(selector.shouldBeMasked("doesNotMatchPattern")).isFalse();
+  }
+
+  @Test
+  void selectsFieldsDueToProvidedFieldNames() {
+    var properties = new ClustersProperties.Masking();
+    properties.setFields(List.of("f1", "f2"));
+
+    var selector = FieldsSelector.create(properties);
+    assertThat(selector.shouldBeMasked("f1")).isTrue();
+    assertThat(selector.shouldBeMasked("f2")).isTrue();
+    assertThat(selector.shouldBeMasked("notInAList")).isFalse();
+  }
+
+  @Test
+  void selectAllFieldsIfNoPatternAndNoNamesProvided() {
+    var properties = new ClustersProperties.Masking();
+
+    var selector = FieldsSelector.create(properties);
+    assertThat(selector.shouldBeMasked("anyPropertyName")).isTrue();
+  }
+
+  @Test
+  void throwsExceptionIfBothFieldListAndPatternProvided() {
+    var properties = new ClustersProperties.Masking();
+    properties.setFieldsNamePattern("f1|f2");
+    properties.setFields(List.of("f3", "f4"));
+
+    assertThatThrownBy(() -> FieldsSelector.create(properties))
+        .isInstanceOf(ValidationException.class);
+  }
+
+}
diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/MaskTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/MaskTest.java
index 9cb97012458..b33a26f3000 100644
--- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/MaskTest.java
+++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/MaskTest.java
@@ -15,35 +15,35 @@
 
 class MaskTest {
 
-  private static final List<String> TARGET_FIELDS = List.of("id", "name");
+  private static final FieldsSelector FIELDS_SELECTOR = fieldName -> List.of("id", "name").contains(fieldName);
   private static final List<String> PATTERN = List.of("X", "x", "n", "-");
 
   @ParameterizedTest
   @MethodSource
-  void testApplyToJsonContainer(List<String> fields, ContainerNode<?> original, ContainerNode<?> expected) {
-    Mask policy = new Mask(fields, PATTERN);
+  void testApplyToJsonContainer(FieldsSelector selector, ContainerNode<?> original, ContainerNode<?> expected) {
+    Mask policy = new Mask(selector, PATTERN);
     assertThat(policy.applyToJsonContainer(original)).isEqualTo(expected);
   }
 
   private static Stream<Arguments> testApplyToJsonContainer() {
     return Stream.of(
         Arguments.of(
-            TARGET_FIELDS,
+            FIELDS_SELECTOR,
             parse("{ \"id\": 123, \"name\": { \"first\": \"James\", \"surname\": \"Bond777!\"}}"),
             parse("{ \"id\": \"nnn\", \"name\": { \"first\": \"Xxxxx\", \"surname\": \"Xxxxnnn-\"}}")
         ),
         Arguments.of(
-            TARGET_FIELDS,
+            FIELDS_SELECTOR,
             parse("[{ \"id\": 123, \"f2\": 234}, { \"name\": \"1.2\", \"f2\": 345} ]"),
             parse("[{ \"id\": \"nnn\", \"f2\": 234}, { \"name\": \"n-n\", \"f2\": 345} ]")
         ),
         Arguments.of(
-            TARGET_FIELDS,
+            FIELDS_SELECTOR,
             parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Bond777!\"}}"),
             parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Xxxxnnn-\"}}")
         ),
         Arguments.of(
-            List.of(),
+            (FieldsSelector) (fieldName -> true),
             parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Bond777!\"}}"),
             parse("{ \"outer\": { \"f1\": \"Xxxxx\", \"name\": \"Xxxxnnn-\"}}")
         )
@@ -57,7 +57,7 @@ private static Stream<Arguments> testApplyToJsonContainer() {
       "null, xxxx"
   })
   void testApplyToString(String original, String expected) {
-    Mask policy = new Mask(List.of(), PATTERN);
+    Mask policy = new Mask(fieldName -> true, PATTERN);
     assertThat(policy.applyToString(original)).isEqualTo(expected);
   }
 
diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/RemoveTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/RemoveTest.java
index 31ef4eb3c35..9393ea1c626 100644
--- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/RemoveTest.java
+++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/RemoveTest.java
@@ -15,39 +15,39 @@
 
 class RemoveTest {
 
-  private static final List<String> TARGET_FIELDS = List.of("id", "name");
+  private static final FieldsSelector FIELDS_SELECTOR = fieldName -> List.of("id", "name").contains(fieldName);
 
   @ParameterizedTest
   @MethodSource
-  void testApplyToJsonContainer(List<String> fields, ContainerNode<?> original, ContainerNode<?>  expected) {
-    var policy = new Remove(fields);
+  void testApplyToJsonContainer(FieldsSelector fieldsSelector, ContainerNode<?> original, ContainerNode<?>  expected) {
+    var policy = new Remove(fieldsSelector);
     assertThat(policy.applyToJsonContainer(original)).isEqualTo(expected);
   }
 
   private static Stream<Arguments> testApplyToJsonContainer() {
     return Stream.of(
         Arguments.of(
-            TARGET_FIELDS,
+            FIELDS_SELECTOR,
             parse("{ \"id\": 123, \"name\": { \"first\": \"James\", \"surname\": \"Bond777!\"}}"),
             parse("{}")
         ),
         Arguments.of(
-            TARGET_FIELDS,
+            FIELDS_SELECTOR,
             parse("[{ \"id\": 123, \"f2\": 234}, { \"name\": \"1.2\", \"f2\": 345} ]"),
             parse("[{ \"f2\": 234}, { \"f2\": 345} ]")
         ),
         Arguments.of(
-            TARGET_FIELDS,
+            FIELDS_SELECTOR,
             parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Bond777!\"}}"),
             parse("{ \"outer\": { \"f1\": \"James\"}}")
         ),
         Arguments.of(
-            List.of(),
+            (FieldsSelector) (fieldName -> true),
             parse("{ \"outer\": { \"f1\": \"v1\", \"f2\": \"v2\", \"inner\" : {\"if1\": \"iv1\"}}}"),
             parse("{}")
         ),
         Arguments.of(
-            List.of(),
+            (FieldsSelector) (fieldName -> true),
             parse("[{ \"f1\": 123}, { \"f2\": \"1.2\"} ]"),
             parse("[{}, {}]")
         )
@@ -66,7 +66,7 @@ private static JsonNode parse(String str) {
       "null, null"
   })
   void testApplyToString(String original, String expected) {
-    var policy = new Remove(List.of());
+    var policy = new Remove(fieldName -> true);
     assertThat(policy.applyToString(original)).isEqualTo(expected);
   }
-}
\ No newline at end of file
+}
diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/ReplaceTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/ReplaceTest.java
index f3ac69747b8..9f2fcd90c4c 100644
--- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/ReplaceTest.java
+++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/masking/policies/ReplaceTest.java
@@ -15,35 +15,35 @@
 
 class ReplaceTest {
 
-  private static final List<String> TARGET_FIELDS = List.of("id", "name");
+  private static final FieldsSelector FIELDS_SELECTOR = fieldName -> List.of("id", "name").contains(fieldName);
   private static final String REPLACEMENT_STRING = "***";
 
   @ParameterizedTest
   @MethodSource
-  void testApplyToJsonContainer(List<String> fields, ContainerNode<?> original, ContainerNode<?>  expected) {
-    var policy = new Replace(fields, REPLACEMENT_STRING);
+  void testApplyToJsonContainer(FieldsSelector fieldsSelector, ContainerNode<?> original, ContainerNode<?>  expected) {
+    var policy = new Replace(fieldsSelector, REPLACEMENT_STRING);
     assertThat(policy.applyToJsonContainer(original)).isEqualTo(expected);
   }
 
   private static Stream<Arguments> testApplyToJsonContainer() {
     return Stream.of(
         Arguments.of(
-            TARGET_FIELDS,
+            FIELDS_SELECTOR,
             parse("{ \"id\": 123, \"name\": { \"first\": \"James\", \"surname\": \"Bond777!\"}}"),
             parse("{ \"id\": \"***\", \"name\": { \"first\": \"***\", \"surname\": \"***\"}}")
         ),
         Arguments.of(
-            TARGET_FIELDS,
+            FIELDS_SELECTOR,
             parse("[{ \"id\": 123, \"f2\": 234}, { \"name\": \"1.2\", \"f2\": 345} ]"),
             parse("[{ \"id\": \"***\", \"f2\": 234}, { \"name\": \"***\", \"f2\": 345} ]")
         ),
         Arguments.of(
-            TARGET_FIELDS,
+            FIELDS_SELECTOR,
             parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Bond777!\"}}"),
             parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"***\"}}")
         ),
         Arguments.of(
-            List.of(),
+            (FieldsSelector) (fieldName -> true),
             parse("{ \"outer\": { \"f1\": \"v1\", \"f2\": \"v2\", \"inner\" : {\"if1\": \"iv1\"}}}"),
             parse("{ \"outer\": { \"f1\": \"***\", \"f2\": \"***\", \"inner\" : {\"if1\": \"***\"}}}}")
         )
@@ -62,7 +62,7 @@ private static JsonNode parse(String str) {
       "null, ***"
   })
   void testApplyToString(String original, String expected) {
-    var policy = new Replace(List.of(), REPLACEMENT_STRING);
+    var policy = new Replace(fieldName -> true, REPLACEMENT_STRING);
     assertThat(policy.applyToString(original)).isEqualTo(expected);
   }
-}
\ No newline at end of file
+}