This is an automated email from the ASF dual-hosted git repository.
jerryshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 422bf772b5 [#13294] improvement(core): Make sensitive property key
keywords configurable (#13295)
422bf772b5 is described below
commit 422bf772b597d08ed475431cc1395615828ae143
Author: MaSai <[email protected]>
AuthorDate: Fri Sep 18 16:38:21 2026 +0800
[#13294] improvement(core): Make sensitive property key keywords
configurable (#13295)
### What changes were proposed in this pull request?
- Replace the always-on sensitive key matcher with one configurable
keyword list.
- New server config: `gravitino.secret.sensitiveKeyKeywords`
(comma-separated, case-insensitive literal substring match, not a
regular expression).
- Default remains `secret,password,token,credential,access,account`. A
configured value replaces that list, so a deployment can drop a keyword
such as `access` or `account`, add one such as `private` or `passwrod`,
or set an empty value to disable name-based matching.
- Initialize the matcher from server config in `GravitinoEnv`.
- Document the setting in `gravitino.conf.template` and the server
config docs.
- Add unit tests for replacement, empty list, masking, and
`buildSecrets`.
### Why are the changes needed?
#12983 masks and recovers credential-like properties when the key name
contains `secret`, `password`, `token`, `credential`, `access`, or
`account`. That list is not right for every deployment:
- Some keys only match a broad default such as `access` or `account` and
should not be masked.
- Common misspellings such as `jdbc-passwrod`, and extra words such as
`private`, are not in the default list.
One replaceable list covers both cases. Keeping the defaults hardcoded
would make it impossible to turn a default keyword off.
Fix: #13294
### Does this PR introduce _any_ user-facing change?
Yes.
1. New server configuration key: `gravitino.secret.sensitiveKeyKeywords`
2. The value is a comma-separated list and replaces the default. Unset,
behavior matches #12983. Example:
`secret,password,token,credential,private,passwrod`
3. An empty value disables name-based masking.
### How was this patch tested?
```bash
./gradlew :core:spotlessApply
./gradlew :core:test \
--tests 'org.apache.gravitino.secret.TestSensitivePropertyKeyMatcher' \
--tests 'org.apache.gravitino.secret.TestSecretPropertyUtils' \
--tests 'org.apache.gravitino.connector.TestHiddenPropertyMaskUtils' \
-PskipITs
```
---------
Co-authored-by: Cursor <[email protected]>
---
conf/gravitino.conf.template | 7 ++
.../main/java/org/apache/gravitino/Configs.java | 20 +++++
.../java/org/apache/gravitino/GravitinoEnv.java | 3 +
.../gravitino/secret/SecretPropertyUtils.java | 34 +++++----
.../secret/SensitivePropertyKeyKeywords.java | 70 +++++++++++++++++
.../secret/SensitivePropertyKeyMatcher.java | 82 ++++++++++++++++++++
.../connector/TestHiddenPropertyMaskUtils.java | 33 ++++++++
.../gravitino/secret/TestSecretPropertyUtils.java | 18 +++++
.../secret/TestSensitivePropertyKeyMatcher.java | 87 ++++++++++++++++++++++
docs/gravitino-server-config.md | 17 +++++
10 files changed, 356 insertions(+), 15 deletions(-)
diff --git a/conf/gravitino.conf.template b/conf/gravitino.conf.template
index 4202d1d174..f5732377fc 100644
--- a/conf/gravitino.conf.template
+++ b/conf/gravitino.conf.template
@@ -140,3 +140,10 @@ gravitino.lance-rest.namespace-backend = gravitino
# gravitino.lance-rest.gravitino-uri = http://localhost:8090
# The metalake name used for Lance REST service gravitino namespace backend,
please create the metalake first before using it, and configure the metalake
name here.
# gravitino.lance-rest.gravitino-metalake = metalake
+
+# THE CONFIGURATION FOR sensitive property key matching
+# Comma-separated keywords. Each entry is a literal substring, not a regular
expression.
+# This list replaces the default
(secret,password,token,credential,access,account).
+# Omit a default keyword to stop masking keys that only match that word. An
empty value
+# disables name-based matching.
+# gravitino.secret.sensitiveKeyKeywords =
secret,password,token,credential,private,passwrod
diff --git a/core/src/main/java/org/apache/gravitino/Configs.java
b/core/src/main/java/org/apache/gravitino/Configs.java
index 863492abf7..23fd879df1 100644
--- a/core/src/main/java/org/apache/gravitino/Configs.java
+++ b/core/src/main/java/org/apache/gravitino/Configs.java
@@ -28,6 +28,7 @@ import org.apache.gravitino.audit.v2.SimpleFormatterV2;
import org.apache.gravitino.config.ConfigBuilder;
import org.apache.gravitino.config.ConfigConstants;
import org.apache.gravitino.config.ConfigEntry;
+import org.apache.gravitino.secret.SensitivePropertyKeyKeywords;
import org.apache.gravitino.stats.storage.JdbcPartitionStatisticStorageFactory;
import org.apache.gravitino.utils.FileFetcher;
import org.apache.gravitino.utils.HierarchicalSchemaUtil;
@@ -636,4 +637,23 @@ public class Configs {
.version(ConfigConstants.VERSION_1_0_0)
.stringConf()
.createWithDefault(JdbcPartitionStatisticStorageFactory.class.getCanonicalName());
+
+ public static final ConfigEntry<List<String>> SENSITIVE_KEY_KEYWORDS =
+ new ConfigBuilder("gravitino.secret.sensitiveKeyKeywords")
+ .doc(
+ "Comma-separated property key keywords treated as
credential-like. Matching is "
+ + "case-insensitive; each entry is a literal substring of
the property key, "
+ + "not a regular expression. This list replaces the default "
+ + "(secret, password, token, credential, access, account).
Set a shorter list "
+ + "to stop masking keys that only match a default keyword,
add words such as "
+ + "private or passwrod, or set an empty value to disable
name-based matching.")
+ .version(ConfigConstants.VERSION_2_0_0)
+ .stringConf()
+ .toSequence()
+ .checkValue(
+ valueList ->
+ valueList != null
+ &&
valueList.stream().allMatch(SensitivePropertyKeyKeywords::isValidKeyword),
+ SensitivePropertyKeyKeywords.invalidKeywordMessage())
+ .createWithDefault(SensitivePropertyKeyKeywords.defaultKeywords());
}
diff --git a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
index a635e62577..5b38b7182e 100644
--- a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
+++ b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
@@ -105,6 +105,7 @@ import org.apache.gravitino.policy.PolicyDispatcher;
import org.apache.gravitino.policy.PolicyManager;
import org.apache.gravitino.secret.SecretManager;
import org.apache.gravitino.secret.SecretPropertyOperationDispatcher;
+import org.apache.gravitino.secret.SecretPropertyUtils;
import org.apache.gravitino.secret.SecretProviderRegistry;
import org.apache.gravitino.stats.StatisticDispatcher;
import org.apache.gravitino.stats.StatisticManager;
@@ -230,6 +231,7 @@ public class GravitinoEnv {
LOG.info("Initializing Gravitino base environment...");
this.config = config;
FileFetcher.get().initialize(config.get(Configs.BLOCK_UNSAFE_REMOTE_URI));
+ SecretPropertyUtils.configureSensitiveKeyKeywords(config);
this.manageFullComponents = false;
initBaseComponents();
LOG.info("Gravitino base environment is initialized.");
@@ -244,6 +246,7 @@ public class GravitinoEnv {
LOG.info("Initializing Gravitino full environment...");
this.config = config;
FileFetcher.get().initialize(config.get(Configs.BLOCK_UNSAFE_REMOTE_URI));
+ SecretPropertyUtils.configureSensitiveKeyKeywords(config);
this.manageFullComponents = true;
initBaseComponents();
initGravitinoServerComponents();
diff --git
a/core/src/main/java/org/apache/gravitino/secret/SecretPropertyUtils.java
b/core/src/main/java/org/apache/gravitino/secret/SecretPropertyUtils.java
index 19f0b89507..b1bece5bce 100644
--- a/core/src/main/java/org/apache/gravitino/secret/SecretPropertyUtils.java
+++ b/core/src/main/java/org/apache/gravitino/secret/SecretPropertyUtils.java
@@ -25,9 +25,10 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
import org.apache.gravitino.connector.PropertiesMetadata;
import org.apache.gravitino.connector.PropertyEntry;
@@ -39,14 +40,6 @@ import org.apache.gravitino.connector.PropertyEntry;
*/
public final class SecretPropertyUtils {
- /**
- * Property keys whose names look like credentials. Matching is
case-insensitive. Used to mask API
- * responses and to expose plaintext via {@code getSecrets} for undeclared /
mistyped credential
- * properties.
- */
- private static final Pattern SENSITIVE_PROPERTY_KEY_PATTERN =
- Pattern.compile(".*(secret|password|token|credential|access|account).*");
-
/** Empty metadata: every property key is undeclared (used for historical
fuzzy recovery). */
private static final PropertiesMetadata EMPTY_PROPERTIES_METADATA =
new PropertiesMetadata() {
@@ -58,22 +51,33 @@ public final class SecretPropertyUtils {
private SecretPropertyUtils() {}
+ /**
+ * Replaces the sensitive property key keywords from server configuration.
+ *
+ * @param config server configuration
+ */
+ public static void configureSensitiveKeyKeywords(Config config) {
+ SensitivePropertyKeyMatcher.configure(config);
+ }
+
/**
* Returns whether a property key name looks sensitive (credential-like).
*
- * <p>A key matches when, after lower-casing, it contains {@code secret},
{@code password}, {@code
- * token}, {@code credential}, {@code access}, or {@code account} as a
substring (covers Azure
- * storage account key/name and GCS service-account file paths). Underscores
and hyphens are not
- * normalized; they are irrelevant because the matched keywords contain
neither.
+ * <p>A key matches when, after lower-casing, it contains one of the active
keywords as a literal
+ * substring. The default keywords are {@code secret}, {@code password},
{@code token}, {@code
+ * credential}, {@code access}, and {@code account} (covers Azure storage
account key/name and GCS
+ * service-account file paths). {@link Configs#SENSITIVE_KEY_KEYWORDS}
replaces that set, so a
+ * deployment can drop a default keyword or add another. Underscores and
hyphens are not
+ * normalized; they are irrelevant because the default keywords contain
neither.
*
* @param key the property key
- * @return true when the key name matches the sensitive pattern
+ * @return true when the key name matches an active keyword
*/
public static boolean isSensitivePropertyKey(@Nullable String key) {
if (key == null || key.isEmpty()) {
return false;
}
- return
SENSITIVE_PROPERTY_KEY_PATTERN.matcher(key.toLowerCase(Locale.ROOT)).matches();
+ return SensitivePropertyKeyMatcher.matches(key.toLowerCase(Locale.ROOT));
}
/**
diff --git
a/core/src/main/java/org/apache/gravitino/secret/SensitivePropertyKeyKeywords.java
b/core/src/main/java/org/apache/gravitino/secret/SensitivePropertyKeyKeywords.java
new file mode 100644
index 0000000000..9b6e076f18
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/secret/SensitivePropertyKeyKeywords.java
@@ -0,0 +1,70 @@
+/*
+ * 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.gravitino.secret;
+
+import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Default credential-like substrings used for sensitive property key
detection.
+ *
+ * <p>These keywords are the default value of {@link
+ * org.apache.gravitino.Configs#SENSITIVE_KEY_KEYWORDS}. A configured list
replaces them entirely.
+ */
+public final class SensitivePropertyKeyKeywords {
+
+ static final List<String> DEFAULT_KEYWORDS =
+ List.of("secret", "password", "token", "credential", "access",
"account");
+
+ private static final String INVALID_KEYWORD_MSG = "Sensitive key keywords
must be non-blank";
+
+ private SensitivePropertyKeyKeywords() {}
+
+ /**
+ * Returns the default sensitive property key keywords.
+ *
+ * @return an immutable list of default keywords
+ */
+ public static List<String> defaultKeywords() {
+ return DEFAULT_KEYWORDS;
+ }
+
+ /**
+ * Returns whether {@code keyword} may appear in {@link
+ * org.apache.gravitino.Configs#SENSITIVE_KEY_KEYWORDS}.
+ *
+ * <p>Blank entries are rejected. The configured list replaces the defaults,
so a deployment can
+ * omit a default keyword such as {@code access} or add one such as {@code
private}.
+ *
+ * @param keyword candidate keyword
+ * @return true when the keyword may be configured
+ */
+ public static boolean isValidKeyword(String keyword) {
+ return StringUtils.isNotBlank(keyword);
+ }
+
+ /**
+ * Returns the validation message for an illegal sensitive key keyword.
+ *
+ * @return the validation message
+ */
+ public static String invalidKeywordMessage() {
+ return INVALID_KEYWORD_MSG;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/secret/SensitivePropertyKeyMatcher.java
b/core/src/main/java/org/apache/gravitino/secret/SensitivePropertyKeyMatcher.java
new file mode 100644
index 0000000000..767b2c479f
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/secret/SensitivePropertyKeyMatcher.java
@@ -0,0 +1,82 @@
+/*
+ * 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.gravitino.secret;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
+
+/**
+ * Matcher for credential-like property key keywords.
+ *
+ * <p>The active set starts as {@link
SensitivePropertyKeyKeywords#defaultKeywords()}. Server
+ * configuration replaces that set entirely. Each keyword is matched as a
case-insensitive literal
+ * substring of the property key.
+ */
+final class SensitivePropertyKeyMatcher {
+
+ private static final Set<String> DEFAULT_KEYWORDS =
+ ImmutableSet.copyOf(SensitivePropertyKeyKeywords.defaultKeywords());
+
+ private static volatile Set<String> keywords = DEFAULT_KEYWORDS;
+
+ private SensitivePropertyKeyMatcher() {}
+
+ static boolean matches(String lowerKey) {
+ for (String keyword : keywords) {
+ if (lowerKey.contains(keyword)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Replaces the active sensitive key keywords from Gravitino configuration.
+ *
+ * @param config server configuration
+ */
+ static void configure(Config config) {
+ configure(config.get(Configs.SENSITIVE_KEY_KEYWORDS));
+ }
+
+ static void configure(List<String> configuredKeywords) {
+ Set<String> normalizedKeywords = new LinkedHashSet<>();
+ if (configuredKeywords != null) {
+ for (String keyword : configuredKeywords) {
+ Preconditions.checkArgument(
+ SensitivePropertyKeyKeywords.isValidKeyword(keyword),
+ SensitivePropertyKeyKeywords.invalidKeywordMessage());
+ normalizedKeywords.add(keyword.trim().toLowerCase(Locale.ROOT));
+ }
+ }
+ keywords = ImmutableSet.copyOf(normalizedKeywords);
+ }
+
+ @VisibleForTesting
+ static void resetToDefaults() {
+ keywords = DEFAULT_KEYWORDS;
+ }
+}
diff --git
a/core/src/test/java/org/apache/gravitino/connector/TestHiddenPropertyMaskUtils.java
b/core/src/test/java/org/apache/gravitino/connector/TestHiddenPropertyMaskUtils.java
index 0a04e119ff..913d9ef358 100644
---
a/core/src/test/java/org/apache/gravitino/connector/TestHiddenPropertyMaskUtils.java
+++
b/core/src/test/java/org/apache/gravitino/connector/TestHiddenPropertyMaskUtils.java
@@ -20,12 +20,22 @@ package org.apache.gravitino.connector;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import java.util.List;
import java.util.Map;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
+import org.apache.gravitino.secret.SecretPropertyUtils;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class TestHiddenPropertyMaskUtils {
+ @AfterEach
+ void resetAdditionalMatcher() {
+ SecretPropertyUtils.configureSensitiveKeyKeywords(new Config(false) {});
+ }
+
@Test
void testMaskHiddenPropertiesByName() {
Map<String, String> properties =
@@ -170,4 +180,27 @@ public class TestHiddenPropertyMaskUtils {
Assertions.assertEquals(
HiddenPropertyMaskUtils.MASKED_VALUE,
masked.get("azure-storage-account-key"));
}
+
+ @Test
+ void testMaskHiddenPropertiesMasksAdditionalSensitiveKey() {
+ Config config = new Config(false) {};
+ config.set(Configs.SENSITIVE_KEY_KEYWORDS, List.of("passwrod"));
+ SecretPropertyUtils.configureSensitiveKeyKeywords(config);
+ PropertiesMetadata metadata =
+ new PropertiesMetadata() {
+ @Override
+ public Map<String, PropertyEntry<?>> propertyEntries() {
+ return ImmutableMap.of(
+ "jdbc-user",
+ PropertyEntry.stringOptionalPropertyEntry("jdbc-user", "user",
false, null, false));
+ }
+ };
+
+ Map<String, String> properties =
+ ImmutableMap.of("jdbc-user", "root", "jdbc-passwrod", "typo-secret");
+ Map<String, String> masked =
HiddenPropertyMaskUtils.maskHiddenProperties(properties, metadata);
+
+ Assertions.assertEquals("root", masked.get("jdbc-user"));
+ Assertions.assertEquals(HiddenPropertyMaskUtils.MASKED_VALUE,
masked.get("jdbc-passwrod"));
+ }
}
diff --git
a/core/src/test/java/org/apache/gravitino/secret/TestSecretPropertyUtils.java
b/core/src/test/java/org/apache/gravitino/secret/TestSecretPropertyUtils.java
index b0c9da8080..f50e85ad4d 100644
---
a/core/src/test/java/org/apache/gravitino/secret/TestSecretPropertyUtils.java
+++
b/core/src/test/java/org/apache/gravitino/secret/TestSecretPropertyUtils.java
@@ -27,11 +27,17 @@ import org.apache.gravitino.Config;
import org.apache.gravitino.connector.PropertiesMetadata;
import org.apache.gravitino.connector.PropertyEntry;
import org.apache.gravitino.secret.memory.InMemorySecretsProvider;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class TestSecretPropertyUtils {
+ @AfterEach
+ void resetAdditionalMatcher() {
+ SensitivePropertyKeyMatcher.resetToDefaults();
+ }
+
@Test
void testAssembleAndWrite() {
try (SecretManager sm = memorySecretManager()) {
@@ -134,6 +140,7 @@ public class TestSecretPropertyUtils {
Assertions.assertTrue(SecretPropertyUtils.isSensitivePropertyKey("azure-storage-account-key"));
Assertions.assertTrue(SecretPropertyUtils.isSensitivePropertyKey("azure-storage-account-name"));
Assertions.assertTrue(SecretPropertyUtils.isSensitivePropertyKey("gcs-service-account-file"));
+
Assertions.assertFalse(SecretPropertyUtils.isSensitivePropertyKey("jdbc-passwrod"));
Assertions.assertFalse(SecretPropertyUtils.isSensitivePropertyKey("jdbc-user"));
Assertions.assertFalse(SecretPropertyUtils.isSensitivePropertyKey("warehouse"));
Assertions.assertFalse(SecretPropertyUtils.isSensitivePropertyKey("aws-region"));
@@ -141,6 +148,17 @@ public class TestSecretPropertyUtils {
Assertions.assertFalse(SecretPropertyUtils.isSensitivePropertyKey(""));
}
+ @Test
+ void testBuildSecretsIncludesAdditionalSensitiveKey() {
+ SensitivePropertyKeyMatcher.configure(List.of("passwrod"));
+ try (SecretManager sm = memorySecretManager()) {
+ Map<String, String> entityProps = Map.of("jdbc-passwrod", "typo-secret",
"jdbc-user", "root");
+ Map<String, String> secrets = SecretPropertyUtils.buildSecrets(sm,
entityProps);
+ Assertions.assertEquals("typo-secret", secrets.get("jdbc-passwrod"));
+ Assertions.assertFalse(secrets.containsKey("jdbc-user"));
+ }
+ }
+
@Test
void testBuildSecretsIncludesInlineSensitivePlaintext() {
try (SecretManager sm = memorySecretManager()) {
diff --git
a/core/src/test/java/org/apache/gravitino/secret/TestSensitivePropertyKeyMatcher.java
b/core/src/test/java/org/apache/gravitino/secret/TestSensitivePropertyKeyMatcher.java
new file mode 100644
index 0000000000..4e01a4f4d9
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/secret/TestSensitivePropertyKeyMatcher.java
@@ -0,0 +1,87 @@
+/*
+ * 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.gravitino.secret;
+
+import java.util.List;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestSensitivePropertyKeyMatcher {
+
+ @AfterEach
+ void resetMatcher() {
+ SensitivePropertyKeyMatcher.resetToDefaults();
+ }
+
+ @Test
+ void testDefaultKeywordsMatchBuiltinNames() {
+
Assertions.assertTrue(SecretPropertyUtils.isSensitivePropertyKey("jdbc-password"));
+
Assertions.assertTrue(SecretPropertyUtils.isSensitivePropertyKey("aws-access-key-id"));
+
Assertions.assertFalse(SecretPropertyUtils.isSensitivePropertyKey("jdbc-passwrod"));
+ }
+
+ @Test
+ void testConfiguredKeywordsReplaceDefaults() {
+ SensitivePropertyKeyMatcher.configure(List.of("passwrod", "secert",
"private"));
+
Assertions.assertTrue(SensitivePropertyKeyMatcher.matches("jdbc-passwrod"));
+
Assertions.assertTrue(SensitivePropertyKeyMatcher.matches("catalog.secert"));
+
Assertions.assertTrue(SecretPropertyUtils.isSensitivePropertyKey("jdbc-private-key"));
+
Assertions.assertFalse(SensitivePropertyKeyMatcher.matches("jdbc-passord"));
+
Assertions.assertFalse(SecretPropertyUtils.isSensitivePropertyKey("jdbc-password"));
+
Assertions.assertFalse(SecretPropertyUtils.isSensitivePropertyKey("aws-access-key-id"));
+ }
+
+ @Test
+ void testKeywordsAreCaseInsensitive() {
+ SensitivePropertyKeyMatcher.configure(List.of("PASSWROD"));
+
Assertions.assertTrue(SensitivePropertyKeyMatcher.matches("jdbc-passwrod"));
+ }
+
+ @Test
+ void testEmptyKeywordListDisablesNameMatching() {
+ SensitivePropertyKeyMatcher.configure(List.of());
+
Assertions.assertFalse(SecretPropertyUtils.isSensitivePropertyKey("jdbc-password"));
+
Assertions.assertFalse(SecretPropertyUtils.isSensitivePropertyKey("aws-access-key-id"));
+ SensitivePropertyKeyMatcher.resetToDefaults();
+
Assertions.assertTrue(SecretPropertyUtils.isSensitivePropertyKey("jdbc-password"));
+ }
+
+ @Test
+ void testRejectsBlankKeyword() {
+ IllegalArgumentException exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> SensitivePropertyKeyMatcher.configure(List.of(" ")));
+ Assertions.assertTrue(
+
exception.getMessage().contains(SensitivePropertyKeyKeywords.invalidKeywordMessage()));
+
Assertions.assertTrue(SecretPropertyUtils.isSensitivePropertyKey("jdbc-password"));
+ }
+
+ @Test
+ void testConfigReplacesDefaultKeywords() {
+ Config config = new Config(false) {};
+ config.set(Configs.SENSITIVE_KEY_KEYWORDS, List.of("private"));
+ SecretPropertyUtils.configureSensitiveKeyKeywords(config);
+
Assertions.assertTrue(SecretPropertyUtils.isSensitivePropertyKey("jdbc-private-key"));
+
Assertions.assertFalse(SecretPropertyUtils.isSensitivePropertyKey("jdbc-password"));
+ }
+}
diff --git a/docs/gravitino-server-config.md b/docs/gravitino-server-config.md
index 3297ba7150..96422306c8 100644
--- a/docs/gravitino-server-config.md
+++ b/docs/gravitino-server-config.md
@@ -348,6 +348,23 @@ vended credentials; the mechanism it opts out of is
described in
| `gravitino.catalog.classloader.sharing.enabled` | Whether catalogs whose
isolation-relevant properties match may share one classloader. Sharing reduces
Metaspace usage; disabling it gives every catalog its own.
| `true` |
| `gravitino.catalog.credential.backfillToProperties` | Whether to return
hidden catalog credentials such as `jdbc-password` in the catalog properties
response, for connectors that cannot consume vended credentials. Anyone who can
read catalog properties can then read those credentials. Turn it off once your
connectors are upgraded. | `false` |
+### Sensitive property key matching
+
+Gravitino masks credential-like property keys on list/get responses and can
recover undeclared
+inline values via `getSecrets`. By default, a key matches when its name
contains `secret`,
+`password`, `token`, `credential`, `access`, or `account` (case-insensitive).
+
+`gravitino.secret.sensitiveKeyKeywords` **replaces** that default list. Use it
to drop a default
+keyword that masks unrelated properties (for example omit `access` and
`account`), to add a typo
+or extra word (for example `passwrod` or `private`), or set it to empty to
disable name-based
+matching. The value is a comma-separated list. Each entry is a
case-insensitive literal substring
+of the property key, not a regular expression. Keep entries specific; overly
broad values such as
+`key` can mask unrelated properties.
+
+| Configuration Item | Description
|
Default Value |
+|-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------|
+| `gravitino.secret.sensitiveKeyKeywords` | Comma-separated keywords for
credential-like property keys. Replaces the default list. Each entry is a
literal substring, not a regular expression. An empty value disables name-based
matching. | `secret,password,token,credential,access,account` |
+
### Securing the Server
#### Authentication