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

Reply via email to