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 f27e0a1855 [#12457] feat(secret): Add getSecrets API for
catalog/schema/fileset (#12458)
f27e0a1855 is described below
commit f27e0a185508aab78739829a2be5018ed8b02bde
Author: MaSai <[email protected]>
AuthorDate: Mon Aug 24 16:27:27 2026 +0800
[#12457] feat(secret): Add getSecrets API for catalog/schema/fileset
(#12458)
### What changes were proposed in this pull request?
Add a dedicated **`getSecrets`** API so callers can retrieve
secret-manager plaintext for catalog/schema/fileset **without** changing
default `load*` omit behavior and **without** folding secrets into
credential vending.
- Core: `SecretPropertyUtils.buildSecrets` +
`SecretPropertyOperationDispatcher`
- REST: `GET .../objects/{catalog|schema|fileset}/{fullName}/secrets`
- OpenAPI + Java/Python clients: `SupportsSecrets#getSecrets()`
- `getSecrets()` returns **every** secret-URN property (including keys
that may also appear in credential vending, e.g. `jdbc-password`)
Example:
```java
Catalog catalog = metalake.loadCatalog(catalogName);
Map<String, String> conf = new HashMap<>(catalog.properties());
conf.putAll(catalog.supportsSecrets().getSecrets());
```
### Scope / follow-ups
- **This PR**: platform API + clients + docs/tests only.
- **Connector adoption** (Spark / Flink / Trino / IRC / Lance / GVFS):
separate PR.
- **Drop/cascade write-through secret cleanup**: separate PR (not
included here).
### Why are the changes needed?
Fix: #12457
Entity secrets persist URNs in properties; default HTTP load omits those
keys. Credential vending covers typed providers, not arbitrary
secretBindings. Callers need a narrow secrets map to merge with default
load properties.
### Does this PR introduce _any_ user-facing change?
- Yes: new `getSecrets()` / `SupportsSecrets` on catalog, schema,
fileset
- Yes: new REST endpoint `GET .../objects/{type}/{fullName}/secrets`
- Default `loadCatalog` / `loadSchema` / `loadFileset` property omit
semantics unchanged
- Credential vending API unchanged
### How was this patch tested?
- `./gradlew :core:test --tests
'org.apache.gravitino.secret.TestSecretPropertyUtils'`
- `./gradlew :server:test --tests
'org.apache.gravitino.server.web.rest.TestMetadataObjectSecretOperations'`
- Compile checks for api/common/core/server/client-java
---------
Co-authored-by: Cursor <[email protected]>
---
.../main/java/org/apache/gravitino/Catalog.java | 10 ++
api/src/main/java/org/apache/gravitino/Schema.java | 10 ++
.../java/org/apache/gravitino/file/Fileset.java | 10 ++
.../apache/gravitino/secret/SupportsSecrets.java | 39 ++++++
.../fileset/integration/test/FilesetSecretsIT.java | 115 +++++++++++++++++
.../apache/gravitino/client/BaseSchemaCatalog.java | 3 +
.../org/apache/gravitino/client/ErrorHandlers.java | 42 +++++++
.../apache/gravitino/client/FilesetCatalog.java | 13 +-
.../apache/gravitino/client/GenericFileset.java | 21 +++-
.../org/apache/gravitino/client/GenericSchema.java | 17 ++-
.../client/MetadataObjectSecretOperations.java | 61 +++++++++
.../apache/gravitino/client/RelationalCatalog.java | 13 +-
.../api/{secret.py => secret/__init__.py} | 30 +----
.../api/{secret.py => secret/secret_types.py} | 0
.../api/{secret.py => secret/supports_secrets.py} | 31 ++---
.../gravitino/client/base_schema_catalog.py | 12 ++
.../gravitino/client/fileset_catalog.py | 9 +-
.../gravitino/client/generic_fileset.py | 17 +++
.../gravitino/client/generic_schema.py | 16 +++
.../client/metadata_object_secret_operations.py | 53 ++++++++
.../gravitino/client/relational_catalog.py | 6 +-
.../responses/secrets_response.py} | 37 +++---
.../handlers/secret_error_handler.py} | 35 +++---
.../gravitino/dto/responses/SecretsResponse.java | 62 +++++++++
.../java/org/apache/gravitino/GravitinoEnv.java | 16 +++
.../secret/SecretPropertyOperationDispatcher.java | 136 ++++++++++++++++++++
.../gravitino/secret/SecretPropertyUtils.java | 58 +++++++++
.../gravitino/secret/TestSecretPropertyUtils.java | 57 +++++++++
docs/apache-hive-catalog.md | 2 +-
docs/aws-glue-catalog.md | 2 +-
docs/jdbc-doris-catalog.md | 2 +-
docs/jdbc-hologres-catalog.md | 2 +-
docs/jdbc-mysql-catalog.md | 2 +-
docs/jdbc-oceanbase-catalog.md | 2 +-
docs/jdbc-postgresql-catalog.md | 2 +-
docs/jdbc-starrocks-catalog.md | 2 +-
docs/lakehouse-iceberg-catalog.md | 2 +-
docs/lakehouse-paimon-catalog.md | 2 +-
docs/open-api/catalogs.yaml | 4 +-
docs/open-api/filesets.yaml | 2 +-
docs/open-api/openapi.yaml | 3 +
docs/open-api/schemas.yaml | 2 +-
docs/open-api/secrets.yaml | 68 ++++++++++
docs/security/credential-vending.md | 2 +-
.../apache/gravitino/server/GravitinoServer.java | 4 +
.../web/filter/GravitinoInterceptionService.java | 4 +-
.../server/web/rest/ExceptionHandlers.java | 33 +++++
.../web/rest/MetadataObjectSecretOperations.java | 116 +++++++++++++++++
.../server/web/rest/TestCatalogOperations.java | 19 +++
.../rest/TestMetadataObjectSecretOperations.java | 140 +++++++++++++++++++++
50 files changed, 1232 insertions(+), 114 deletions(-)
diff --git a/api/src/main/java/org/apache/gravitino/Catalog.java
b/api/src/main/java/org/apache/gravitino/Catalog.java
index 9880f8d8d0..3fb8b81693 100644
--- a/api/src/main/java/org/apache/gravitino/Catalog.java
+++ b/api/src/main/java/org/apache/gravitino/Catalog.java
@@ -30,6 +30,7 @@ import org.apache.gravitino.model.ModelCatalog;
import org.apache.gravitino.policy.SupportsPolicies;
import org.apache.gravitino.rel.TableCatalog;
import org.apache.gravitino.rel.ViewCatalog;
+import org.apache.gravitino.secret.SupportsSecrets;
import org.apache.gravitino.tag.SupportsTags;
/**
@@ -286,4 +287,13 @@ public interface Catalog extends Auditable {
default SupportsCredentials supportsCredentials() throws
UnsupportedOperationException {
throw new UnsupportedOperationException("Catalog does not support
credential operations");
}
+
+ /**
+ * @return the {@link SupportsSecrets} if the catalog supports secret
property operations.
+ * @throws UnsupportedOperationException if the catalog does not support
secret property
+ * operations.
+ */
+ default SupportsSecrets supportsSecrets() throws
UnsupportedOperationException {
+ throw new UnsupportedOperationException("Catalog does not support secret
property operations");
+ }
}
diff --git a/api/src/main/java/org/apache/gravitino/Schema.java
b/api/src/main/java/org/apache/gravitino/Schema.java
index c42d760011..19d84c5163 100644
--- a/api/src/main/java/org/apache/gravitino/Schema.java
+++ b/api/src/main/java/org/apache/gravitino/Schema.java
@@ -24,6 +24,7 @@ import javax.annotation.Nullable;
import org.apache.gravitino.annotation.Evolving;
import org.apache.gravitino.authorization.SupportsRoles;
import org.apache.gravitino.policy.SupportsPolicies;
+import org.apache.gravitino.secret.SupportsSecrets;
import org.apache.gravitino.tag.SupportsTags;
/**
@@ -93,4 +94,13 @@ public interface Schema extends Auditable {
default SupportsRoles supportsRoles() {
throw new UnsupportedOperationException("Schema does not support role
operations.");
}
+
+ /**
+ * @return the {@link SupportsSecrets} if the schema supports secret
property operations.
+ * @throws UnsupportedOperationException if the schema does not support
secret property
+ * operations.
+ */
+ default SupportsSecrets supportsSecrets() {
+ throw new UnsupportedOperationException("Schema does not support secret
property operations.");
+ }
}
diff --git a/api/src/main/java/org/apache/gravitino/file/Fileset.java
b/api/src/main/java/org/apache/gravitino/file/Fileset.java
index 7089213984..342161293d 100644
--- a/api/src/main/java/org/apache/gravitino/file/Fileset.java
+++ b/api/src/main/java/org/apache/gravitino/file/Fileset.java
@@ -27,6 +27,7 @@ import org.apache.gravitino.annotation.Evolving;
import org.apache.gravitino.authorization.SupportsRoles;
import org.apache.gravitino.credential.SupportsCredentials;
import org.apache.gravitino.policy.SupportsPolicies;
+import org.apache.gravitino.secret.SupportsSecrets;
import org.apache.gravitino.tag.SupportsTags;
/**
@@ -272,4 +273,13 @@ public interface Fileset extends Auditable {
default SupportsCredentials supportsCredentials() {
throw new UnsupportedOperationException("Fileset does not support
credential operations.");
}
+
+ /**
+ * @return The {@link SupportsSecrets} if the fileset supports secret
property operations.
+ * @throws UnsupportedOperationException If the fileset does not support
secret property
+ * operations.
+ */
+ default SupportsSecrets supportsSecrets() {
+ throw new UnsupportedOperationException("Fileset does not support secret
property operations.");
+ }
}
diff --git a/api/src/main/java/org/apache/gravitino/secret/SupportsSecrets.java
b/api/src/main/java/org/apache/gravitino/secret/SupportsSecrets.java
new file mode 100644
index 0000000000..cf04a38537
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/secret/SupportsSecrets.java
@@ -0,0 +1,39 @@
+/*
+ * 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.Map;
+
+/**
+ * Interface to retrieve secret-manager plaintext properties for a metadata
object.
+ *
+ * <p>Every secret-URN property value is resolved and returned, including keys
that may also be
+ * delivered via {@link org.apache.gravitino.credential.SupportsCredentials}
(for example {@code
+ * jdbc-password}). Normal non-secret properties are not included; combine
with {@code
+ * load*().properties()} on the client.
+ */
+public interface SupportsSecrets {
+
+ /**
+ * Returns secret-manager plaintext properties for this metadata object.
+ *
+ * @return a map of property key to resolved plaintext value; never null,
may be empty
+ */
+ Map<String, String> getSecrets();
+}
diff --git
a/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/integration/test/FilesetSecretsIT.java
b/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/integration/test/FilesetSecretsIT.java
new file mode 100644
index 0000000000..6bc0774c71
--- /dev/null
+++
b/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/integration/test/FilesetSecretsIT.java
@@ -0,0 +1,115 @@
+/*
+ * 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.catalog.fileset.integration.test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.client.GravitinoMetalake;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.integration.test.util.BaseIT;
+import org.apache.gravitino.integration.test.util.GravitinoITUtils;
+import org.apache.gravitino.secret.SecretBinding;
+import org.apache.gravitino.secret.SecretProviderRegistry;
+import org.apache.gravitino.secret.memory.InMemorySecretsProvider;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * IT covering fileset create-time secretBindings with the in-memory secret
provider and getSecrets.
+ */
+public class FilesetSecretsIT extends BaseIT {
+
+ private String metalakeName =
GravitinoITUtils.genRandomName("fileset_secrets_ml");
+ private String catalogName =
GravitinoITUtils.genRandomName("fileset_secrets_cat");
+ private String schemaName =
GravitinoITUtils.genRandomName("fileset_secrets_schema");
+ private GravitinoMetalake metalake;
+ private Catalog catalog;
+ private String baseLocation;
+
+ @BeforeAll
+ @Override
+ public void startIntegrationTest() throws Exception {
+ Map<String, String> configs = Maps.newHashMap();
+ configs.put(SecretProviderRegistry.GRAVITINO_SECRET_PROVIDERS, "memory");
+ configs.put(
+ SecretProviderRegistry.GRAVITINO_SECRET_PROVIDER_PREFIX
+ + "memory."
+ + SecretProviderRegistry.CLASS_NAME,
+ InMemorySecretsProvider.class.getName());
+ registerCustomConfigs(configs);
+ super.startIntegrationTest();
+
+ baseLocation =
+
Files.createTempDirectory("fileset-secrets-it").toAbsolutePath().toUri().toString();
+ client.createMetalake(metalakeName, "comment", Collections.emptyMap());
+ metalake = client.loadMetalake(metalakeName);
+ metalake.createCatalog(
+ catalogName, Catalog.Type.FILESET, "hadoop", "comment",
ImmutableMap.of());
+ catalog = metalake.loadCatalog(catalogName);
+ catalog
+ .asSchemas()
+ .createSchema(schemaName, "comment", ImmutableMap.of("location",
baseLocation));
+ }
+
+ @AfterAll
+ @Override
+ public void stopIntegrationTest() throws IOException, InterruptedException {
+ if (metalake != null) {
+ catalog.asSchemas().dropSchema(schemaName, true);
+ metalake.dropCatalog(catalogName, true);
+ client.dropMetalake(metalakeName, true);
+ }
+ super.stopIntegrationTest();
+ }
+
+ @Test
+ public void testCreateFilesetWithMemorySecretBindingsAndGetSecrets() {
+ NameIdentifier filesetIdent = NameIdentifier.of(schemaName,
"secret_fileset");
+ String location = baseLocation + "/secret_fileset";
+ Fileset fileset =
+ catalog
+ .asFilesetCatalog()
+ .createFileset(
+ filesetIdent,
+ "comment",
+ Fileset.Type.MANAGED,
+ location,
+ ImmutableMap.of("visible-key", "visible-value"),
+ ImmutableMap.of("custom-secret", new SecretBinding("memory",
"mem-plaintext")),
+ ImmutableMap.of());
+
+ Assertions.assertEquals("visible-value",
fileset.properties().get("visible-key"));
+ // Default load properties omit secret-manager values; plaintext comes
from getSecrets().
+ Assertions.assertFalse(
+ fileset.properties().containsKey("custom-secret"),
+ "secret bindings must not appear in default load properties");
+
+ Map<String, String> secrets = fileset.supportsSecrets().getSecrets();
+ Assertions.assertEquals("mem-plaintext", secrets.get("custom-secret"));
+ Assertions.assertFalse(secrets.containsKey("visible-key"));
+ }
+}
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java
index 636090dba0..cba10fa820 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java
@@ -83,6 +83,7 @@ abstract class BaseSchemaCatalog extends CatalogDTO
private final MetadataObjectPolicyOperations objectPolicyOperations;
private final MetadataObjectRoleOperations objectRoleOperations;
protected final MetadataObjectCredentialOperations
objectCredentialOperations;
+ protected final MetadataObjectSecretOperations objectSecretOperations;
private final FunctionCatalogOperations functionOperations;
BaseSchemaCatalog(
@@ -114,6 +115,8 @@ abstract class BaseSchemaCatalog extends CatalogDTO
this.objectCredentialOperations =
new MetadataObjectCredentialOperations(
catalogNamespace.level(0), metadataObject, restClient);
+ this.objectSecretOperations =
+ new MetadataObjectSecretOperations(catalogNamespace.level(0),
metadataObject, restClient);
this.functionOperations =
new FunctionCatalogOperations(restClient, catalogNamespace,
this.name());
}
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
index 79d4ca6903..99547936c8 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/ErrorHandlers.java
@@ -247,6 +247,15 @@ public class ErrorHandlers {
return CredentialErrorHandler.INSTANCE;
}
+ /**
+ * Creates an error handler specific to secret property operations.
+ *
+ * @return A Consumer representing the secret error handler.
+ */
+ public static Consumer<ErrorResponse> secretErrorHandler() {
+ return SecretErrorHandler.INSTANCE;
+ }
+
/**
* Creates an error handler specific to Owner operations.
*
@@ -1036,6 +1045,39 @@ public class ErrorHandlers {
}
}
+ /** Error handler specific to secret property operations. */
+ @SuppressWarnings("FormatStringAnnotation")
+ private static class SecretErrorHandler extends RestErrorHandler {
+
+ private static final SecretErrorHandler INSTANCE = new
SecretErrorHandler();
+
+ @Override
+ public void accept(ErrorResponse errorResponse) {
+ String errorMessage = formatErrorMessage(errorResponse);
+
+ switch (errorResponse.getCode()) {
+ case ErrorConstants.ILLEGAL_ARGUMENTS_CODE:
+ throw new IllegalArgumentException(errorMessage);
+
+ case ErrorConstants.NOT_FOUND_CODE:
+ if
(errorResponse.getType().equals(NoSuchMetalakeException.class.getSimpleName()))
{
+ throw new NoSuchMetalakeException(errorMessage);
+ } else {
+ throw new NotFoundException(errorMessage);
+ }
+
+ case ErrorConstants.NOT_IN_USE_CODE:
+ throw new MetalakeNotInUseException(errorMessage);
+
+ case ErrorConstants.INTERNAL_ERROR_CODE:
+ throw new RuntimeException(errorMessage);
+
+ default:
+ super.accept(errorResponse);
+ }
+ }
+ }
+
/** Error handler specific to Tag operations. */
@SuppressWarnings("FormatStringAnnotation")
private static class TagErrorHandler extends RestErrorHandler {
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/FilesetCatalog.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/FilesetCatalog.java
index 40dc2d7286..6c144548b1 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/FilesetCatalog.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/FilesetCatalog.java
@@ -53,6 +53,7 @@ import org.apache.gravitino.file.FilesetChange;
import org.apache.gravitino.rest.RESTUtils;
import org.apache.gravitino.secret.SecretBinding;
import org.apache.gravitino.secret.SecretReference;
+import org.apache.gravitino.secret.SupportsSecrets;
/**
* Fileset catalog is a catalog implementation that supports fileset like
metadata operations, for
@@ -60,7 +61,7 @@ import org.apache.gravitino.secret.SecretReference;
* metalake.
*/
class FilesetCatalog extends BaseSchemaCatalog
- implements org.apache.gravitino.file.FilesetCatalog, SupportsCredentials {
+ implements org.apache.gravitino.file.FilesetCatalog, SupportsCredentials,
SupportsSecrets {
FilesetCatalog(
Namespace namespace,
@@ -291,6 +292,16 @@ class FilesetCatalog extends BaseSchemaCatalog
return objectCredentialOperations.getCredentials();
}
+ @Override
+ public SupportsSecrets supportsSecrets() throws
UnsupportedOperationException {
+ return this;
+ }
+
+ @Override
+ public Map<String, String> getSecrets() {
+ return objectSecretOperations.getSecrets();
+ }
+
@VisibleForTesting
static String formatFilesetRequestPath(Namespace ns) {
Namespace schemaNs = Namespace.of(ns.level(0), ns.level(1));
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java
index d51e4fea37..065445ada3 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java
@@ -36,19 +36,26 @@ import
org.apache.gravitino.exceptions.PolicyAlreadyAssociatedException;
import org.apache.gravitino.file.Fileset;
import org.apache.gravitino.policy.Policy;
import org.apache.gravitino.policy.SupportsPolicies;
+import org.apache.gravitino.secret.SupportsSecrets;
import org.apache.gravitino.tag.SupportsTags;
import org.apache.gravitino.tag.Tag;
import org.apache.gravitino.tag.TagValue;
/** Represents a generic fileset. */
class GenericFileset
- implements Fileset, SupportsTags, SupportsRoles, SupportsCredentials,
SupportsPolicies {
+ implements Fileset,
+ SupportsTags,
+ SupportsRoles,
+ SupportsCredentials,
+ SupportsSecrets,
+ SupportsPolicies {
private final FilesetDTO filesetDTO;
private final MetadataObjectTagOperations objectTagOperations;
private final MetadataObjectRoleOperations objectRoleOperations;
private final MetadataObjectCredentialOperations objectCredentialOperations;
+ private final MetadataObjectSecretOperations objectSecretOperations;
private final MetadataObjectPolicyOperations objectPolicyOperations;
GenericFileset(FilesetDTO filesetDTO, RESTClient restClient, Namespace
filesetNs) {
@@ -62,6 +69,8 @@ class GenericFileset
new MetadataObjectRoleOperations(filesetNs.level(0), filesetObject,
restClient);
this.objectCredentialOperations =
new MetadataObjectCredentialOperations(filesetNs.level(0),
filesetObject, restClient);
+ this.objectSecretOperations =
+ new MetadataObjectSecretOperations(filesetNs.level(0), filesetObject,
restClient);
this.objectPolicyOperations =
new MetadataObjectPolicyOperations(filesetNs.level(0), filesetObject,
restClient);
}
@@ -117,6 +126,11 @@ class GenericFileset
return this;
}
+ @Override
+ public SupportsSecrets supportsSecrets() {
+ return this;
+ }
+
@Override
public String[] listTags() {
return objectTagOperations.listTags();
@@ -173,6 +187,11 @@ class GenericFileset
return objectCredentialOperations.getCredentials();
}
+ @Override
+ public Map<String, String> getSecrets() {
+ return objectSecretOperations.getSecrets();
+ }
+
@Override
public boolean equals(Object obj) {
if (this == obj) {
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java
index 09ecbc8916..6c4d437643 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java
@@ -31,18 +31,21 @@ import org.apache.gravitino.exceptions.NoSuchTagException;
import org.apache.gravitino.exceptions.PolicyAlreadyAssociatedException;
import org.apache.gravitino.policy.Policy;
import org.apache.gravitino.policy.SupportsPolicies;
+import org.apache.gravitino.secret.SupportsSecrets;
import org.apache.gravitino.tag.SupportsTags;
import org.apache.gravitino.tag.Tag;
import org.apache.gravitino.tag.TagValue;
/** Represents a generic schema. */
-class GenericSchema implements Schema, SupportsTags, SupportsRoles,
SupportsPolicies {
+class GenericSchema
+ implements Schema, SupportsTags, SupportsRoles, SupportsPolicies,
SupportsSecrets {
private final SchemaDTO schemaDTO;
private final MetadataObjectTagOperations objectTagOperations;
private final MetadataObjectRoleOperations objectRoleOperations;
private final MetadataObjectPolicyOperations objectPolicyOperations;
+ private final MetadataObjectSecretOperations objectSecretOperations;
GenericSchema(SchemaDTO schemaDTO, RESTClient restClient, String metalake,
String catalog) {
this.schemaDTO = schemaDTO;
@@ -53,6 +56,8 @@ class GenericSchema implements Schema, SupportsTags,
SupportsRoles, SupportsPoli
new MetadataObjectRoleOperations(metalake, schemaObject, restClient);
this.objectPolicyOperations =
new MetadataObjectPolicyOperations(metalake, schemaObject, restClient);
+ this.objectSecretOperations =
+ new MetadataObjectSecretOperations(metalake, schemaObject, restClient);
}
@Override
@@ -70,6 +75,16 @@ class GenericSchema implements Schema, SupportsTags,
SupportsRoles, SupportsPoli
return this;
}
+ @Override
+ public SupportsSecrets supportsSecrets() {
+ return this;
+ }
+
+ @Override
+ public Map<String, String> getSecrets() {
+ return objectSecretOperations.getSecrets();
+ }
+
@Override
public String name() {
return schemaDTO.name();
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectSecretOperations.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectSecretOperations.java
new file mode 100644
index 0000000000..a723df3538
--- /dev/null
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectSecretOperations.java
@@ -0,0 +1,61 @@
+/*
+ * 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.client;
+
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Map;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.dto.responses.SecretsResponse;
+import org.apache.gravitino.rest.RESTUtils;
+import org.apache.gravitino.secret.SupportsSecrets;
+
+/**
+ * The implementation of {@link SupportsSecrets}. This interface will be
composited into catalog,
+ * schema, and fileset to provide secret property operations for these
metadata objects.
+ */
+class MetadataObjectSecretOperations implements SupportsSecrets {
+
+ private final RESTClient restClient;
+
+ private final String secretsRequestPath;
+
+ MetadataObjectSecretOperations(
+ String metalakeName, MetadataObject metadataObject, RESTClient
restClient) {
+ this.restClient = restClient;
+ this.secretsRequestPath =
+ String.format(
+ "api/metalakes/%s/objects/%s/%s/secrets",
+ RESTUtils.encodeString(metalakeName),
+ metadataObject.type().name().toLowerCase(Locale.ROOT),
+ RESTUtils.encodeString(metadataObject.fullName()));
+ }
+
+ @Override
+ public Map<String, String> getSecrets() {
+ SecretsResponse resp =
+ restClient.get(
+ secretsRequestPath,
+ SecretsResponse.class,
+ Collections.emptyMap(),
+ ErrorHandlers.secretErrorHandler());
+ resp.validate();
+ return resp.getSecrets();
+ }
+}
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalCatalog.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalCatalog.java
index 1ae0df08b5..22fa794f69 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalCatalog.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalCatalog.java
@@ -71,6 +71,7 @@ import org.apache.gravitino.rel.expressions.sorts.SortOrder;
import org.apache.gravitino.rel.expressions.transforms.Transform;
import org.apache.gravitino.rel.indexes.Index;
import org.apache.gravitino.rest.RESTUtils;
+import org.apache.gravitino.secret.SupportsSecrets;
/**
* Relational catalog is a catalog implementation that supports relational
database like metadata
@@ -78,7 +79,7 @@ import org.apache.gravitino.rest.RESTUtils;
* catalog is under the metalake.
*/
class RelationalCatalog extends BaseSchemaCatalog
- implements TableCatalog, ViewCatalog, SupportsCredentials {
+ implements TableCatalog, ViewCatalog, SupportsCredentials, SupportsSecrets
{
public static final String PRIVILEGES = "privileges";
@@ -313,6 +314,16 @@ class RelationalCatalog extends BaseSchemaCatalog
return objectCredentialOperations.getCredentials();
}
+ @Override
+ public SupportsSecrets supportsSecrets() throws
UnsupportedOperationException {
+ return this;
+ }
+
+ @Override
+ public Map<String, String> getSecrets() {
+ return objectSecretOperations.getSecrets();
+ }
+
/**
* List all the views under the given Schema namespace.
*
diff --git a/clients/client-python/gravitino/api/secret.py
b/clients/client-python/gravitino/api/secret/__init__.py
similarity index 50%
copy from clients/client-python/gravitino/api/secret.py
copy to clients/client-python/gravitino/api/secret/__init__.py
index fb1526afae..8b1c26b337 100644
--- a/clients/client-python/gravitino/api/secret.py
+++ b/clients/client-python/gravitino/api/secret/__init__.py
@@ -15,31 +15,7 @@
# specific language governing permissions and limitations
# under the License.
-from dataclasses import dataclass
-from typing import Dict
+from gravitino.api.secret.secret_types import SecretBinding, SecretReference
+from gravitino.api.secret.supports_secrets import SupportsSecrets
-
-@dataclass
-class SecretBinding:
- """Write-through secret binding: provider instance name plus plaintext."""
-
- provider: str
- plaintext: str
-
- def __repr__(self) -> str:
- return f"SecretBinding(provider={self.provider!r}, plaintext=***)"
-
-
-@dataclass
-class SecretReference:
- """External secret locator: provider instance name plus provider-specific
attributes."""
-
- provider: str
- attributes: Dict[str, str]
-
- def __post_init__(self):
- if self.attributes is None or len(self.attributes) == 0:
- raise ValueError("attributes must not be null or empty")
-
- def __repr__(self) -> str:
- return f"SecretReference(provider={self.provider!r},
attributes={self.attributes!r})"
+__all__ = ["SecretBinding", "SecretReference", "SupportsSecrets"]
diff --git a/clients/client-python/gravitino/api/secret.py
b/clients/client-python/gravitino/api/secret/secret_types.py
similarity index 100%
copy from clients/client-python/gravitino/api/secret.py
copy to clients/client-python/gravitino/api/secret/secret_types.py
diff --git a/clients/client-python/gravitino/api/secret.py
b/clients/client-python/gravitino/api/secret/supports_secrets.py
similarity index 51%
copy from clients/client-python/gravitino/api/secret.py
copy to clients/client-python/gravitino/api/secret/supports_secrets.py
index fb1526afae..92bcc7a155 100644
--- a/clients/client-python/gravitino/api/secret.py
+++ b/clients/client-python/gravitino/api/secret/supports_secrets.py
@@ -15,31 +15,14 @@
# specific language governing permissions and limitations
# under the License.
-from dataclasses import dataclass
+from abc import ABC, abstractmethod
from typing import Dict
-@dataclass
-class SecretBinding:
- """Write-through secret binding: provider instance name plus plaintext."""
+class SupportsSecrets(ABC):
+ """Interface to retrieve secret-manager plaintext properties."""
- provider: str
- plaintext: str
-
- def __repr__(self) -> str:
- return f"SecretBinding(provider={self.provider!r}, plaintext=***)"
-
-
-@dataclass
-class SecretReference:
- """External secret locator: provider instance name plus provider-specific
attributes."""
-
- provider: str
- attributes: Dict[str, str]
-
- def __post_init__(self):
- if self.attributes is None or len(self.attributes) == 0:
- raise ValueError("attributes must not be null or empty")
-
- def __repr__(self) -> str:
- return f"SecretReference(provider={self.provider!r},
attributes={self.attributes!r})"
+ @abstractmethod
+ def get_secrets(self) -> Dict[str, str]:
+ """Return secret-manager plaintext properties for this metadata
object."""
+ pass
diff --git a/clients/client-python/gravitino/client/base_schema_catalog.py
b/clients/client-python/gravitino/client/base_schema_catalog.py
index cda7b933b1..e0fb8c20ce 100644
--- a/clients/client-python/gravitino/client/base_schema_catalog.py
+++ b/clients/client-python/gravitino/client/base_schema_catalog.py
@@ -36,6 +36,9 @@ from gravitino.client.generic_schema import GenericSchema
from gravitino.client.metadata_object_credential_operations import (
MetadataObjectCredentialOperations,
)
+from gravitino.client.metadata_object_secret_operations import (
+ MetadataObjectSecretOperations,
+)
from gravitino.client.metadata_object_tag_operations import
MetadataObjectTagOperations
from gravitino.dto.audit_dto import AuditDTO
from gravitino.dto.catalog_dto import CatalogDTO
@@ -76,6 +79,9 @@ class BaseSchemaCatalog(
# The metadata object credential operations
_object_credential_operations: MetadataObjectCredentialOperations
+ # The metadata object secret property operations
+ _object_secret_operations: MetadataObjectSecretOperations
+
_function_operations: FunctionCatalogOperations
def __init__(
@@ -104,6 +110,9 @@ class BaseSchemaCatalog(
self._object_credential_operations =
MetadataObjectCredentialOperations(
catalog_namespace.level(0), metadata_object, rest_client
)
+ self._object_secret_operations = MetadataObjectSecretOperations(
+ catalog_namespace.level(0), metadata_object, rest_client
+ )
self._function_operations = FunctionCatalogOperations(
rest_client, catalog_namespace, self.name()
)
@@ -113,6 +122,9 @@ class BaseSchemaCatalog(
self.validate()
+ def get_secrets(self) -> Dict[str, str]:
+ return self._object_secret_operations.get_secrets()
+
def as_schemas(self):
return self
diff --git a/clients/client-python/gravitino/client/fileset_catalog.py
b/clients/client-python/gravitino/client/fileset_catalog.py
index 50b458b379..9f4a0edafc 100644
--- a/clients/client-python/gravitino/client/fileset_catalog.py
+++ b/clients/client-python/gravitino/client/fileset_catalog.py
@@ -24,6 +24,7 @@ from gravitino.dto.requests.fileset_create_request import
FilesetCreateRequest
from gravitino.api.catalog import Catalog
from gravitino.api.credential.credential import Credential
from gravitino.api.credential.supports_credentials import SupportsCredentials
+from gravitino.api.secret.supports_secrets import SupportsSecrets
from gravitino.api.file.fileset import Fileset
from gravitino.api.file.fileset_change import FilesetChange
from gravitino.api.secret import SecretBinding, SecretReference
@@ -50,7 +51,7 @@ _EMPTY_SECRET_REFERENCES: Mapping[str, SecretReference] =
MappingProxyType({})
class FilesetCatalog(
- BaseSchemaCatalog, SupportsCredentials
+ BaseSchemaCatalog, SupportsCredentials, SupportsSecrets
): # pylint: disable=too-many-ancestors
"""
Fileset catalog is a catalog implementation that supports fileset like
metadata operations, for
@@ -402,3 +403,9 @@ class FilesetCatalog(
def get_credentials(self) -> List[Credential]:
return self._object_credential_operations.get_credentials()
+
+ def support_secrets(self) -> SupportsSecrets:
+ return self
+
+ def get_secrets(self) -> Dict[str, str]:
+ return self._object_secret_operations.get_secrets()
diff --git a/clients/client-python/gravitino/client/generic_fileset.py
b/clients/client-python/gravitino/client/generic_fileset.py
index f7c90e63c4..9516cea807 100644
--- a/clients/client-python/gravitino/client/generic_fileset.py
+++ b/clients/client-python/gravitino/client/generic_fileset.py
@@ -18,6 +18,7 @@ from typing import Dict, List, Optional
from gravitino.api.credential.credential import Credential
from gravitino.api.credential.supports_credentials import SupportsCredentials
+from gravitino.api.secret.supports_secrets import SupportsSecrets
from gravitino.api.file.fileset import Fileset
from gravitino.api.metadata_object import MetadataObject
from gravitino.api.metadata_objects import MetadataObjects
@@ -26,6 +27,9 @@ from gravitino.api.tag.tag import Tag
from gravitino.client.metadata_object_credential_operations import (
MetadataObjectCredentialOperations,
)
+from gravitino.client.metadata_object_secret_operations import (
+ MetadataObjectSecretOperations,
+)
from gravitino.client.metadata_object_tag_operations import
MetadataObjectTagOperations
from gravitino.dto.audit_dto import AuditDTO
from gravitino.dto.fileset_dto import FilesetDTO
@@ -36,6 +40,7 @@ from gravitino.utils import HTTPClient
class GenericFileset(
Fileset,
SupportsCredentials,
+ SupportsSecrets,
SupportsTags,
):
_fileset: FilesetDTO
@@ -44,6 +49,9 @@ class GenericFileset(
_object_credential_operations: MetadataObjectCredentialOperations
"""The metadata object credential operations"""
+ _object_secret_operations: MetadataObjectSecretOperations
+ """The metadata object secret property operations"""
+
def __init__(
self, fileset: FilesetDTO, rest_client: HTTPClient, full_namespace:
Namespace
):
@@ -55,6 +63,9 @@ class GenericFileset(
self._object_credential_operations =
MetadataObjectCredentialOperations(
full_namespace.level(0), metadata_object, rest_client
)
+ self._object_secret_operations = MetadataObjectSecretOperations(
+ full_namespace.level(0), metadata_object, rest_client
+ )
self._object_tag_operations = MetadataObjectTagOperations(
full_namespace.level(0), metadata_object, rest_client
)
@@ -86,6 +97,12 @@ class GenericFileset(
def get_credentials(self) -> List[Credential]:
return self._object_credential_operations.get_credentials()
+ def support_secrets(self) -> SupportsSecrets:
+ return self
+
+ def get_secrets(self) -> Dict[str, str]:
+ return self._object_secret_operations.get_secrets()
+
def list_tags(self) -> List[str]:
return self._object_tag_operations.list_tags()
diff --git a/clients/client-python/gravitino/client/generic_schema.py
b/clients/client-python/gravitino/client/generic_schema.py
index 09c895dcf0..52ade4c630 100644
--- a/clients/client-python/gravitino/client/generic_schema.py
+++ b/clients/client-python/gravitino/client/generic_schema.py
@@ -23,8 +23,12 @@ from gravitino.api.audit import Audit
from gravitino.api.metadata_object import MetadataObject
from gravitino.api.metadata_objects import MetadataObjects
from gravitino.api.schema import Schema
+from gravitino.api.secret.supports_secrets import SupportsSecrets
from gravitino.api.tag.supports_tags import SupportsTags
from gravitino.api.tag.tag import Tag
+from gravitino.client.metadata_object_secret_operations import (
+ MetadataObjectSecretOperations,
+)
from gravitino.client.metadata_object_tag_operations import
MetadataObjectTagOperations
from gravitino.dto.schema_dto import SchemaDTO
from gravitino.utils.http_client import HTTPClient
@@ -33,6 +37,7 @@ from gravitino.utils.http_client import HTTPClient
class GenericSchema(
Schema,
SupportsTags,
+ SupportsSecrets,
):
def __init__(
self,
@@ -54,6 +59,11 @@ class GenericSchema(
metadata_object,
rest_client,
)
+ self._object_secret_operations = MetadataObjectSecretOperations(
+ metalake,
+ metadata_object,
+ rest_client,
+ )
def __eq__(self, value: object) -> bool:
if not isinstance(value, GenericSchema):
@@ -103,3 +113,9 @@ class GenericSchema(
def supports_tags(self) -> SupportsTags:
return self
+
+ def support_secrets(self) -> SupportsSecrets:
+ return self
+
+ def get_secrets(self) -> Dict[str, str]:
+ return self._object_secret_operations.get_secrets()
diff --git
a/clients/client-python/gravitino/client/metadata_object_secret_operations.py
b/clients/client-python/gravitino/client/metadata_object_secret_operations.py
new file mode 100644
index 0000000000..63996f3a6d
--- /dev/null
+++
b/clients/client-python/gravitino/client/metadata_object_secret_operations.py
@@ -0,0 +1,53 @@
+# 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.
+
+from typing import Dict
+
+from gravitino.api.metadata_object import MetadataObject
+from gravitino.api.secret.supports_secrets import SupportsSecrets
+from gravitino.dto.responses.secrets_response import SecretsResponse
+from gravitino.exceptions.handlers.secret_error_handler import
SECRET_ERROR_HANDLER
+from gravitino.rest.rest_utils import encode_string
+from gravitino.utils import HTTPClient
+
+
+class MetadataObjectSecretOperations(SupportsSecrets):
+ _rest_client: HTTPClient
+ _request_path: str
+
+ def __init__(
+ self,
+ metalake_name: str,
+ metadata_object: MetadataObject,
+ rest_client: HTTPClient,
+ ):
+ self._rest_client = rest_client
+ metadata_object_type = metadata_object.type().value
+ metadata_object_fullname = metadata_object.full_name()
+ self._request_path = (
+
f"api/metalakes/{encode_string(metalake_name)}/objects/{metadata_object_type}/"
+ f"{encode_string(metadata_object_fullname)}/secrets"
+ )
+
+ def get_secrets(self) -> Dict[str, str]:
+ resp = self._rest_client.get(
+ self._request_path,
+ error_handler=SECRET_ERROR_HANDLER,
+ )
+ secret_resp = SecretsResponse.from_json(resp.body, infer_missing=True)
+ secret_resp.validate()
+ return secret_resp.secrets()
diff --git a/clients/client-python/gravitino/client/relational_catalog.py
b/clients/client-python/gravitino/client/relational_catalog.py
index c8cffbe662..c4024e26f3 100644
--- a/clients/client-python/gravitino/client/relational_catalog.py
+++ b/clients/client-python/gravitino/client/relational_catalog.py
@@ -19,6 +19,7 @@ from typing import Final, Optional, overload
from gravitino.api.authorization.privileges import Privilege
from gravitino.api.catalog import Catalog
+from gravitino.api.secret.supports_secrets import SupportsSecrets
from gravitino.api.rel.column import Column
from gravitino.api.rel.expressions.distributions.distribution import
Distribution
from gravitino.api.rel.expressions.sorts.sort_order import SortOrder
@@ -64,7 +65,7 @@ from gravitino.utils import HTTPClient
class RelationalCatalog(
- BaseSchemaCatalog, TableCatalog, ViewCatalog
+ BaseSchemaCatalog, TableCatalog, ViewCatalog, SupportsSecrets
): # pylint: disable=too-many-ancestors
"""Relational catalog is a catalog implementation
@@ -97,6 +98,9 @@ class RelationalCatalog(
rest_client,
)
+ def support_secrets(self) -> SupportsSecrets:
+ return self
+
def as_table_catalog(self) -> TableCatalog:
"""Return this relational catalog as a :class:`TableCatalog`.
diff --git a/clients/client-python/gravitino/api/secret.py
b/clients/client-python/gravitino/dto/responses/secrets_response.py
similarity index 52%
copy from clients/client-python/gravitino/api/secret.py
copy to clients/client-python/gravitino/dto/responses/secrets_response.py
index fb1526afae..ab7654d4e5 100644
--- a/clients/client-python/gravitino/api/secret.py
+++ b/clients/client-python/gravitino/dto/responses/secrets_response.py
@@ -15,31 +15,30 @@
# specific language governing permissions and limitations
# under the License.
-from dataclasses import dataclass
from typing import Dict
+from dataclasses import dataclass, field
+from dataclasses_json import config
+from gravitino.dto.responses.base_response import BaseResponse
+from gravitino.exceptions.base import IllegalArgumentException
-@dataclass
-class SecretBinding:
- """Write-through secret binding: provider instance name plus plaintext."""
-
- provider: str
- plaintext: str
- def __repr__(self) -> str:
- return f"SecretBinding(provider={self.provider!r}, plaintext=***)"
+@dataclass
+class SecretsResponse(BaseResponse):
+ """Response for secret properties."""
+ _secrets: Dict[str, str] = field(metadata=config(field_name="secrets"))
-@dataclass
-class SecretReference:
- """External secret locator: provider instance name plus provider-specific
attributes."""
+ def secrets(self) -> Dict[str, str]:
+ return self._secrets
- provider: str
- attributes: Dict[str, str]
+ def validate(self):
+ """Validates the response data.
- def __post_init__(self):
- if self.attributes is None or len(self.attributes) == 0:
- raise ValueError("attributes must not be null or empty")
+ Raises:
+ IllegalArgumentException if secrets are None.
+ """
+ super().validate()
- def __repr__(self) -> str:
- return f"SecretReference(provider={self.provider!r},
attributes={self.attributes!r})"
+ if self._secrets is None:
+ raise IllegalArgumentException('"secrets" must not be null')
diff --git a/clients/client-python/gravitino/api/secret.py
b/clients/client-python/gravitino/exceptions/handlers/secret_error_handler.py
similarity index 50%
rename from clients/client-python/gravitino/api/secret.py
rename to
clients/client-python/gravitino/exceptions/handlers/secret_error_handler.py
index fb1526afae..4eff8c9544 100644
--- a/clients/client-python/gravitino/api/secret.py
+++
b/clients/client-python/gravitino/exceptions/handlers/secret_error_handler.py
@@ -15,31 +15,24 @@
# specific language governing permissions and limitations
# under the License.
-from dataclasses import dataclass
-from typing import Dict
+from gravitino.constants.error import ErrorConstants
+from gravitino.dto.responses.error_response import ErrorResponse
+from gravitino.exceptions.base import CatalogNotInUseException,
NotFoundException
+from gravitino.exceptions.handlers.rest_error_handler import RestErrorHandler
-@dataclass
-class SecretBinding:
- """Write-through secret binding: provider instance name plus plaintext."""
+class SecretErrorHandler(RestErrorHandler):
+ def handle(self, error_response: ErrorResponse):
+ error_message = error_response.format_error_message()
+ code = error_response.code()
- provider: str
- plaintext: str
+ if code == ErrorConstants.NOT_FOUND_CODE:
+ raise NotFoundException(error_message)
- def __repr__(self) -> str:
- return f"SecretBinding(provider={self.provider!r}, plaintext=***)"
+ if code == ErrorConstants.NOT_IN_USE_CODE:
+ raise CatalogNotInUseException(error_message)
+ super().handle(error_response)
-@dataclass
-class SecretReference:
- """External secret locator: provider instance name plus provider-specific
attributes."""
- provider: str
- attributes: Dict[str, str]
-
- def __post_init__(self):
- if self.attributes is None or len(self.attributes) == 0:
- raise ValueError("attributes must not be null or empty")
-
- def __repr__(self) -> str:
- return f"SecretReference(provider={self.provider!r},
attributes={self.attributes!r})"
+SECRET_ERROR_HANDLER = SecretErrorHandler()
diff --git
a/common/src/main/java/org/apache/gravitino/dto/responses/SecretsResponse.java
b/common/src/main/java/org/apache/gravitino/dto/responses/SecretsResponse.java
new file mode 100644
index 0000000000..ebef221da0
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/dto/responses/SecretsResponse.java
@@ -0,0 +1,62 @@
+/*
+ * 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.responses;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import java.util.Map;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+
+/** Represents a response for secret properties. */
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = true)
+public class SecretsResponse extends BaseResponse {
+
+ @JsonProperty("secrets")
+ private final Map<String, String> secrets;
+
+ /**
+ * Creates a new SecretsResponse.
+ *
+ * @param secrets The secret properties map.
+ */
+ public SecretsResponse(Map<String, String> secrets) {
+ super(0);
+ this.secrets = secrets;
+ }
+
+ /**
+ * This is the constructor that is used by Jackson deserializer to create an
instance of
+ * SecretsResponse.
+ */
+ public SecretsResponse() {
+ super();
+ this.secrets = null;
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ super.validate();
+
+ Preconditions.checkArgument(secrets != null, "\"secrets\" must not be
null");
+ }
+}
diff --git a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
index e0a671b844..6c9e9f52ea 100644
--- a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
+++ b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
@@ -100,6 +100,7 @@ import org.apache.gravitino.metrics.source.JVMMetricsSource;
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.SecretProviderRegistry;
import org.apache.gravitino.stats.StatisticDispatcher;
import org.apache.gravitino.stats.StatisticManager;
@@ -158,6 +159,8 @@ public class GravitinoEnv {
private CredentialOperationDispatcher credentialOperationDispatcher;
+ private SecretPropertyOperationDispatcher secretPropertyOperationDispatcher;
+
private KmsClientRegistry kmsClientRegistry;
private SecretManager secretManager;
@@ -427,6 +430,15 @@ public class GravitinoEnv {
return credentialOperationDispatcher;
}
+ /**
+ * Get the {@link SecretPropertyOperationDispatcher} associated with the
Gravitino environment.
+ *
+ * @return The {@link SecretPropertyOperationDispatcher} instance.
+ */
+ public SecretPropertyOperationDispatcher secretPropertyOperationDispatcher()
{
+ return secretPropertyOperationDispatcher;
+ }
+
/**
* Get the metadata-only KMS client registry associated with the Gravitino
environment.
*
@@ -756,6 +768,10 @@ public class GravitinoEnv {
this.credentialOperationDispatcher =
new CredentialOperationDispatcher(catalogManager, entityStore,
idGenerator, secretManager);
+ this.secretPropertyOperationDispatcher =
+ new SecretPropertyOperationDispatcher(
+ catalogManager, entityStore, idGenerator, secretManager);
+
SchemaOperationDispatcher schemaOperationDispatcher =
new SchemaOperationDispatcher(catalogManager, entityStore,
idGenerator, secretManager);
this.internalSchemaDispatcher = schemaOperationDispatcher;
diff --git
a/core/src/main/java/org/apache/gravitino/secret/SecretPropertyOperationDispatcher.java
b/core/src/main/java/org/apache/gravitino/secret/SecretPropertyOperationDispatcher.java
new file mode 100644
index 0000000000..0c82fa3e8a
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/secret/SecretPropertyOperationDispatcher.java
@@ -0,0 +1,136 @@
+/*
+ * 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.io.IOException;
+import java.util.Map;
+import javax.ws.rs.NotSupportedException;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.catalog.CatalogManager;
+import org.apache.gravitino.catalog.OperationDispatcher;
+import org.apache.gravitino.exceptions.NoSuchCatalogException;
+import org.apache.gravitino.exceptions.NoSuchEntityException;
+import org.apache.gravitino.exceptions.NoSuchFilesetException;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.meta.FilesetEntity;
+import org.apache.gravitino.meta.SchemaEntity;
+import org.apache.gravitino.storage.IdGenerator;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/**
+ * Dispatches secrets requests for catalog, schema, and fileset metadata
objects.
+ *
+ * <p>Loads raw entity properties (including secret URNs) from the entity
store / catalog entity,
+ * then resolves only secret-manager URNs via {@link
SecretPropertyUtils#buildSecrets}.
+ */
+public class SecretPropertyOperationDispatcher extends OperationDispatcher {
+
+ /**
+ * Creates a new SecretPropertyOperationDispatcher.
+ *
+ * @param catalogManager The CatalogManager instance.
+ * @param store The EntityStore instance.
+ * @param idGenerator The IdGenerator instance.
+ * @param secretManager The SecretManager instance.
+ */
+ public SecretPropertyOperationDispatcher(
+ CatalogManager catalogManager,
+ EntityStore store,
+ IdGenerator idGenerator,
+ SecretManager secretManager) {
+ super(catalogManager, store, idGenerator, secretManager);
+ }
+
+ /**
+ * Returns resolved secret-manager plaintext properties for the given
metadata object.
+ *
+ * @param identifier The entity name identifier.
+ * @param entityType The entity type (CATALOG, SCHEMA, or FILESET).
+ * @return secret plaintext properties; never null
+ */
+ public Map<String, String> getSecrets(NameIdentifier identifier,
Entity.EntityType entityType) {
+ Map<String, String> rawProperties = loadRawProperties(identifier,
entityType);
+ return SecretPropertyUtils.buildSecrets(secretManager, rawProperties);
+ }
+
+ private Map<String, String> loadRawProperties(
+ NameIdentifier identifier, Entity.EntityType entityType) {
+ switch (entityType) {
+ case CATALOG:
+ return loadCatalogRawProperties(identifier);
+ case SCHEMA:
+ return loadSchemaRawProperties(identifier);
+ case FILESET:
+ return loadFilesetRawProperties(identifier);
+ default:
+ throw new NotSupportedException(
+ "Doesn't support secret property operations for entity type: " +
entityType);
+ }
+ }
+
+ private Map<String, String> loadCatalogRawProperties(NameIdentifier
identifier) {
+ return doWithCatalog(
+ identifier,
+ wrapper -> {
+ wrapper.catalog().checkMetalakeInUse();
+ return wrapper.catalog().entity().getProperties();
+ },
+ NoSuchCatalogException.class);
+ }
+
+ private Map<String, String> loadSchemaRawProperties(NameIdentifier
identifier) {
+ NameIdentifier catalogIdent =
NameIdentifierUtil.getCatalogIdentifier(identifier);
+ doWithCatalog(
+ catalogIdent,
+ wrapper -> {
+ wrapper.catalog().checkMetalakeInUse();
+ return null;
+ },
+ NoSuchCatalogException.class);
+ try {
+ SchemaEntity entity = store.get(identifier, Entity.EntityType.SCHEMA,
SchemaEntity.class);
+ return entity.properties() == null ? Map.of() : entity.properties();
+ } catch (NoSuchEntityException e) {
+ throw new NoSuchSchemaException(e, "Schema %s does not exist",
identifier);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load schema entity " + identifier,
e);
+ }
+ }
+
+ private Map<String, String> loadFilesetRawProperties(NameIdentifier
identifier) {
+ NameIdentifier catalogIdent =
NameIdentifierUtil.getCatalogIdentifier(identifier);
+ doWithCatalog(
+ catalogIdent,
+ wrapper -> {
+ wrapper.catalog().checkMetalakeInUse();
+ return null;
+ },
+ NoSuchCatalogException.class);
+ try {
+ FilesetEntity entity = store.get(identifier, Entity.EntityType.FILESET,
FilesetEntity.class);
+ return entity.properties() == null ? Map.of() : entity.properties();
+ } catch (NoSuchEntityException e) {
+ throw new NoSuchFilesetException(e, "Fileset %s does not exist",
identifier);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load fileset entity " +
identifier, e);
+ }
+ }
+}
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 b7988cd6dc..f7ae60b570 100644
--- a/core/src/main/java/org/apache/gravitino/secret/SecretPropertyUtils.java
+++ b/core/src/main/java/org/apache/gravitino/secret/SecretPropertyUtils.java
@@ -48,6 +48,64 @@ public final class SecretPropertyUtils {
return key != null && value != null && value.startsWith(URN_PREFIX) &&
value.endsWith(key);
}
+ /**
+ * Builds a map of secret-manager plaintext properties only.
+ *
+ * <p>Starting from raw entity properties:
+ *
+ * <ol>
+ * <li>Include every entry where {@link #isSecretProperty} is true,
including keys that may also
+ * appear in credential vending (for example {@code jdbc-password} or
{@code
+ * s3-secret-access-key}).
+ * <li>Resolve secret URN values to plaintext via {@link
SecretManager#readSecret}.
+ * </ol>
+ *
+ * <p>Normal non-secret properties are not included. Plaintext values that
are not secret URNs are
+ * not included even when the key is sensitive.
+ *
+ * @param secretManager secret manager used to resolve URNs
+ * @param rawProperties raw entity properties (may be null)
+ * @return a new secret plaintext property map; never null
+ */
+ public static Map<String, String> buildSecrets(
+ SecretManager secretManager, @Nullable Map<String, String>
rawProperties) {
+ Preconditions.checkArgument(secretManager != null, "secretManager must not
be null");
+ if (rawProperties == null || rawProperties.isEmpty()) {
+ return Map.of();
+ }
+ Map<String, String> secrets = new HashMap<>();
+ for (Map.Entry<String, String> entry : rawProperties.entrySet()) {
+ String key = entry.getKey();
+ String value = entry.getValue();
+ if (key == null || value == null) {
+ continue;
+ }
+ if (isSecretProperty(key, value)) {
+ secrets.put(key, secretManager.readSecret(SecretUrn.parse(value)));
+ }
+ }
+ return secrets;
+ }
+
+ /**
+ * Merges base properties with secret plaintext properties.
+ *
+ * <p>Returns a new mutable map containing all entries from {@code base},
then overlays {@code
+ * secrets}. Null maps are treated as empty.
+ *
+ * @param base non-secret / default-load properties (may be null)
+ * @param secrets secret plaintext properties from {@link #buildSecrets}
(may be null)
+ * @return a new mutable merged property map; never null
+ */
+ public static Map<String, String> mergeProperties(
+ @Nullable Map<String, String> base, @Nullable Map<String, String>
secrets) {
+ Map<String, String> merged = copyEntityProperties(base);
+ if (secrets != null && !secrets.isEmpty()) {
+ merged.putAll(secrets);
+ }
+ return merged;
+ }
+
/**
* Returns a mutable copy of a property map for create-time assembly.
*
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 89fd5d065b..d51e0a8859 100644
---
a/core/src/test/java/org/apache/gravitino/secret/TestSecretPropertyUtils.java
+++
b/core/src/test/java/org/apache/gravitino/secret/TestSecretPropertyUtils.java
@@ -70,6 +70,63 @@ public class TestSecretPropertyUtils {
}
}
+ @Test
+ void testBuildSecrets() {
+ try (SecretManager sm = memorySecretManager()) {
+ Map<String, String> entityProps = new HashMap<>();
+ entityProps.put("jdbc-url", "jdbc:mysql://localhost/db");
+ entityProps.put("jdbc-user", "root");
+ Map<String, SecretBinding> bindings =
+ Map.of(
+ "jdbc-password",
+ new SecretBinding("memory", "s3cr3t"),
+ "custom-secret",
+ new SecretBinding("memory", "custom-value"),
+ "s3-secret-access-key",
+ new SecretBinding("memory", "s3-secret-value"));
+ List<SecretMaterial> writes =
+ sm.assembleSecretMaterials(
+ Map.of("jdbc-url", "jdbc:mysql://localhost/db", "jdbc-user",
"root"),
+ entityProps,
+ "catalog",
+ 42L,
+ bindings,
+ Map.of());
+ sm.writeSecrets(writes);
+
+ entityProps.put("s3-access-key-id", "AKIA");
+ entityProps.put("visible", "ok");
+
+ Map<String, String> secrets = SecretPropertyUtils.buildSecrets(sm,
entityProps);
+
+ // All secret-URN entries, including keys also used by credential vending
+ Assertions.assertEquals("custom-value", secrets.get("custom-secret"));
+ Assertions.assertEquals("s3cr3t", secrets.get("jdbc-password"));
+ Assertions.assertEquals("s3-secret-value",
secrets.get("s3-secret-access-key"));
+ Assertions.assertFalse(secrets.containsKey("jdbc-user"));
+ Assertions.assertFalse(secrets.containsKey("jdbc-url"));
+ Assertions.assertFalse(secrets.containsKey("visible"));
+ Assertions.assertFalse(secrets.containsKey("s3-access-key-id"));
+ }
+ }
+
+ @Test
+ void testBuildSecretsNullAndEmpty() {
+ try (SecretManager sm = memorySecretManager()) {
+ Assertions.assertTrue(SecretPropertyUtils.buildSecrets(sm,
null).isEmpty());
+ Assertions.assertTrue(SecretPropertyUtils.buildSecrets(sm,
Map.of()).isEmpty());
+ }
+ }
+
+ @Test
+ void testMergeProperties() {
+ Map<String, String> merged =
+ SecretPropertyUtils.mergeProperties(Map.of("a", "1"), Map.of("b", "2",
"a", "override"));
+ Assertions.assertEquals("override", merged.get("a"));
+ Assertions.assertEquals("2", merged.get("b"));
+ Assertions.assertTrue(SecretPropertyUtils.mergeProperties(null,
null).isEmpty());
+ }
+
private static SecretManager memorySecretManager() {
Config config = new Config(false) {};
Properties properties = new Properties();
diff --git a/docs/apache-hive-catalog.md b/docs/apache-hive-catalog.md
index 67f6fcc584..3d6770ecdf 100644
--- a/docs/apache-hive-catalog.md
+++ b/docs/apache-hive-catalog.md
@@ -83,7 +83,7 @@ When using Gravitino authorization for Hive with Apache
Ranger, see the [Authori
Refer to [Manage Catalogs and
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
:::note
-Sensitive catalog properties such as `s3-access-key-id`,
`s3-secret-access-key`, `oss-access-key-id`, and `oss-secret-access-key` are
hidden from the load catalog response. Use the [credential vending
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as credential-vending keys are hidden from
the default load catalog response. Retrieve secret-manager-backed properties
(including keys that overlap with credential vending) via `getSecrets` / `GET
.../objects/{type}/{fullName}/secrets`. The [credential vending
API](security/credential-vending.md) remains available for typed credential
delivery.
:::
## Schema
diff --git a/docs/aws-glue-catalog.md b/docs/aws-glue-catalog.md
index 590f5bae2e..658e733f51 100644
--- a/docs/aws-glue-catalog.md
+++ b/docs/aws-glue-catalog.md
@@ -55,7 +55,7 @@ Besides the [common catalog
properties](./gravitino-server-config.md#catalog-pro
Refer to [Manage Catalogs and
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
:::note
-Sensitive catalog properties such as `aws-access-key-id` and
`aws-secret-access-key` are hidden from the load catalog response. Use the
[credential vending API](security/credential-vending.md) to retrieve them at
runtime.
+Sensitive catalog properties such as credential-vending keys are hidden from
the default load catalog response. Retrieve secret-manager-backed properties
(including keys that overlap with credential vending) via `getSecrets` / `GET
.../objects/{type}/{fullName}/secrets`. The [credential vending
API](security/credential-vending.md) remains available for typed credential
delivery.
:::
## Schema
diff --git a/docs/jdbc-doris-catalog.md b/docs/jdbc-doris-catalog.md
index 9b9647d87e..998d57b2b2 100644
--- a/docs/jdbc-doris-catalog.md
+++ b/docs/jdbc-doris-catalog.md
@@ -84,7 +84,7 @@ Returning null for DATETIME type precision. Driver version:
mysql-connector-java
Refer to [Manage Catalogs and
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
:::note
-Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are
hidden from the load catalog response. Use the [credential vending
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are
hidden from the default load catalog response. Retrieve secret-manager-backed
properties (including `jdbc-password` when stored as a secret URN) via
`getSecrets` / `GET .../objects/{type}/{fullName}/secrets`. The [credential
vending API](security/credential-vending.md) (`getCredentials` /
`JdbcCredential`) remains available for typed credential delivery.
:::
## Schema
diff --git a/docs/jdbc-hologres-catalog.md b/docs/jdbc-hologres-catalog.md
index a4412dbd47..332ce4ccb4 100644
--- a/docs/jdbc-hologres-catalog.md
+++ b/docs/jdbc-hologres-catalog.md
@@ -62,7 +62,7 @@ Hologres uses the PostgreSQL JDBC Driver (version 42.3.2 or
later recommended).
Refer to [Manage Catalogs and
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
:::note
-Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are
hidden from the load catalog response. Use the [credential vending
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are
hidden from the default load catalog response. Retrieve secret-manager-backed
properties (including `jdbc-password` when stored as a secret URN) via
`getSecrets` / `GET .../objects/{type}/{fullName}/secrets`. The [credential
vending API](security/credential-vending.md) (`getCredentials` /
`JdbcCredential`) remains available for typed credential delivery.
:::
## Schema
diff --git a/docs/jdbc-mysql-catalog.md b/docs/jdbc-mysql-catalog.md
index 6e7c4172ce..58429cf994 100644
--- a/docs/jdbc-mysql-catalog.md
+++ b/docs/jdbc-mysql-catalog.md
@@ -87,7 +87,7 @@ Returning null for TIMESTAMP type precision. Driver version:
mysql-connector-jav
Refer to [Manage Catalogs and
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
:::note
-Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are
hidden from the load catalog response. Use the [credential vending
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are
hidden from the default load catalog response. Retrieve secret-manager-backed
properties (including `jdbc-password` when stored as a secret URN) via
`getSecrets` / `GET .../objects/{type}/{fullName}/secrets`. The [credential
vending API](security/credential-vending.md) (`getCredentials` /
`JdbcCredential`) remains available for typed credential delivery.
:::
## Schema
diff --git a/docs/jdbc-oceanbase-catalog.md b/docs/jdbc-oceanbase-catalog.md
index 319a2ad302..815d631937 100644
--- a/docs/jdbc-oceanbase-catalog.md
+++ b/docs/jdbc-oceanbase-catalog.md
@@ -87,7 +87,7 @@ Returning null for TIMESTAMP type precision. Driver version:
mysql-connector-jav
Refer to [Manage Catalogs and
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
:::note
-Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are
hidden from the load catalog response. Use the [credential vending
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are
hidden from the default load catalog response. Retrieve secret-manager-backed
properties (including `jdbc-password` when stored as a secret URN) via
`getSecrets` / `GET .../objects/{type}/{fullName}/secrets`. The [credential
vending API](security/credential-vending.md) (`getCredentials` /
`JdbcCredential`) remains available for typed credential delivery.
:::
## Schema
diff --git a/docs/jdbc-postgresql-catalog.md b/docs/jdbc-postgresql-catalog.md
index 28fa2b2c8b..674cc26c24 100644
--- a/docs/jdbc-postgresql-catalog.md
+++ b/docs/jdbc-postgresql-catalog.md
@@ -63,7 +63,7 @@ In PostgreSQL, the database corresponds to the Gravitino
catalog, and the schema
Refer to [Manage Catalogs and
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
:::note
-Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are
hidden from the load catalog response. Use the [credential vending
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are
hidden from the default load catalog response. Retrieve secret-manager-backed
properties (including `jdbc-password` when stored as a secret URN) via
`getSecrets` / `GET .../objects/{type}/{fullName}/secrets`. The [credential
vending API](security/credential-vending.md) (`getCredentials` /
`JdbcCredential`) remains available for typed credential delivery.
:::
## Schema
diff --git a/docs/jdbc-starrocks-catalog.md b/docs/jdbc-starrocks-catalog.md
index 178f8bf4d1..f16d47988f 100644
--- a/docs/jdbc-starrocks-catalog.md
+++ b/docs/jdbc-starrocks-catalog.md
@@ -83,7 +83,7 @@ Returning null for DATETIME type precision. Driver version:
mysql-connector-java
Refer to [Manage Catalogs and
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
:::note
-Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are
hidden from the load catalog response. Use the [credential vending
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are
hidden from the default load catalog response. Retrieve secret-manager-backed
properties (including `jdbc-password` when stored as a secret URN) via
`getSecrets` / `GET .../objects/{type}/{fullName}/secrets`. The [credential
vending API](security/credential-vending.md) (`getCredentials` /
`JdbcCredential`) remains available for typed credential delivery.
:::
## Schema
diff --git a/docs/lakehouse-iceberg-catalog.md
b/docs/lakehouse-iceberg-catalog.md
index 281c8d86ac..996c0427e9 100644
--- a/docs/lakehouse-iceberg-catalog.md
+++ b/docs/lakehouse-iceberg-catalog.md
@@ -257,7 +257,7 @@ Gravitino provides the build-in
`org.apache.gravitino.iceberg.common.cache.Local
Refer to [Manage Catalogs and
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
:::note
-Sensitive catalog properties such as `s3-access-key-id`,
`s3-secret-access-key`, `oss-access-key-id`, and `oss-secret-access-key` are
hidden from the load catalog response. Use the [credential vending
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as credential-vending keys are hidden from
the default load catalog response. Retrieve secret-manager-backed properties
(including keys that overlap with credential vending) via `getSecrets` / `GET
.../objects/{type}/{fullName}/secrets`. The [credential vending
API](security/credential-vending.md) remains available for typed credential
delivery.
:::
## Schema
diff --git a/docs/lakehouse-paimon-catalog.md b/docs/lakehouse-paimon-catalog.md
index e398c5e5b4..0ee3cc9e31 100644
--- a/docs/lakehouse-paimon-catalog.md
+++ b/docs/lakehouse-paimon-catalog.md
@@ -99,7 +99,7 @@ Download the corresponding JDBC driver and place it to the
`catalogs/lakehouse-p
Refer to [Manage Catalogs and
Schemas](./manage-catalogs-and-schemas.md#catalog-operations) for more details.
:::note
-Sensitive catalog properties such as `s3-access-key-id`,
`s3-secret-access-key`, `jdbc-user`, and `jdbc-password` are hidden from the
load catalog response. Use the [credential vending
API](security/credential-vending.md) to retrieve them at runtime.
+Sensitive catalog properties such as `jdbc-user` and `jdbc-password` are
hidden from the default load catalog response. Retrieve secret-manager-backed
properties (including `jdbc-password` when stored as a secret URN) via
`getSecrets` / `GET .../objects/{type}/{fullName}/secrets`. The [credential
vending API](security/credential-vending.md) (`getCredentials` /
`JdbcCredential`) remains available for typed credential delivery.
:::
## Schema
diff --git a/docs/open-api/catalogs.yaml b/docs/open-api/catalogs.yaml
index f568b04c52..00be079033 100644
--- a/docs/open-api/catalogs.yaml
+++ b/docs/open-api/catalogs.yaml
@@ -154,7 +154,7 @@ paths:
- catalog
summary: Get catalog
operationId: loadCatalog
- description: Returns the specified catalog information in the specified
metalake
+ description: Returns the specified catalog information in the specified
metalake.
responses:
"200":
$ref: "#/components/responses/CatalogResponse"
@@ -258,8 +258,6 @@ paths:
"5xx":
$ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
-
-
components:
parameters:
details:
diff --git a/docs/open-api/filesets.yaml b/docs/open-api/filesets.yaml
index 6ef16f304a..6ed8fe32bc 100644
--- a/docs/open-api/filesets.yaml
+++ b/docs/open-api/filesets.yaml
@@ -79,7 +79,7 @@ paths:
- fileset
summary: Get fileset
operationId: loadFileset
- description: Returns the specified fileset object
+ description: Returns the specified fileset object.
responses:
"200":
$ref: "#/components/responses/FilesetResponse"
diff --git a/docs/open-api/openapi.yaml b/docs/open-api/openapi.yaml
index 76d32c69fb..c8007aa52e 100644
--- a/docs/open-api/openapi.yaml
+++ b/docs/open-api/openapi.yaml
@@ -99,6 +99,9 @@ paths:
/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectFullName}/credentials:
$ref:
"./credentials.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1objects~1%7BmetadataObjectType%7D~1%7BmetadataObjectFullName%7D~1credentials"
+
/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectFullName}/secrets:
+ $ref:
"./secrets.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1objects~1%7BmetadataObjectType%7D~1%7BmetadataObjectFullName%7D~1secrets"
+
/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectFullName}/tags/{tag}:
$ref:
"./tags.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1objects~1%7BmetadataObjectType%7D~1%7BmetadataObjectFullName%7D~1tags~1%7Btag%7D"
diff --git a/docs/open-api/schemas.yaml b/docs/open-api/schemas.yaml
index 06f90f5056..634519cbbb 100644
--- a/docs/open-api/schemas.yaml
+++ b/docs/open-api/schemas.yaml
@@ -79,7 +79,7 @@ paths:
- schema
summary: Get schema
operationId: loadSchema
- description: Returns the specified schema in the specified catalog and
metalake
+ description: Returns the specified schema in the specified catalog and
metalake.
responses:
"200":
$ref: "#/components/responses/SchemaResponse"
diff --git a/docs/open-api/secrets.yaml b/docs/open-api/secrets.yaml
index cfcf9db149..dfea6a9041 100644
--- a/docs/open-api/secrets.yaml
+++ b/docs/open-api/secrets.yaml
@@ -16,6 +16,49 @@
# under the License.
---
+
+paths:
+
+
/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectFullName}/secrets:
+ parameters:
+ - $ref: "./openapi.yaml#/components/parameters/metalake"
+ - $ref: "./openapi.yaml#/components/parameters/metadataObjectType"
+ - $ref: "./openapi.yaml#/components/parameters/metadataObjectFullName"
+ get:
+ tags:
+ - secrets
+ summary: Get secrets
+ operationId: getSecrets
+ description: |
+ Returns secret-manager plaintext properties for the metadata object.
+ Every secret-URN value is resolved and returned, including keys that
may
+ also appear in the credentials API (for example jdbc-password).
+ Supported types: catalog, schema, fileset.
+ responses:
+ "200":
+ description: Returns the secrets map for the specified metadata
object.
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ $ref: "#/components/responses/SecretsResponse"
+ examples:
+ SecretsResponse:
+ $ref: "#/components/examples/SecretsResponse"
+ "400":
+ $ref: "./openapi.yaml#/components/responses/BadRequestErrorResponse"
+ "404":
+ description: Not Found - The specified metalake or object does not
exist
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+ examples:
+ NoSuchMetalakeException:
+ $ref:
"./metalakes.yaml#/components/examples/NoSuchMetalakeException"
+ "5xx":
+ $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+
+
components:
schemas:
SecretBinding:
@@ -55,3 +98,28 @@ components:
additionalProperties:
type: string
minProperties: 1
+
+ responses:
+ SecretsResponse:
+ type: object
+ properties:
+ code:
+ type: integer
+ format: int32
+ description: Status code of the response
+ enum:
+ - 0
+ secrets:
+ type: object
+ description: Map of secret property key to plaintext value
+ additionalProperties:
+ type: string
+
+ examples:
+ SecretsResponse:
+ value: {
+ "code": 0,
+ "secrets": {
+ "authentication.kerberos.keytab": "plaintext-keytab-content"
+ }
+ }
diff --git a/docs/security/credential-vending.md
b/docs/security/credential-vending.md
index a8277cfa95..ea8f11306b 100755
--- a/docs/security/credential-vending.md
+++ b/docs/security/credential-vending.md
@@ -399,7 +399,7 @@ Bundle jars on Maven Central:
## Upgrading From a Release Earlier Than 1.3.0
-Sensitive catalog properties such as `s3-access-key-id`,
`s3-secret-access-key`, `jdbc-user`, and `jdbc-password` are excluded from `GET
/api/metalakes/{metalake}/catalogs/{catalog}`. Clients written against earlier
releases that read those properties directly lose access to them.
+Sensitive catalog properties such as `s3-access-key-id`,
`s3-secret-access-key`, `jdbc-user`, and `jdbc-password` are excluded from the
default `GET /api/metalakes/{metalake}/catalogs/{catalog}` response. Retrieve
secret-manager-backed properties (including those keys when stored as secret
URNs) via `getSecrets` / `GET .../objects/{type}/{fullName}/secrets`. The
credentials API (`getCredentials` / `JdbcCredential`) remains available for
typed credential delivery. Clients written against [...]
For a zero-downtime migration, set the following in `gravitino.conf`:
diff --git
a/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
b/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
index a9391470af..b12308c228 100644
--- a/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
+++ b/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
@@ -46,6 +46,7 @@ import org.apache.gravitino.metalake.MetalakeDispatcher;
import org.apache.gravitino.metrics.MetricsSystem;
import org.apache.gravitino.metrics.source.MetricsSource;
import org.apache.gravitino.policy.PolicyDispatcher;
+import org.apache.gravitino.secret.SecretPropertyOperationDispatcher;
import org.apache.gravitino.server.authentication.ServerAuthenticator;
import org.apache.gravitino.server.authorization.GravitinoAuthorizerProvider;
import org.apache.gravitino.server.web.ConfigServlet;
@@ -156,6 +157,9 @@ public class GravitinoServer extends ResourceConfig {
bind(gravitinoEnv.credentialOperationDispatcher())
.to(CredentialOperationDispatcher.class)
.ranked(1);
+ bind(gravitinoEnv.secretPropertyOperationDispatcher())
+ .to(SecretPropertyOperationDispatcher.class)
+ .ranked(1);
bind(gravitinoEnv.modelDispatcher()).to(ModelDispatcher.class).ranked(1);
bind(gravitinoEnv.functionDispatcher()).to(FunctionDispatcher.class).ranked(1);
bind(lineageService).to(LineageDispatcher.class).ranked(1);
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
index ae67980587..11926facc3 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
@@ -62,6 +62,7 @@ import org.apache.gravitino.server.web.rest.GroupOperations;
import org.apache.gravitino.server.web.rest.JobOperations;
import org.apache.gravitino.server.web.rest.MetadataObjectCredentialOperations;
import org.apache.gravitino.server.web.rest.MetadataObjectPolicyOperations;
+import org.apache.gravitino.server.web.rest.MetadataObjectSecretOperations;
import org.apache.gravitino.server.web.rest.MetadataObjectTagOperations;
import org.apache.gravitino.server.web.rest.MetalakeOperations;
import org.apache.gravitino.server.web.rest.ModelOperations;
@@ -115,7 +116,8 @@ public class GravitinoInterceptionService implements
InterceptionService {
PolicyOperations.class.getName(),
MetadataObjectPolicyOperations.class.getName(),
JobOperations.class.getName(),
- MetadataObjectCredentialOperations.class.getName()));
+ MetadataObjectCredentialOperations.class.getName(),
+ MetadataObjectSecretOperations.class.getName()));
}
@Override
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 a3fb6e0a99..2570eac02a 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
@@ -145,6 +145,11 @@ public class ExceptionHandlers {
return CredentialExceptionHandler.INSTANCE.handle(op, metadataObjectName,
"", e);
}
+ public static Response handleSecretException(
+ OperationType op, String metadataObjectName, Exception e) {
+ return SecretExceptionHandler.INSTANCE.handle(op, metadataObjectName, "",
e);
+ }
+
public static Response handleModelException(
OperationType op, String model, String schema, Exception e) {
return ModelExceptionHandler.INSTANCE.handle(op, model, schema, e);
@@ -749,6 +754,34 @@ public class ExceptionHandlers {
}
}
+ private static class SecretExceptionHandler extends BaseExceptionHandler {
+
+ private static final ExceptionHandler INSTANCE = new
SecretExceptionHandler();
+
+ private static String getSecretErrorMsg(String parent, String reason) {
+ return String.format("Failed to get secrets under object [%s], reason
[%s]", parent, reason);
+ }
+
+ @Override
+ public Response handle(OperationType op, String secret, String parent,
Exception e) {
+ String errorMsg = getSecretErrorMsg(parent, getErrorMsg(e));
+ LOG.warn(errorMsg, e);
+
+ if (e instanceof IllegalArgumentException) {
+ return Utils.illegalArguments(errorMsg, e);
+
+ } else if (e instanceof NotFoundException) {
+ return Utils.notFound(errorMsg, e);
+
+ } else if (e instanceof NotInUseException) {
+ return Utils.notInUse(errorMsg, e);
+
+ } else {
+ return super.handle(op, secret, parent, e);
+ }
+ }
+ }
+
private static class TagExceptionHandler extends BaseExceptionHandler {
private static final ExceptionHandler INSTANCE = new TagExceptionHandler();
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
new file mode 100644
index 0000000000..a55a6e6767
--- /dev/null
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectSecretOperations.java
@@ -0,0 +1,116 @@
+/*
+ * 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.server.web.rest;
+
+import com.codahale.metrics.annotation.ResponseMetered;
+import com.codahale.metrics.annotation.Timed;
+import com.google.common.collect.ImmutableSet;
+import java.util.Locale;
+import java.util.Map;
+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;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.dto.responses.SecretsResponse;
+import org.apache.gravitino.metrics.MetricNames;
+import org.apache.gravitino.secret.SecretPropertyOperationDispatcher;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationFullName;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationObjectType;
+import
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
+import org.apache.gravitino.server.web.Utils;
+import org.apache.gravitino.utils.MetadataObjectUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Path("/metalakes/{metalake}/objects/{type}/{fullName}/secrets")
+public class MetadataObjectSecretOperations {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(MetadataObjectSecretOperations.class);
+
+ private static final Set<MetadataObject.Type> supportsSecretMetadataTypes =
+ ImmutableSet.of(
+ MetadataObject.Type.CATALOG, MetadataObject.Type.SCHEMA,
MetadataObject.Type.FILESET);
+
+ private final SecretPropertyOperationDispatcher
secretPropertyOperationDispatcher;
+
+ @SuppressWarnings("unused")
+ @Context
+ private HttpServletRequest httpRequest;
+
+ @Inject
+ public MetadataObjectSecretOperations(SecretPropertyOperationDispatcher
dispatcher) {
+ this.secretPropertyOperationDispatcher = dispatcher;
+ }
+
+ @GET
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "get-secrets." + MetricNames.HTTP_PROCESS_DURATION, absolute =
true)
+ @ResponseMetered(name = "get-secrets", absolute = true)
+ @AuthorizationExpression(expression =
AuthorizationExpressionConstants.CAN_ACCESS_METADATA)
+ public Response getSecrets(
+ @PathParam("metalake") @AuthorizationMetadata(type =
Entity.EntityType.METALAKE)
+ String metalake,
+ @PathParam("type") @AuthorizationObjectType String type,
+ @PathParam("fullName") @AuthorizationFullName String fullName) {
+ LOG.info(
+ "Received get secrets request for object type: {}, full name: {} under
metalake: {}",
+ type,
+ fullName,
+ metalake);
+
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ MetadataObject object =
+ MetadataObjects.parse(
+ fullName,
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
+ if (!supportsSecretOperations(object)) {
+ throw new NotSupportedException(
+ "Doesn't support secret property operations for metadata
object type");
+ }
+
+ NameIdentifier identifier =
MetadataObjectUtil.toEntityIdent(metalake, object);
+ Entity.EntityType entityType =
MetadataObjectUtil.toEntityType(object);
+ Map<String, String> secrets =
+ secretPropertyOperationDispatcher.getSecrets(identifier,
entityType);
+ return Utils.ok(new SecretsResponse(secrets));
+ });
+ } catch (Exception e) {
+ return ExceptionHandlers.handleSecretException(OperationType.GET,
fullName, e);
+ }
+ }
+
+ private static boolean supportsSecretOperations(MetadataObject
metadataObject) {
+ return supportsSecretMetadataTypes.contains(metadataObject.type());
+ }
+}
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
index 4298c5d2c7..36fda659ea 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogOperations.java
@@ -37,6 +37,7 @@ import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.time.Instant;
import java.util.Collections;
+import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Application;
@@ -588,6 +589,24 @@ public class TestCatalogOperations extends
BaseOperationsTest {
Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResponse1.getType());
}
+ private static TestCatalog buildCatalogWithProperties(
+ String metalake, String catalogName, Map<String, String> properties) {
+ CatalogEntity entity =
+ CatalogEntity.builder()
+ .withId(1L)
+ .withName(catalogName)
+ .withComment("comment")
+ .withNamespace(Namespace.of(metalake))
+ .withProperties(properties)
+ .withType(Catalog.Type.RELATIONAL)
+ .withProvider("test")
+ .withAuditInfo(
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+ .build();
+
+ return new
TestCatalog().withCatalogConf(Collections.emptyMap()).withCatalogEntity(entity);
+ }
+
private static TestCatalog buildCatalog(String metalake, String catalogName)
{
CatalogEntity entity =
CatalogEntity.builder()
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectSecretOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectSecretOperations.java
new file mode 100644
index 0000000000..0d2e3703f6
--- /dev/null
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectSecretOperations.java
@@ -0,0 +1,140 @@
+/*
+ * 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.server.web.rest;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.dto.responses.SecretsResponse;
+import org.apache.gravitino.rest.RESTUtils;
+import org.apache.gravitino.secret.SecretPropertyOperationDispatcher;
+import org.glassfish.jersey.internal.inject.AbstractBinder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.TestProperties;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestMetadataObjectSecretOperations extends JerseyTest {
+
+ private static class MockServletRequestFactory extends
ServletRequestFactoryBase {
+
+ @Override
+ public HttpServletRequest get() {
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ when(request.getRemoteUser()).thenReturn(null);
+ return request;
+ }
+ }
+
+ private SecretPropertyOperationDispatcher secretPropertyOperationDispatcher =
+ mock(SecretPropertyOperationDispatcher.class);
+
+ private String metalake = "test_metalake";
+
+ @Override
+ protected Application configure() {
+ try {
+ forceSet(
+ TestProperties.CONTAINER_PORT,
String.valueOf(RESTUtils.findAvailablePort(2000, 3000)));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ ResourceConfig resourceConfig = new ResourceConfig();
+ resourceConfig.register(MetadataObjectSecretOperations.class);
+ resourceConfig.register(
+ new AbstractBinder() {
+ @Override
+ protected void configure() {
+ bind(secretPropertyOperationDispatcher)
+ .to(SecretPropertyOperationDispatcher.class)
+ .ranked(2);
+
bindFactory(MockServletRequestFactory.class).to(HttpServletRequest.class);
+ }
+ });
+
+ return resourceConfig;
+ }
+
+ @Test
+ public void testGetSecretsForCatalog() {
+ testGetSecretsForObject(MetadataObjects.parse("catalog",
MetadataObject.Type.CATALOG));
+ }
+
+ @Test
+ public void testGetSecretsForSchema() {
+ testGetSecretsForObject(MetadataObjects.parse("catalog.schema",
MetadataObject.Type.SCHEMA));
+ }
+
+ @Test
+ public void testGetSecretsForFileset() {
+ testGetSecretsForObject(
+ MetadataObjects.parse("catalog.schema.fileset",
MetadataObject.Type.FILESET));
+ }
+
+ private void testGetSecretsForObject(MetadataObject metadataObject) {
+ when(secretPropertyOperationDispatcher.getSecrets(any(),
any(Entity.EntityType.class)))
+ .thenReturn(Map.of("custom-secret", "plaintext"));
+
+ Response response =
+ target(basePath(metalake))
+ .path(metadataObject.type().toString())
+ .path(metadataObject.fullName())
+ .path("/secrets")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response.getStatus());
+ SecretsResponse secretResponse =
response.readEntity(SecretsResponse.class);
+ Assertions.assertEquals(0, secretResponse.getCode());
+ Assertions.assertEquals("plaintext",
secretResponse.getSecrets().get("custom-secret"));
+
+ when(secretPropertyOperationDispatcher.getSecrets(any(),
any(Entity.EntityType.class)))
+ .thenReturn(Map.of());
+ response =
+ target(basePath(metalake))
+ .path(metadataObject.type().toString())
+ .path(metadataObject.fullName())
+ .path("/secrets")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response.getStatus());
+ secretResponse = response.readEntity(SecretsResponse.class);
+ Assertions.assertEquals(0, secretResponse.getCode());
+ Assertions.assertTrue(secretResponse.getSecrets().isEmpty());
+ }
+
+ private String basePath(String metalake) {
+ return "/metalakes/" + metalake + "/objects";
+ }
+}