This is an automated email from the ASF dual-hosted git repository.
apkhmv pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new 785ef6baec IGNITE-19166 Introduce @Secret annotation for configuration
framework (#2073)
785ef6baec is described below
commit 785ef6baeca64d0594e16497d6b2d5dd4824b285
Author: Ivan Gagarkin <[email protected]>
AuthorDate: Wed May 17 14:54:23 2023 +0400
IGNITE-19166 Introduce @Secret annotation for configuration framework
(#2073)
---
.../processor/ItConfigurationProcessorTest.java | 8 ++
.../SecretMustBeStringConfigurationSchema.java} | 17 ++--
.../processor/ConfigurationProcessor.java | 11 +++
.../ignite/configuration/annotation/Secret.java} | 21 ++--
modules/configuration/build.gradle | 1 +
.../configuration/hocon/HoconConverter.java | 7 +-
.../configuration/tree/ConverterToMapVisitor.java | 56 ++++++++---
.../tree/ConverterToMapVisitorBuilder.java | 58 +++++++++++
.../tree/ConverterToMapVisitorTest.java | 106 +++++++++++++++++++++
.../configuration/util/ConfigurationUtilTest.java | 33 ++++++-
.../org.mockito.plugins.MockMaker | 2 +
.../ignite/internal/index/IndexManagerTest.java | 8 +-
.../configuration/KeyStoreConfigurationSchema.java | 2 +
.../KeyStoreConfigurationSchemaTest.java} | 29 +++---
.../AbstractConfigurationController.java | 29 +-----
.../internal/rest/configuration/JsonMasker.java | 94 ------------------
.../ConfigurationControllerBaseTest.java | 15 ++-
.../rest/configuration/JsonMaskerTest.java | 105 --------------------
.../TestSubSensitiveConfigurationSchema.java | 2 +
.../storage/LocalFileConfigurationStorage.java | 10 +-
...cAuthenticationProviderConfigurationSchema.java | 2 +
...enticationProviderConfigurationSchemaTest.java} | 26 ++---
22 files changed, 350 insertions(+), 292 deletions(-)
diff --git
a/modules/configuration-annotation-processor/src/integrationTest/java/org/apache/ignite/internal/configuration/processor/ItConfigurationProcessorTest.java
b/modules/configuration-annotation-processor/src/integrationTest/java/org/apache/ignite/internal/configuration/processor/ItConfigurationProcessorTest.java
index fee7307113..b47f017e45 100644
---
a/modules/configuration-annotation-processor/src/integrationTest/java/org/apache/ignite/internal/configuration/processor/ItConfigurationProcessorTest.java
+++
b/modules/configuration-annotation-processor/src/integrationTest/java/org/apache/ignite/internal/configuration/processor/ItConfigurationProcessorTest.java
@@ -547,6 +547,14 @@ public class ItConfigurationProcessorTest extends
AbstractProcessorTest {
() -> batchCompile(packageName,
"ConfigMustNotContainInternalIdWithAbstractConfigConfigurationSchema"),
"Field with @InternalId is already present in the superclass"
);
+
+ // Let's check @Secret must be String.
+
+ assertThrowsEx(
+ IllegalStateException.class,
+ () -> batchCompile(packageName,
"SecretMustBeStringConfigurationSchema"),
+ "must be String. Only String field can be annotated with
@Secret"
+ );
}
@Test
diff --git
a/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/TestSubSensitiveConfigurationSchema.java
b/modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/abstractconfig/validation/SecretMustBeStringConfigurationSchema.java
similarity index 58%
copy from
modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/TestSubSensitiveConfigurationSchema.java
copy to
modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/abstractconfig/validation/SecretMustBeStringConfigurationSchema.java
index acfb752c1f..de38c6217f 100644
---
a/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/TestSubSensitiveConfigurationSchema.java
+++
b/modules/configuration-annotation-processor/src/integrationTest/resources/org/apache/ignite/internal/configuration/processor/abstractconfig/validation/SecretMustBeStringConfigurationSchema.java
@@ -15,17 +15,20 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.rest.configuration;
+package
org.apache.ignite.internal.configuration.processor.abstractconfig.validation;
+import java.util.UUID;
+import org.apache.ignite.configuration.annotation.AbstractConfiguration;
import org.apache.ignite.configuration.annotation.Config;
-import org.apache.ignite.configuration.annotation.Value;
+import org.apache.ignite.configuration.annotation.InternalId;
+import org.apache.ignite.configuration.annotation.Secret;
+import
org.apache.ignite.internal.configuration.processor.abstractconfig.AbstractConfigConfigurationSchema;
/**
- * Test sub sensitive configuration schema.
+ * Checks that field annotated with {@link
org.apache.ignite.configuration.annotation.Secret} must be {@link String}.
*/
@Config
-public class TestSubSensitiveConfigurationSchema {
-
- @Value(hasDefault = true)
- public String password = "";
+public class SecretMustBeStringConfigurationSchema {
+ @Secret
+ public int id;
}
diff --git
a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/ConfigurationProcessor.java
b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/ConfigurationProcessor.java
index 9c743b55a5..3e8775c304 100644
---
a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/ConfigurationProcessor.java
+++
b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/ConfigurationProcessor.java
@@ -85,6 +85,7 @@ import
org.apache.ignite.configuration.annotation.NamedConfigValue;
import org.apache.ignite.configuration.annotation.PolymorphicConfig;
import org.apache.ignite.configuration.annotation.PolymorphicConfigInstance;
import org.apache.ignite.configuration.annotation.PolymorphicId;
+import org.apache.ignite.configuration.annotation.Secret;
import org.apache.ignite.configuration.annotation.Value;
import org.jetbrains.annotations.Nullable;
@@ -112,6 +113,8 @@ public class ConfigurationProcessor extends
AbstractProcessor {
/** Error format is that the field must be a specific class. */
private static final String FIELD_MUST_BE_SPECIFIC_CLASS_ERROR_FORMAT =
"%s %s.%s field must be a %s";
+ private static final String SECRET_FIELD_MUST_BE_STRING = "%s.%s must be
String. Only String field can be annotated with @Secret";
+
/** Postfix with which any configuration schema class name must end. */
private static final String CONFIGURATION_SCHEMA_POSTFIX =
"ConfigurationSchema";
@@ -226,6 +229,14 @@ public class ConfigurationProcessor extends
AbstractProcessor {
}
}
+ if (field.getAnnotation(Secret.class) != null) {
+ if (!isClass(field.asType(), String.class)) {
+ throw new ConfigurationProcessorException(
+ String.format(SECRET_FIELD_MUST_BE_STRING,
clazz.getQualifiedName(), field.getSimpleName())
+ );
+ }
+ }
+
createGetters(configurationInterfaceBuilder, fieldName,
interfaceGetMethodType);
}
diff --git
a/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/TestSubSensitiveConfigurationSchema.java
b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/Secret.java
similarity index 65%
copy from
modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/TestSubSensitiveConfigurationSchema.java
copy to
modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/Secret.java
index acfb752c1f..6e72ad85f0 100644
---
a/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/TestSubSensitiveConfigurationSchema.java
+++
b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/annotation/Secret.java
@@ -15,17 +15,20 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.rest.configuration;
+package org.apache.ignite.configuration.annotation;
-import org.apache.ignite.configuration.annotation.Config;
-import org.apache.ignite.configuration.annotation.Value;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
/**
- * Test sub sensitive configuration schema.
+ * This annotation is used to mark a configuration field as sensitive
information.
*/
-@Config
-public class TestSubSensitiveConfigurationSchema {
-
- @Value(hasDefault = true)
- public String password = "";
+@Target(FIELD)
+@Retention(RUNTIME)
+@Documented
+public @interface Secret {
}
diff --git a/modules/configuration/build.gradle
b/modules/configuration/build.gradle
index 1093086543..59dda0d1b4 100644
--- a/modules/configuration/build.gradle
+++ b/modules/configuration/build.gradle
@@ -35,6 +35,7 @@ dependencies {
testImplementation(testFixtures(project(':ignite-core')))
testImplementation libs.hamcrest.core
testImplementation libs.mockito.core
+ testImplementation libs.mockito.junit
testFixturesAnnotationProcessor
project(':ignite-configuration-annotation-processor')
testFixturesImplementation project(':ignite-core')
diff --git
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconConverter.java
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconConverter.java
index a6ca6223ee..74bdb1d223 100644
---
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconConverter.java
+++
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/hocon/HoconConverter.java
@@ -42,7 +42,12 @@ public class HoconConverter {
ConfigurationRegistry registry,
@NotNull List<String> path
) throws IllegalArgumentException {
- Object res = registry.represent(path, new
ConverterToMapVisitor(false));
+ Object res = registry.represent(
+ path,
+ ConverterToMapVisitor.builder()
+ .includeInternal(false)
+ .maskSecretValues(true)
+ .build());
return ConfigImpl.fromAnyRef(res, null);
}
diff --git
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConverterToMapVisitor.java
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConverterToMapVisitor.java
index c4a26c1d88..76a396e6f7 100644
---
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConverterToMapVisitor.java
+++
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConverterToMapVisitor.java
@@ -31,38 +31,47 @@ import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
+import org.apache.ignite.configuration.annotation.Secret;
/**
* {@link ConfigurationVisitor} implementation that converts a configuration
tree to a combination of {@link Map} and {@link List} objects.
*/
public class ConverterToMapVisitor implements ConfigurationVisitor<Object> {
+
+ /** Leaf node value will be replaced with this string, if the {@link
Field} of the value has {@link Secret} annotation. */
+ private static final String MASKED_VALUE = "********";
+
/** Include internal configuration nodes (private configuration
extensions). */
private final boolean includeInternal;
/** Skip nulls, empty Maps and empty lists. */
private final boolean skipEmptyValues;
+ /** Mask values, if their {@link Field} has {@link Secret} annotation. */
+ private final boolean maskSecretValues;
+
/** Stack with intermediate results. Used to store values during recursive
calls. */
private final Deque<Object> deque = new ArrayDeque<>();
+ public static ConverterToMapVisitorBuilder builder() {
+ return new ConverterToMapVisitorBuilder();
+ }
+
/**
* Constructor.
*
* @param includeInternal Include internal configuration nodes (private
configuration extensions).
* @param skipEmptyValues Skip empty values.
+ * @param maskSecretValues Mask values, if their {@link Field} has {@link
Secret} annotation.
*/
- public ConverterToMapVisitor(boolean includeInternal, boolean
skipEmptyValues) {
+ ConverterToMapVisitor(
+ boolean includeInternal,
+ boolean skipEmptyValues,
+ boolean maskSecretValues
+ ) {
this.includeInternal = includeInternal;
this.skipEmptyValues = skipEmptyValues;
- }
-
- /**
- * Constructor.
- *
- * @param includeInternal Include internal configuration nodes (private
configuration extensions).
- */
- public ConverterToMapVisitor(boolean includeInternal) {
- this(includeInternal, false);
+ this.maskSecretValues = maskSecretValues;
}
/** {@inheritDoc} */
@@ -73,7 +82,9 @@ public class ConverterToMapVisitor implements
ConfigurationVisitor<Object> {
if (val instanceof Character || val instanceof UUID) {
valObj = val.toString();
} else if (val != null && val.getClass().isArray()) {
- valObj = toListOfObjects(val);
+ valObj = toListOfObjects(field, val);
+ } else if (val instanceof String) {
+ valObj = maskIfNeeded(field, (String) val);
}
addToParent(key, valObj);
@@ -162,10 +173,11 @@ public class ConverterToMapVisitor implements
ConfigurationVisitor<Object> {
/**
* Converts array into a list of objects. Boxes array elements if they are
primitive values.
*
+ * @param field Field of the array
* @param val Array of primitives or array of {@link String}s
* @return List of objects corresponding to the passed array.
*/
- private List<?> toListOfObjects(Serializable val) {
+ private List<?> toListOfObjects(Field field, Serializable val) {
Stream<?> stream = IntStream.range(0, Array.getLength(val)).mapToObj(i
-> Array.get(val, i));
if (val.getClass().getComponentType() == char.class ||
val.getClass().getComponentType() == UUID.class) {
@@ -174,4 +186,24 @@ public class ConverterToMapVisitor implements
ConfigurationVisitor<Object> {
return stream.collect(Collectors.toList());
}
+
+ /**
+ * Returns {@link ConverterToMapVisitor#MASKED_VALUE} if the field is
annotated with {@link Secret} and
+ * {@link ConverterToMapVisitor#maskSecretValues} is {@code true}.
+ *
+ * @param field Field to check
+ * @param val Value to mask
+ * @return Masked value
+ */
+ private Object maskIfNeeded(Field field, String val) {
+ if (!maskSecretValues) {
+ return val;
+ }
+
+ if (field != null && field.isAnnotationPresent(Secret.class)) {
+ return MASKED_VALUE;
+ } else {
+ return val;
+ }
+ }
}
diff --git
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConverterToMapVisitorBuilder.java
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConverterToMapVisitorBuilder.java
new file mode 100644
index 0000000000..15973cf514
--- /dev/null
+++
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConverterToMapVisitorBuilder.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.configuration.tree;
+
+import java.lang.reflect.Field;
+import org.apache.ignite.configuration.annotation.Secret;
+
+/**
+ * Builder for {@link ConverterToMapVisitor}.
+ */
+public final class ConverterToMapVisitorBuilder {
+
+ /** Include internal configuration nodes (private configuration
extensions). */
+ private boolean includeInternal = true;
+
+ /** Skip nulls, empty Maps and empty lists. */
+ private boolean skipEmptyValues = false;
+
+ /** Mask values, if their {@link Field} has {@link Secret} annotation. */
+ private boolean maskSecretValues = false;
+
+ ConverterToMapVisitorBuilder() {
+ }
+
+ public ConverterToMapVisitorBuilder includeInternal(boolean
includeInternal) {
+ this.includeInternal = includeInternal;
+ return this;
+ }
+
+ public ConverterToMapVisitorBuilder skipEmptyValues(boolean
skipEmptyValues) {
+ this.skipEmptyValues = skipEmptyValues;
+ return this;
+ }
+
+ public ConverterToMapVisitorBuilder maskSecretValues(boolean
maskSecretValues) {
+ this.maskSecretValues = maskSecretValues;
+ return this;
+ }
+
+ public ConverterToMapVisitor build() {
+ return new ConverterToMapVisitor(includeInternal, skipEmptyValues,
maskSecretValues);
+ }
+}
diff --git
a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/ConverterToMapVisitorTest.java
b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/ConverterToMapVisitorTest.java
new file mode 100644
index 0000000000..ce5e320cf8
--- /dev/null
+++
b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/ConverterToMapVisitorTest.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.configuration.tree;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.matchesPattern;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.lenient;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.UUID;
+import org.apache.ignite.configuration.annotation.Secret;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class ConverterToMapVisitorTest {
+
+ private final ConverterToMapVisitor maskingVisitor =
ConverterToMapVisitor.builder()
+ .maskSecretValues(true)
+ .build();
+
+ private final ConverterToMapVisitor notMaskingVisitor =
ConverterToMapVisitor.builder()
+ .maskSecretValues(false)
+ .build();
+
+ @Mock
+ Field secretField;
+
+ @Mock
+ Field notSecretField;
+
+
+ @BeforeEach
+ void setUp() {
+
lenient().when(secretField.isAnnotationPresent(Secret.class)).thenReturn(true);
+
lenient().when(notSecretField.isAnnotationPresent(Secret.class)).thenReturn(false);
+ }
+
+ @Test
+ public void notSecretStringNotMasked() {
+ Object password = maskingVisitor.visitLeafNode(notSecretField, null,
"password");
+ assertEquals("password", password.toString());
+ }
+
+ @Test
+ public void secretStringIsMasked() {
+ Object password = maskingVisitor.visitLeafNode(secretField, null,
"password");
+ assertThat(password.toString(), matchesPattern("^[\\*]+$"));
+ }
+
+ @Test
+ public void secretStringIsNotMaskedIfMaskSecretValuesIsFalse() {
+ Object password = notMaskingVisitor.visitLeafNode(secretField, null,
"password");
+ assertEquals("password", password.toString());
+ }
+
+ @Test
+ public void secretStringsHaveSameLength() {
+ Object string1 = maskingVisitor.visitLeafNode(secretField, null,
"password");
+ Object string2 = maskingVisitor.visitLeafNode(secretField, null,
"admin");
+ assertEquals(string1.toString().length(), string2.toString().length());
+ }
+
+ @Test
+ public void notStringsNotMasked() {
+ // int value
+ assertEquals(1, maskingVisitor.visitLeafNode(secretField, null, 1));
+
+ // long value
+ assertEquals(1L, maskingVisitor.visitLeafNode(secretField, null, 1L));
+
+ // double value
+ assertEquals(1.1, maskingVisitor.visitLeafNode(secretField, null,
1.1));
+
+ // Character value
+ assertEquals("a", maskingVisitor.visitLeafNode(secretField, null,
'a'));
+
+ // UUID value
+ UUID uuid = UUID.randomUUID();
+ assertEquals(uuid.toString(),
maskingVisitor.visitLeafNode(secretField, null, uuid));
+
+ // array value
+ String[] strings = {"a", "b", "c", "d"};
+ assertEquals(Arrays.asList(strings),
maskingVisitor.visitLeafNode(secretField, null, strings));
+ }
+}
diff --git
a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
index 984a42023f..04d5ec045d 100644
---
a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
+++
b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
@@ -673,7 +673,13 @@ public class ConfigurationUtilTest {
addDefaults(innerNode);
- Map<String, Object> config = (Map<String, Object>)
innerNode.accept(null, null, new ConverterToMapVisitor(false));
+ Map<String, Object> config = (Map<String, Object>) innerNode.accept(
+ null,
+ null,
+ ConverterToMapVisitor.builder()
+ .includeInternal(false)
+ .build()
+ );
// Check that no internal configuration will be received.
@@ -690,7 +696,13 @@ public class ConfigurationUtilTest {
// Check that no internal configuration will be received.
- config = (Map<String, Object>) innerNode.accept(null, null, new
ConverterToMapVisitor(true));
+ config = (Map<String, Object>) innerNode.accept(
+ null,
+ null,
+ ConverterToMapVisitor.builder()
+ .includeInternal(true)
+ .build()
+ );
assertEquals(7, config.size());
assertNull(config.get("str0"));
@@ -739,12 +751,23 @@ public class ConfigurationUtilTest {
assertNotNull(find(List.of(schemaKey.key()), superRoot, true));
- Map<String, Object> config =
- (Map<String, Object>) superRoot.accept(null, schemaKey.key(),
new ConverterToMapVisitor(false));
+ Map<String, Object> config = (Map<String, Object>) superRoot.accept(
+ null,
+ schemaKey.key(),
+ ConverterToMapVisitor.builder()
+ .includeInternal(false)
+ .build()
+ );
assertTrue(config.isEmpty());
- config = (Map<String, Object>) superRoot.accept(null, schemaKey.key(),
new ConverterToMapVisitor(true));
+ config = (Map<String, Object>) superRoot.accept(
+ null,
+ schemaKey.key(),
+ ConverterToMapVisitor.builder()
+ .includeInternal(true)
+ .build()
+ );
assertEquals(1, config.size());
assertNotNull(config.get(schemaKey.key()));
diff --git
a/modules/configuration/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
b/modules/configuration/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..1ed358c179
--- /dev/null
+++
b/modules/configuration/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1,2 @@
+# to mock final classes
+mock-maker-inline
diff --git
a/modules/index/src/test/java/org/apache/ignite/internal/index/IndexManagerTest.java
b/modules/index/src/test/java/org/apache/ignite/internal/index/IndexManagerTest.java
index 33d576539b..b01675ec75 100644
---
a/modules/index/src/test/java/org/apache/ignite/internal/index/IndexManagerTest.java
+++
b/modules/index/src/test/java/org/apache/ignite/internal/index/IndexManagerTest.java
@@ -213,7 +213,13 @@ public class IndexManagerTest {
private static Object toMap(Object obj) {
assert obj instanceof TraversableTreeNode;
- return ((TraversableTreeNode) obj).accept(null, null, new
ConverterToMapVisitor(false));
+ return ((TraversableTreeNode) obj).accept(
+ null,
+ null,
+ ConverterToMapVisitor.builder()
+ .includeInternal(false)
+ .build()
+ );
}
private static void assertSameObjects(Object expected, Object actual) {
diff --git
a/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationSchema.java
b/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationSchema.java
index bfb3fb1664..cac0c6cf99 100644
---
a/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationSchema.java
+++
b/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationSchema.java
@@ -18,6 +18,7 @@
package org.apache.ignite.internal.network.configuration;
import org.apache.ignite.configuration.annotation.Config;
+import org.apache.ignite.configuration.annotation.Secret;
import org.apache.ignite.configuration.annotation.Value;
/** Keystore configuration schema. */
@@ -32,6 +33,7 @@ public class KeyStoreConfigurationSchema {
public String path = "";
/** Keystore password. */
+ @Secret
@Value(hasDefault = true)
public String password = "";
}
diff --git
a/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationSchema.java
b/modules/network/src/test/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationSchemaTest.java
similarity index 59%
copy from
modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationSchema.java
copy to
modules/network/src/test/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationSchemaTest.java
index bfb3fb1664..58d3722145 100644
---
a/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationSchema.java
+++
b/modules/network/src/test/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationSchemaTest.java
@@ -17,21 +17,22 @@
package org.apache.ignite.internal.network.configuration;
-import org.apache.ignite.configuration.annotation.Config;
-import org.apache.ignite.configuration.annotation.Value;
+import static org.junit.jupiter.api.Assertions.assertTrue;
-/** Keystore configuration schema. */
-@Config
-public class KeyStoreConfigurationSchema {
- /** Keystore type. */
- @Value(hasDefault = true)
- public String type = "PKCS12";
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import org.apache.ignite.configuration.annotation.Secret;
+import org.junit.jupiter.api.Test;
- /** Keystore path. */
- @Value(hasDefault = true)
- public String path = "";
+class KeyStoreConfigurationSchemaTest {
- /** Keystore password. */
- @Value(hasDefault = true)
- public String password = "";
+ @Test
+ public void passwordIsSecret() {
+ Field password =
Arrays.stream(KeyStoreConfigurationSchema.class.getDeclaredFields())
+ .filter(it -> it.getName().equals("password"))
+ .findFirst()
+ .orElseThrow();
+
+ assertTrue(password.isAnnotationPresent(Secret.class));
+ }
}
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/AbstractConfigurationController.java
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/AbstractConfigurationController.java
index f04f272a5d..33ba7b5d0f 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/AbstractConfigurationController.java
+++
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/AbstractConfigurationController.java
@@ -17,12 +17,8 @@
package org.apache.ignite.internal.rest.configuration;
-import java.util.Collections;
-import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
import
org.apache.ignite.configuration.validation.ConfigurationValidationException;
import
org.apache.ignite.internal.configuration.presentation.ConfigurationPresentation;
import org.apache.ignite.lang.IgniteException;
@@ -32,10 +28,6 @@ import org.apache.ignite.lang.IgniteException;
*/
public abstract class AbstractConfigurationController {
- private final Set<String> keysToMask = Set.of("password");
- private final Pattern sensitiveInformationPattern =
sensitiveInformationPattern(keysToMask);
- private final JsonMasker jsonMasker = new JsonMasker();
-
/** Presentation of the configuration. */
private final ConfigurationPresentation<String> cfgPresentation;
@@ -49,7 +41,7 @@ public abstract class AbstractConfigurationController {
* @return the presentation of configuration.
*/
public String getConfiguration() {
- return maskSensitiveInformation(cfgPresentation.represent());
+ return cfgPresentation.represent();
}
/**
@@ -60,7 +52,7 @@ public abstract class AbstractConfigurationController {
*/
public String getConfigurationByPath(String path) {
try {
- return maskSensitiveInformation(path,
cfgPresentation.representByPath(path));
+ return cfgPresentation.representByPath(path);
} catch (IllegalArgumentException ex) {
throw new IgniteException(ex);
}
@@ -84,21 +76,4 @@ public abstract class AbstractConfigurationController {
throw new IgniteException(ex);
});
}
-
- private String maskSensitiveInformation(String configuration) {
- return maskSensitiveInformation("", configuration);
- }
-
- private String maskSensitiveInformation(String path, String configuration)
{
- boolean containsOnlySensitiveInformation =
sensitiveInformationPattern.matcher(path).find();
- Set<String> maskedKeys = containsOnlySensitiveInformation ?
Collections.emptySet() : keysToMask;
- return jsonMasker.mask(configuration, maskedKeys).toString();
- }
-
- private Pattern sensitiveInformationPattern(Set<String> keys) {
- String regexp = keys.stream()
- .map(it -> "(." + it + "$)")
- .collect(Collectors.joining("|"));
- return Pattern.compile(regexp);
- }
}
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/JsonMasker.java
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/JsonMasker.java
deleted file mode 100644
index 7e9174ac36..0000000000
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/JsonMasker.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.internal.rest.configuration;
-
-import static java.util.Collections.emptySet;
-
-import com.fasterxml.jackson.core.JsonFactory;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.fasterxml.jackson.databind.node.TextNode;
-import java.io.IOException;
-import java.util.Locale;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/**
- * JSON masker.
- */
-public class JsonMasker {
-
- private static final String MASKING_STRING = "******";
-
- private final ObjectMapper mapper = new ObjectMapper();
-
- /**
- * Masks the given JSON string.
- *
- * @param json JSON string.
- * @param keysToMask Set of keys to mask. If empty, all keys will be
masked.
- * @return Masked {@link JsonNode}.
- */
- public JsonNode mask(String json, Set<String> keysToMask) {
- Set<String> keysToMaskInLowerCase = keysToMask.stream()
- .map(it -> it.toLowerCase(Locale.ROOT))
- .collect(Collectors.toSet());
- return traverseAndMask(stringToJsonNode(json).deepCopy(),
keysToMaskInLowerCase);
- }
-
- private JsonNode traverseAndMask(JsonNode target, Set<String> keysToMask) {
- if (target.isTextual() && !target.textValue().isBlank() &&
keysToMask.isEmpty()) {
- return new TextNode(MASKING_STRING);
- }
-
- if (target.isArray()) {
- for (int i = 0; i < target.size(); i++) {
- ((ArrayNode) target).set(i, traverseAndMask(target.get(i),
keysToMask));
- }
- }
-
- if (target.isObject()) {
- ObjectNode targetObjectNode = (ObjectNode) target;
- target.fields().forEachRemaining(field -> {
- if (keysToMask.isEmpty()) {
- targetObjectNode.replace(field.getKey(),
traverseAndMask(field.getValue(), emptySet()));
- } else if
(keysToMask.contains(field.getKey().toLowerCase(Locale.ROOT))) {
- targetObjectNode.replace(field.getKey(),
traverseAndMask(field.getValue(), emptySet()));
- } else {
- traverseAndMask(field.getValue(), keysToMask);
- }
- }
- );
- }
-
- return target;
- }
-
- private JsonNode stringToJsonNode(String str) {
- JsonFactory factory = mapper.getFactory();
- try {
- JsonParser parser = factory.createParser(str);
- return mapper.readTree(parser);
- } catch (IOException e) {
- throw new IllegalArgumentException("Invalid JSON string: " + str,
e);
- }
- }
-}
diff --git
a/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/ConfigurationControllerBaseTest.java
b/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/ConfigurationControllerBaseTest.java
index 3c09cda83a..26a43a3b75 100644
---
a/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/ConfigurationControllerBaseTest.java
+++
b/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/ConfigurationControllerBaseTest.java
@@ -17,12 +17,13 @@
package org.apache.ignite.internal.rest.configuration;
-import static java.util.Collections.emptySet;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.matchesPattern;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import io.micronaut.context.ApplicationContext;
@@ -53,8 +54,6 @@ public abstract class ConfigurationControllerBaseTest {
private final Set<String> secretKeys = Set.of("password");
- private final JsonMasker jsonMasker = new JsonMasker();
-
@Inject
private EmbeddedServer server;
@@ -79,10 +78,14 @@ public abstract class ConfigurationControllerBaseTest {
@Test
void testGetConfig() {
var response = client().toBlocking().exchange("", String.class);
- var expectedBody = jsonMasker.mask(cfgPresentation.represent(),
secretKeys).toString();
+ var expectedBody = cfgPresentation.represent();
assertEquals(HttpStatus.OK, response.status());
assertEquals(expectedBody, response.body());
+ assertEquals(
+
"{\"root\":{\"foo\":\"foo\",\"sensitive\":{\"password\":\"********\"},\"subCfg\":{\"bar\":\"bar\"}}}",
+ response.body()
+ );
}
@Test
@@ -91,15 +94,17 @@ public abstract class ConfigurationControllerBaseTest {
assertEquals(HttpStatus.OK, response.status());
assertEquals(cfgPresentation.representByPath("root.subCfg"),
response.body());
+ assertNotNull(response.body());
}
@Test
void testGetSensitiveInformationByPath() {
var response =
client().toBlocking().exchange("/root.sensitive.password", String.class);
- var expectedBody =
jsonMasker.mask(cfgPresentation.representByPath("root.sensitive.password"),
emptySet()).toString();
+ var expectedBody =
cfgPresentation.representByPath("root.sensitive.password");
assertEquals(HttpStatus.OK, response.status());
assertEquals(expectedBody, response.body());
+ assertThat(response.body(), matchesPattern("^\\\"[\\*]+\\\"$"));
}
@Test
diff --git
a/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/JsonMaskerTest.java
b/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/JsonMaskerTest.java
deleted file mode 100644
index be4cc184d2..0000000000
---
a/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/JsonMaskerTest.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.internal.rest.configuration;
-
-import static java.util.Collections.emptySet;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import java.util.Set;
-import org.junit.jupiter.api.Test;
-
-class JsonMaskerTest {
-
- private final Set<String> keysToMask = Set.of("credentials");
- private final JsonMasker jsonMasker = new JsonMasker();
-
- @Test
- void notingToMask() {
- String json = "{\"name\": \"John Doe\", \"age\": 30}";
- String masked = jsonMasker.mask(json, keysToMask).toString();
- assertEquals("{\"name\":\"John Doe\",\"age\":30}", masked);
- }
-
- @Test
- void maskAll() {
- String json = "{\"name\": \"John Doe\", \"age\": 30}";
- String masked = jsonMasker.mask(json, emptySet()).toString();
- assertEquals("{\"name\":\"******\",\"age\":30}", masked);
- }
-
- @Test
- void maskField() {
- String json = "{\"name\": \"John Doe\", \"age\": 30, \"credentials\":
\"admin\"}";
- String masked = jsonMasker.mask(json, keysToMask).toString();
- assertEquals("{\"name\":\"John
Doe\",\"age\":30,\"credentials\":\"******\"}", masked);
- }
-
- @Test
- void maskNestedArray() {
- String json = "{\"name\": \"John Doe\", \"age\": 30, \"credentials\":
[\"admin1\", \"admin2\"]}";
- String masked = jsonMasker.mask(json, keysToMask).toString();
- assertEquals("{\"name\":\"John
Doe\",\"age\":30,\"credentials\":[\"******\",\"******\"]}", masked);
- }
-
- @Test
- void maskNestedObject() {
- String json = "{\n"
- + " \"name\": \"John Doe\",\n"
- + " \"age\": 30,\n"
- + " \"credentials\": {\n"
- + " \"basic\": {\n"
- + " \"user\": \"admin\",\n"
- + " \"password\": \"admin\"\n"
- + " }\n"
- + " }\n"
- + "}";
- String masked = jsonMasker.mask(json, keysToMask).toString();
- assertEquals(
- "{\"name\":\"John
Doe\",\"age\":30,\"credentials\":{\"basic\":{\"user\":\"******\",\"password\":\"******\"}}}",
- masked
- );
- }
-
- @Test
- void booleanNotMasked() {
- String json = "{\"name\": \"John Doe\", \"age\": 30, \"credentials\":
true}";
- String masked = jsonMasker.mask(json, keysToMask).toString();
- assertEquals("{\"name\":\"John
Doe\",\"age\":30,\"credentials\":true}", masked);
- }
-
- @Test
- void numberNotMasked() {
- String json = "{\"name\": \"John Doe\", \"age\": 30, \"credentials\":
123}";
- String masked = jsonMasker.mask(json, keysToMask).toString();
- assertEquals("{\"name\":\"John Doe\",\"age\":30,\"credentials\":123}",
masked);
- }
-
- @Test
- void emptyNotMasked() {
- String json = "{\"name\": \"John Doe\", \"age\": 30, \"credentials\":
\"\"}";
- String masked = jsonMasker.mask(json, keysToMask).toString();
- assertEquals("{\"name\":\"John
Doe\",\"age\":30,\"credentials\":\"\"}", masked);
- }
-
- @Test
- void fieldInUpperCase() {
- String json = "{\"name\": \"John Doe\", \"age\": 30, \"CREDENTIALS\":
\"admin\"}";
- String masked = jsonMasker.mask(json, keysToMask).toString();
- assertEquals("{\"name\":\"John
Doe\",\"age\":30,\"CREDENTIALS\":\"******\"}", masked);
- }
-}
diff --git
a/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/TestSubSensitiveConfigurationSchema.java
b/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/TestSubSensitiveConfigurationSchema.java
index acfb752c1f..e3aeb5d95b 100644
---
a/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/TestSubSensitiveConfigurationSchema.java
+++
b/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/TestSubSensitiveConfigurationSchema.java
@@ -18,6 +18,7 @@
package org.apache.ignite.internal.rest.configuration;
import org.apache.ignite.configuration.annotation.Config;
+import org.apache.ignite.configuration.annotation.Secret;
import org.apache.ignite.configuration.annotation.Value;
/**
@@ -26,6 +27,7 @@ import org.apache.ignite.configuration.annotation.Value;
@Config
public class TestSubSensitiveConfigurationSchema {
+ @Secret
@Value(hasDefault = true)
public String password = "";
}
diff --git
a/modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java
b/modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java
index b52d5715a3..7afc824278 100644
---
a/modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java
+++
b/modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java
@@ -261,7 +261,15 @@ public class LocalFileConfigurationStorage implements
ConfigurationStorage {
fillFromPrefixMap(rootNode, toPrefixMap(latest));
- Object transformed = rootNode.accept(null, null, new
ConverterToMapVisitor(false, true));
+ Object transformed = rootNode.accept(
+ null,
+ null,
+ ConverterToMapVisitor.builder()
+ .includeInternal(false)
+ .skipEmptyValues(true)
+ .maskSecretValues(false)
+ .build()
+ );
ConfigValue conf = ConfigImpl.fromAnyRef(transformed, null);
diff --git
a/modules/security/src/main/java/org/apache/ignite/internal/configuration/BasicAuthenticationProviderConfigurationSchema.java
b/modules/security/src/main/java/org/apache/ignite/internal/configuration/BasicAuthenticationProviderConfigurationSchema.java
index f639362c89..52415f4cc8 100644
---
a/modules/security/src/main/java/org/apache/ignite/internal/configuration/BasicAuthenticationProviderConfigurationSchema.java
+++
b/modules/security/src/main/java/org/apache/ignite/internal/configuration/BasicAuthenticationProviderConfigurationSchema.java
@@ -18,6 +18,7 @@
package org.apache.ignite.internal.configuration;
import org.apache.ignite.configuration.annotation.PolymorphicConfigInstance;
+import org.apache.ignite.configuration.annotation.Secret;
import org.apache.ignite.configuration.annotation.Value;
/** Basic authentication configuration. */
@@ -29,6 +30,7 @@ public class BasicAuthenticationProviderConfigurationSchema
extends Authenticati
public String username;
/** Password. */
+ @Secret
@Value
public String password;
}
diff --git
a/modules/security/src/main/java/org/apache/ignite/internal/configuration/BasicAuthenticationProviderConfigurationSchema.java
b/modules/security/src/test/java/org/apache/ignite/internal/configuration/BasicAuthenticationProviderConfigurationSchemaTest.java
similarity index 57%
copy from
modules/security/src/main/java/org/apache/ignite/internal/configuration/BasicAuthenticationProviderConfigurationSchema.java
copy to
modules/security/src/test/java/org/apache/ignite/internal/configuration/BasicAuthenticationProviderConfigurationSchemaTest.java
index f639362c89..13f82c1502 100644
---
a/modules/security/src/main/java/org/apache/ignite/internal/configuration/BasicAuthenticationProviderConfigurationSchema.java
+++
b/modules/security/src/test/java/org/apache/ignite/internal/configuration/BasicAuthenticationProviderConfigurationSchemaTest.java
@@ -17,18 +17,22 @@
package org.apache.ignite.internal.configuration;
-import org.apache.ignite.configuration.annotation.PolymorphicConfigInstance;
-import org.apache.ignite.configuration.annotation.Value;
+import static org.junit.jupiter.api.Assertions.assertTrue;
-/** Basic authentication configuration. */
-@PolymorphicConfigInstance(AuthenticationProviderConfigurationSchema.TYPE_BASIC)
-public class BasicAuthenticationProviderConfigurationSchema extends
AuthenticationProviderConfigurationSchema {
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import org.apache.ignite.configuration.annotation.Secret;
+import org.junit.jupiter.api.Test;
- /** Username. */
- @Value
- public String username;
+class BasicAuthenticationProviderConfigurationSchemaTest {
- /** Password. */
- @Value
- public String password;
+ @Test
+ public void passwordIsSecret() {
+ Field password =
Arrays.stream(BasicAuthenticationProviderConfigurationSchema.class.getDeclaredFields())
+ .filter(it -> it.getName().equals("password"))
+ .findFirst()
+ .orElseThrow();
+
+ assertTrue(password.isAnnotationPresent(Secret.class));
+ }
}