This is an automated email from the ASF dual-hosted git repository.
roryqi 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 7f1dd0b182 [#14305,#14306,#14437,#14442] fix(secret): Harden secret
binding and catalog ops (#14479)
7f1dd0b182 is described below
commit 7f1dd0b182cd42f6fa3e48d6ecfa52b8d7349c73
Author: MaSai <[email protected]>
AuthorDate: Tue Nov 3 16:15:01 2026 +0800
[#14305,#14306,#14437,#14442] fix(secret): Harden secret binding and
catalog ops (#14479)
### What changes were proposed in this pull request?
- Map unsupported metadata-object secret reads to 400
(`IllegalArgumentException` / legacy `NotSupportedException`).
- Reject create-time `secretBindings` whose plaintext is the masked
placeholder `******` (same rule as alter `setSecretBinding`).
- Reject scalar/array `SecretBindingDTO` values without quoting them,
and omit Jackson stacks from malformed-JSON 400 responses so plaintext
is not echoed.
- Add `SecretManager.toPlaintextOrMasked` for catalog load so list /
disable / drop / re-bind still work when a bound secret cannot be
resolved; isolate listing failures per catalog.
### Why are the changes needed?
Fixes #14305, #14306, #14437, #14442.
Incomplete or missing secrets currently surface as 500s, leak plaintext
in error stacks, accept the read-path mask as a real secret on create,
or brick catalog management and detailed listing when the provider no
longer has the material.
### Does this PR introduce _any_ user-facing change?
Yes.
- Unsupported object-type secret GETs return 400 instead of 500.
- Create with `plaintext: "******"` is rejected.
- Malformed `secretBindings` 400 responses no longer include Jackson
stacks that may quote secrets.
- Catalogs whose secrets cannot be resolved remain manageable (list /
disable / drop / re-bind).
### How was this patch tested?
- `./gradlew :common:test --tests
org.apache.gravitino.dto.secret.TestSecretBindingDTO`
- `./gradlew :core:test --tests
org.apache.gravitino.secret.TestSecretManager --tests
org.apache.gravitino.catalog.TestCatalogManager.testCreateRejectsMaskedPlaceholderSecretBinding
--tests
org.apache.gravitino.catalog.TestCatalogManager.testCatalogManageableWhenSecretMissing`
- `./gradlew :server:test --tests
org.apache.gravitino.server.web.rest.TestExceptionHandlers.testUnsupportedSecretObjectTypeReturnsBadRequest
--tests
org.apache.gravitino.server.web.mapper.TestJsonMappingExceptionMapper`
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Cursor <[email protected]>
---
.../gravitino/dto/secret/SecretBindingDTO.java | 46 +++++++++++++
.../gravitino/dto/secret/TestSecretBindingDTO.java | 66 +++++++++++++++++++
.../apache/gravitino/catalog/CatalogManager.java | 30 ++++++++-
.../apache/gravitino/connector/BaseCatalog.java | 4 +-
.../org/apache/gravitino/secret/SecretManager.java | 46 ++++++++++++-
.../gravitino/catalog/TestCatalogManager.java | 77 ++++++++++++++++++++++
.../apache/gravitino/secret/TestSecretManager.java | 35 ++++++++++
.../web/mapper/JsonMappingExceptionMapper.java | 4 +-
.../server/web/rest/ExceptionHandlers.java | 3 +-
.../web/rest/MetadataObjectSecretOperations.java | 3 +-
.../web/mapper/TestJsonMappingExceptionMapper.java | 5 +-
.../server/web/rest/TestExceptionHandlers.java | 14 ++++
12 files changed, 322 insertions(+), 11 deletions(-)
diff --git
a/common/src/main/java/org/apache/gravitino/dto/secret/SecretBindingDTO.java
b/common/src/main/java/org/apache/gravitino/dto/secret/SecretBindingDTO.java
index fd029fdb39..ace17440bb 100644
--- a/common/src/main/java/org/apache/gravitino/dto/secret/SecretBindingDTO.java
+++ b/common/src/main/java/org/apache/gravitino/dto/secret/SecretBindingDTO.java
@@ -19,7 +19,14 @@
package org.apache.gravitino.dto.secret;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
import java.util.Map;
import javax.annotation.Nullable;
import lombok.AccessLevel;
@@ -38,6 +45,7 @@ import org.apache.gravitino.secret.SecretBinding;
@AllArgsConstructor
@Builder(setterPrefix = "with")
@ToString(exclude = "plaintext")
+@JsonDeserialize(using = SecretBindingDTO.Deserializer.class)
public class SecretBindingDTO {
@JsonProperty("provider")
@@ -105,4 +113,42 @@ public class SecretBindingDTO {
}
return dtos.build();
}
+
+ /**
+ * Rejects scalar / array {@code secretBindings} values without quoting them
in the exception
+ * message, so a mistaken bare-string binding does not echo the secret in
the HTTP 400 stack.
+ */
+ public static final class Deserializer extends
JsonDeserializer<SecretBindingDTO> {
+
+ @Override
+ public SecretBindingDTO deserialize(JsonParser p, DeserializationContext
ctxt)
+ throws IOException {
+ JsonNode node = p.getCodec().readTree(p);
+ if (node == null || node.isNull()) {
+ return null;
+ }
+ if (!node.isObject()) {
+ // Pass a null parser so Jackson does not append the input source
(which may be the
+ // secret plaintext) into the exception message / HTTP error stack.
+ throw new JsonMappingException(
+ null,
+ "secretBindings values must be JSON objects with 'provider' and
'plaintext', not a"
+ + " scalar or array");
+ }
+ if (node.has("plaintext")
+ && !node.get("plaintext").isNull()
+ && !node.get("plaintext").isTextual()) {
+ throw new JsonMappingException(null, "secretBindings.plaintext must be
a string");
+ }
+ String provider =
+ node.has("provider") && !node.get("provider").isNull()
+ ? node.get("provider").asText()
+ : null;
+ String plaintext =
+ node.has("plaintext") && !node.get("plaintext").isNull()
+ ? node.get("plaintext").asText()
+ : null;
+ return
SecretBindingDTO.builder().withProvider(provider).withPlaintext(plaintext).build();
+ }
+ }
}
diff --git
a/common/src/test/java/org/apache/gravitino/dto/secret/TestSecretBindingDTO.java
b/common/src/test/java/org/apache/gravitino/dto/secret/TestSecretBindingDTO.java
new file mode 100644
index 0000000000..d49efc13d6
--- /dev/null
+++
b/common/src/test/java/org/apache/gravitino/dto/secret/TestSecretBindingDTO.java
@@ -0,0 +1,66 @@
+/*
+ * 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.dto.secret;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.gravitino.json.JsonUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/** SerDe tests for {@link SecretBindingDTO}, including scalar-rejection
without plaintext echo. */
+public class TestSecretBindingDTO {
+
+ private static final ObjectMapper MAPPER = JsonUtils.objectMapper();
+
+ @Test
+ void testObjectRoundTrip() throws Exception {
+ SecretBindingDTO dto =
+
SecretBindingDTO.builder().withProvider("memory").withPlaintext("s3cr3t").build();
+ String json = MAPPER.writeValueAsString(dto);
+ SecretBindingDTO parsed = MAPPER.readValue(json, SecretBindingDTO.class);
+ Assertions.assertEquals("memory", parsed.getProvider());
+ Assertions.assertEquals("s3cr3t", parsed.getPlaintext());
+ }
+
+ @Test
+ void testScalarBindingDoesNotEchoPlaintext() {
+ String secret = "MY-SECRET-VALUE-123";
+ JsonMappingException ex =
+ Assertions.assertThrows(
+ JsonMappingException.class,
+ () -> MAPPER.readValue("\"" + secret + "\"",
SecretBindingDTO.class));
+ Assertions.assertFalse(ex.getMessage().contains(secret));
+ Assertions.assertTrue(ex.getMessage().contains("JSON objects"));
+ }
+
+ @Test
+ void testArrayPlaintextRejectedWithoutEcho() {
+ String secret = "LIST-SECRET-999";
+ JsonProcessingException ex =
+ Assertions.assertThrows(
+ JsonProcessingException.class,
+ () ->
+ MAPPER.readValue(
+ "{\"provider\":\"memory\",\"plaintext\":[\"" + secret +
"\"]}",
+ SecretBindingDTO.class));
+ Assertions.assertFalse(ex.getMessage().contains(secret));
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
index d0e850b402..5bf613b92d 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
@@ -84,6 +84,7 @@ import org.apache.gravitino.connector.BaseCatalog;
import org.apache.gravitino.connector.CatalogDropAware;
import org.apache.gravitino.connector.CatalogOperations;
import org.apache.gravitino.connector.HasPropertyMetadata;
+import org.apache.gravitino.connector.HiddenPropertyMaskUtils;
import org.apache.gravitino.connector.SupportsSchemas;
import org.apache.gravitino.connector.authorization.BaseAuthorization;
import org.apache.gravitino.connector.capability.Capability;
@@ -711,7 +712,7 @@ public class CatalogManager implements CatalogDispatcher,
Closeable {
// provider is "fileset", still using "hadoop" will lead to catalog
loading issue. So
// after reading the catalog entity, we convert it to the new
fileset catalog entity.
.map(this::convertFilesetCatalogEntity)
- .map(e -> e.toCatalogInfoWithResolvedProps(getResolvedProperties(e)))
+ .map(this::toCatalogInfoForList)
.toArray(Catalog[]::new);
} catch (IOException ioe) {
LOG.error("Failed to list catalogs in metalake {}", metalakeIdent, ioe);
@@ -1940,6 +1941,30 @@ public class CatalogManager implements
CatalogDispatcher, Closeable {
}
}
+ /**
+ * Builds a catalog info for {@link #listCatalogsInfo}. A single catalog
that fails to resolve
+ * (for example after a temporary ClassLoader error) must not fail the
entire metalake listing.
+ */
+ private Catalog toCatalogInfoForList(CatalogEntity entity) {
+ try {
+ return
entity.toCatalogInfoWithResolvedProps(getResolvedProperties(entity));
+ } catch (RuntimeException e) {
+ LOG.warn(
+ "Failed to resolve catalog {} while listing; returning entity
properties with secrets"
+ + " masked",
+ entity.nameIdentifier(),
+ e);
+ Map<String, String> props =
+ entity.getProperties() == null ? new HashMap<>() : new
HashMap<>(entity.getProperties());
+ for (Map.Entry<String, String> entry : props.entrySet()) {
+ if (SecretPropertyUtils.isSecretProperty(entry.getKey(),
entry.getValue())) {
+ entry.setValue(HiddenPropertyMaskUtils.MASKED_VALUE);
+ }
+ }
+ return entity.toCatalogInfoWithResolvedProps(props);
+ }
+ }
+
private Catalog toCatalogInfo(CatalogWrapper wrapper) {
try {
return wrapper.doWithCatalog(
@@ -1972,8 +1997,9 @@ public class CatalogManager implements CatalogDispatcher,
Closeable {
// Load Catalog class instance
BaseCatalog<?> catalog = createCatalogInstance(classLoader,
entity.getProvider());
// Resolve secret URNs to plaintext for connector init only; entity
storage keeps URNs.
+ // toPlaintextOrMasked: a deleted/rotated secret must not block disable,
drop, re-bind or list.
catalog
-
.withCatalogConf(secretManager.toPlaintextProperties(entity.getProperties()))
+
.withCatalogConf(secretManager.toPlaintextOrMasked(entity.getProperties()))
.withCatalogEntity(entity)
.withSecretManager(secretManager);
catalog.initAuthorizationPluginInstance(classLoader, metalakeEntity.id());
diff --git a/core/src/main/java/org/apache/gravitino/connector/BaseCatalog.java
b/core/src/main/java/org/apache/gravitino/connector/BaseCatalog.java
index afa2daaec0..9871e50232 100644
--- a/core/src/main/java/org/apache/gravitino/connector/BaseCatalog.java
+++ b/core/src/main/java/org/apache/gravitino/connector/BaseCatalog.java
@@ -626,9 +626,11 @@ public abstract class BaseCatalog<T extends BaseCatalog>
*/
public Map<String, String> propertiesWithCredentialProviders() {
// Entity storage keeps secret URNs; resolve to plaintext when
SecretManager is available.
+ // toPlaintextOrMasked so a deleted/rotated secret does not block catalog
load (disable, drop,
+ // list, re-bind). Unresolved secrets become the masked placeholder.
Map<String, String> props = Maps.newHashMap(entity().getProperties());
if (secretManager != null) {
- props = Maps.newHashMap(secretManager.toPlaintextProperties(props));
+ props = Maps.newHashMap(secretManager.toPlaintextOrMasked(props));
}
if
(StringUtils.isNotBlank(props.get(CredentialConstants.CREDENTIAL_PROVIDERS))) {
// Explicit credential-providers wins: do not auto-append detected
static providers (e.g.
diff --git a/core/src/main/java/org/apache/gravitino/secret/SecretManager.java
b/core/src/main/java/org/apache/gravitino/secret/SecretManager.java
index aa88fd4cf5..43dd1c0ec3 100644
--- a/core/src/main/java/org/apache/gravitino/secret/SecretManager.java
+++ b/core/src/main/java/org/apache/gravitino/secret/SecretManager.java
@@ -36,6 +36,7 @@ import java.util.Set;
import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Config;
+import org.apache.gravitino.connector.HiddenPropertyMaskUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -515,13 +516,32 @@ public class SecretManager implements Closeable {
* Returns a copy of {@code properties} with secret URN values replaced by
plaintext.
*
* <p>Used so connectors receive plaintext conf while entity storage keeps
URN strings. Non-secret
- * entries are copied unchanged.
+ * entries are copied unchanged. Missing secrets fail with {@link
IllegalArgumentException}.
*
* @param properties entity or request properties (may be null)
* @return a new map with secret URNs replaced by plaintext; empty map when
{@code properties} is
* null
*/
public Map<String, String> toPlaintextProperties(@Nullable Map<String,
String> properties) {
+ return resolvePlaintextProperties(properties, true);
+ }
+
+ /**
+ * Like {@link #toPlaintextProperties(Map)}, but unresolved secrets become
{@link
+ * HiddenPropertyMaskUtils#MASKED_VALUE} instead of throwing.
+ *
+ * <p>Catalog load for disable / drop / re-bind / listing uses this so a
deleted or rotated secret
+ * does not block management of the catalog.
+ *
+ * @param properties entity or request properties (may be null)
+ * @return a new map with secret URNs replaced by plaintext or the masked
placeholder
+ */
+ public Map<String, String> toPlaintextOrMasked(@Nullable Map<String, String>
properties) {
+ return resolvePlaintextProperties(properties, false);
+ }
+
+ private Map<String, String> resolvePlaintextProperties(
+ @Nullable Map<String, String> properties, boolean failOnMissing) {
if (properties == null || properties.isEmpty()) {
return properties == null ? Map.of() : Map.copyOf(properties);
}
@@ -530,7 +550,19 @@ public class SecretManager implements Closeable {
String key = entry.getKey();
String value = entry.getValue();
if (SecretPropertyUtils.isSecretProperty(key, value)) {
- plaintext.put(key, readSecret(SecretUrn.parse(value)));
+ try {
+ plaintext.put(key, readSecret(SecretUrn.parse(value)));
+ } catch (IllegalArgumentException e) {
+ if (failOnMissing) {
+ throw e;
+ }
+ LOG.warn(
+ "Secret for property '{}' could not be resolved during catalog
load; "
+ + "using masked placeholder so management operations can
proceed: {}",
+ key,
+ e.getMessage());
+ plaintext.put(key, HiddenPropertyMaskUtils.MASKED_VALUE);
+ }
} else {
plaintext.put(key, value);
}
@@ -551,7 +583,15 @@ public class SecretManager implements Closeable {
SecretBinding binding = bindings.get(propertyKey);
Preconditions.checkArgument(
binding != null, "No secretBindings entry for property key \"%s\"",
propertyKey);
- secretMaterials.add(new SecretMaterial(urn, binding.plaintext()));
+ String plaintext = binding.plaintext();
+ // Same rule as alter setSecretBinding: never persist the read-path mask
as a real secret.
+ if (HiddenPropertyMaskUtils.isMaskedPlaceholder(plaintext)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Property '%s' cannot be set to the masked placeholder value
'%s'",
+ propertyKey, HiddenPropertyMaskUtils.MASKED_VALUE));
+ }
+ secretMaterials.add(new SecretMaterial(urn, plaintext));
}
return List.copyOf(secretMaterials);
}
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
index 31bdae35c1..e375280325 100644
--- a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
+++ b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
@@ -41,6 +41,7 @@ import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -2502,6 +2503,82 @@ public class TestCatalogManager {
}
}
+ @Test
+ void testCreateRejectsMaskedPlaceholderSecretBinding() throws Exception {
+ try (SecretManager secrets = memorySecretManager();
+ CatalogManager manager =
+ new CatalogManager(config, entityStore, new RandomIdGenerator(),
secrets)) {
+ NameIdentifier ident = NameIdentifier.of("metalake",
"secret_mask_create");
+ IllegalArgumentException ex =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ manager.createCatalog(
+ ident,
+ Catalog.Type.RELATIONAL,
+ provider,
+ "comment",
+ catalogProps(),
+ Map.of(
+ PROPERTY_KEY4,
+ new SecretBinding("memory",
HiddenPropertyMaskUtils.MASKED_VALUE)),
+ Map.of()));
+ Assertions.assertTrue(ex.getMessage().contains("masked placeholder"));
+ Assertions.assertFalse(manager.catalogExists(ident));
+ }
+ }
+
+ @Test
+ void testCatalogManageableWhenSecretMissing() throws Exception {
+ try (SecretManager secrets = memorySecretManager();
+ CatalogManager manager =
+ new CatalogManager(config, entityStore, new RandomIdGenerator(),
secrets)) {
+ NameIdentifier broken = NameIdentifier.of("metalake", "secret_missing");
+ NameIdentifier healthy = NameIdentifier.of("metalake", "secret_healthy");
+ manager.createCatalog(
+ broken,
+ Catalog.Type.RELATIONAL,
+ provider,
+ "comment",
+ catalogProps(),
+ Map.of(PROPERTY_KEY4, new SecretBinding("memory", "gone-soon")),
+ Map.of());
+ manager.createCatalog(healthy, Catalog.Type.RELATIONAL, provider,
"comment", catalogProps());
+
+ String urn =
+ entityStore
+ .get(broken, EntityType.CATALOG, CatalogEntity.class)
+ .getProperties()
+ .get(PROPERTY_KEY4);
+ secrets.deleteSecretsFromProperties(Map.of(PROPERTY_KEY4, urn));
+ manager.getCatalogCache().invalidate(broken);
+
+ Catalog[] listed = manager.listCatalogsInfo(broken.namespace());
+ Assertions.assertTrue(Arrays.stream(listed).anyMatch(c ->
c.name().equals(broken.name())));
+ Assertions.assertTrue(Arrays.stream(listed).anyMatch(c ->
c.name().equals(healthy.name())));
+
+ // Re-bind while still in use (alter requires catalog in use).
+ Assertions.assertDoesNotThrow(
+ () ->
+ manager.alterCatalog(
+ broken,
+ CatalogChange.setSecretBinding(
+ PROPERTY_KEY4, new SecretBinding("memory", "rebound"))));
+ String reboundUrn =
+ entityStore
+ .get(broken, EntityType.CATALOG, CatalogEntity.class)
+ .getProperties()
+ .get(PROPERTY_KEY4);
+ Assertions.assertEquals("rebound",
secrets.readSecret(SecretUrn.parse(reboundUrn)));
+
+ secrets.deleteSecretsFromProperties(Map.of(PROPERTY_KEY4, reboundUrn));
+ manager.getCatalogCache().invalidate(broken);
+ Assertions.assertDoesNotThrow(() -> manager.disableCatalog(broken));
+ Assertions.assertTrue(manager.dropCatalog(broken, true));
+ Assertions.assertTrue(manager.dropCatalog(healthy, true));
+ }
+ }
+
private static Map<String, String> catalogProps() {
return ImmutableMap.of(
"provider",
diff --git
a/core/src/test/java/org/apache/gravitino/secret/TestSecretManager.java
b/core/src/test/java/org/apache/gravitino/secret/TestSecretManager.java
index ccc1f54d0d..10430a3457 100644
--- a/core/src/test/java/org/apache/gravitino/secret/TestSecretManager.java
+++ b/core/src/test/java/org/apache/gravitino/secret/TestSecretManager.java
@@ -71,6 +71,41 @@ public class TestSecretManager {
}
}
+ @Test
+ void testToPlaintextOrMaskedWhenMissing() {
+ try (SecretManager sm = memorySecretManager()) {
+ Map<String, String> props = new HashMap<>();
+ List<SecretUrn> urns = sm.buildSecretBindingUrns("catalog", 42L,
BINDINGS);
+ SecretPropertyUtils.putSecretUrns(props, urns);
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
sm.toPlaintextProperties(props));
+ Map<String, String> masked = sm.toPlaintextOrMasked(props);
+ Assertions.assertEquals(
+ org.apache.gravitino.connector.HiddenPropertyMaskUtils.MASKED_VALUE,
+ masked.get("jdbc-password"));
+ }
+ }
+
+ @Test
+ void testAssembleRejectsMaskedPlaceholderPlaintext() {
+ try (SecretManager sm = memorySecretManager()) {
+ Map<String, String> entityProps = new HashMap<>();
+ Map<String, SecretBinding> bindings =
+ Map.of(
+ "jdbc-password",
+ new SecretBinding(
+ "memory",
org.apache.gravitino.connector.HiddenPropertyMaskUtils.MASKED_VALUE));
+ IllegalArgumentException ex =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ sm.assembleSecretMaterials(
+ Map.of(), entityProps, "catalog", 42L, bindings,
Map.of()));
+ Assertions.assertTrue(ex.getMessage().contains("masked placeholder"));
+ Assertions.assertTrue(ex.getMessage().contains("jdbc-password"));
+ }
+ }
+
@Test
void testRejectReferenceUrns() {
try (SecretManager sm = memorySecretManager()) {
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/mapper/JsonMappingExceptionMapper.java
b/server/src/main/java/org/apache/gravitino/server/web/mapper/JsonMappingExceptionMapper.java
index df972d0ffa..3bdc32f33e 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/mapper/JsonMappingExceptionMapper.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/mapper/JsonMappingExceptionMapper.java
@@ -34,6 +34,8 @@ public class JsonMappingExceptionMapper implements
ExceptionMapper<JsonMappingEx
@Override
public Response toResponse(JsonMappingException e) {
String errorMsg = "Malformed json request";
- return
Utils.illegalArguments(IllegalArgumentException.class.getSimpleName(),
errorMsg, e);
+ // Do not attach the Jackson throwable: its message / stack can quote
request body fragments
+ // (for example a bare-string secretBindings value that is the secret
plaintext).
+ return
Utils.illegalArguments(IllegalArgumentException.class.getSimpleName(),
errorMsg, null);
}
}
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
index 80ee553cd5..c58559c303 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
@@ -816,7 +816,8 @@ public class ExceptionHandlers {
@Override
public Response handle(OperationType op, String secret, String parent,
Exception e) {
String reason = e instanceof ServiceUnavailableException ?
e.getMessage() : getErrorMsg(e);
- String errorMsg = getSecretErrorMsg(parent, reason);
+ // metadataObjectName is passed as {@code secret}; parent is unused for
this handler.
+ String errorMsg = getSecretErrorMsg(secret, reason);
LOG.warn(errorMsg, e);
if (e instanceof IllegalArgumentException) {
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectSecretOperations.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectSecretOperations.java
index c294123a8d..45689d3739 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectSecretOperations.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectSecretOperations.java
@@ -29,7 +29,6 @@ import java.util.Set;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
-import javax.ws.rs.NotSupportedException;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
@@ -105,7 +104,7 @@ public class MetadataObjectSecretOperations {
MetadataObjects.parse(
fullName,
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
if (!supportsSecretOperations(object)) {
- throw new NotSupportedException(
+ throw new IllegalArgumentException(
"Doesn't support secret property operations for metadata
object type");
}
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/mapper/TestJsonMappingExceptionMapper.java
b/server/src/test/java/org/apache/gravitino/server/web/mapper/TestJsonMappingExceptionMapper.java
index 96ca84017b..45b35d03e1 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/mapper/TestJsonMappingExceptionMapper.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/mapper/TestJsonMappingExceptionMapper.java
@@ -34,12 +34,15 @@ public class TestJsonMappingExceptionMapper {
public void testJsonMappingExceptionMapper() {
JsonParser mockParser = Mockito.mock(JsonParser.class);
Response response =
-
jsonMappingExceptionMapper.toResponse(JsonMappingException.from(mockParser,
""));
+ jsonMappingExceptionMapper.toResponse(
+ JsonMappingException.from(mockParser, "from String value
('MY-SECRET-VALUE-123')"));
ErrorResponse errorResponse = ErrorResponse.illegalArguments("Malformed
json request");
ErrorResponse entity = (ErrorResponse) response.getEntity();
Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
response.getStatus());
Assertions.assertEquals(errorResponse.getCode(), entity.getCode());
Assertions.assertEquals(errorResponse.getMessage(), entity.getMessage());
Assertions.assertEquals(errorResponse.getType(), entity.getType());
+ Assertions.assertNull(entity.getStack());
+
Assertions.assertFalse(entity.getMessage().contains("MY-SECRET-VALUE-123"));
}
}
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestExceptionHandlers.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestExceptionHandlers.java
index 44fec2564f..4856395f27 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestExceptionHandlers.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestExceptionHandlers.java
@@ -175,6 +175,20 @@ public class TestExceptionHandlers {
});
}
+ @Test
+ void testUnsupportedSecretObjectTypeReturnsBadRequest() {
+ String reason = "Doesn't support secret property operations for metadata
object type";
+ try (Response response =
+ ExceptionHandlers.handleSecretException(
+ OperationType.GET, "probe_tag", new
IllegalArgumentException(reason))) {
+ Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
response.getStatus());
+ ErrorResponse error = (ErrorResponse) response.getEntity();
+ Assertions.assertEquals(ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
error.getCode());
+ Assertions.assertTrue(error.getMessage().contains("probe_tag"));
+ Assertions.assertTrue(error.getMessage().contains(reason));
+ }
+ }
+
@Test
void testUnmodifiableOperationReturnsConflict() {
UnmodifiableStatisticException exception =