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 f0fbf2a541 [#12297] feat(secret): Support
setSecretBinding/setSecretReference on fileset alter (#12646)
f0fbf2a541 is described below
commit f0fbf2a54146d2476262615ddb5dfa93ee46ba54
Author: MaSai <[email protected]>
AuthorDate: Thu Aug 27 17:54:57 2026 +0800
[#12297] feat(secret): Support setSecretBinding/setSecretReference on
fileset alter (#12646)
### What changes were proposed in this pull request?
- Add fileset alter change types `setSecretBinding` /
`setSecretReference` (API, DTOs, OpenAPI, Java/Python clients).
- Wire `FilesetOperationDispatcher` with `prepareFilesetSecretChanges`,
calling `SecretManager` alter helpers and rewriting secret ops to
`setProperty` / `removeProperty`.
### Why are the changes needed?
Create-time `secretBindings` / `secretReferences` for fileset are
already on `main` (#12366). Fileset alter still needs the typed secret
update contract from the entity-secrets design (ยง5.9.4).
Fix: #12297
### Does this PR introduce _any_ user-facing change?
- Yes. Fileset alter APIs / REST update requests gain `setSecretBinding`
and `setSecretReference`.
### How was this patch tested?
- Unit tests: `TestSecretManagerAlter`,
`TestFilesetOperationDispatcher`, `TestRequestJsonSerDe`
- `./gradlew spotlessApply`
- `./gradlew :core:test :common:test -PskipITs` (targeted secret/alter
tests)
---------
Co-authored-by: Cursor <[email protected]>
---
.../org/apache/gravitino/file/FilesetChange.java | 118 ++++++++++++++
.../org/apache/gravitino/client/DTOConverters.java | 13 ++
.../gravitino/api/file/fileset_change.py | 66 ++++++++
.../gravitino/client/fileset_catalog.py | 10 ++
.../dto/requests/fileset_update_request.py | 63 ++++++++
.../dto/requests/FilesetUpdateRequest.java | 82 +++++++++-
.../gravitino/json/TestRequestJsonSerDe.java | 19 +++
.../catalog/FilesetOperationDispatcher.java | 119 +++++++++++++-
.../gravitino/catalog/OperationDispatcher.java | 7 +
.../org/apache/gravitino/secret/SecretManager.java | 134 ++++++++++++++++
.../gravitino/secret/SecretPropertyUtils.java | 57 +++++++
.../catalog/TestFilesetOperationDispatcher.java | 45 ++++++
.../gravitino/secret/TestSecretManagerAlter.java | 172 +++++++++++++++++++++
docs/open-api/filesets.yaml | 67 ++++++++
14 files changed, 964 insertions(+), 8 deletions(-)
diff --git a/api/src/main/java/org/apache/gravitino/file/FilesetChange.java
b/api/src/main/java/org/apache/gravitino/file/FilesetChange.java
index 6b79aed41a..5d5e5aecb5 100644
--- a/api/src/main/java/org/apache/gravitino/file/FilesetChange.java
+++ b/api/src/main/java/org/apache/gravitino/file/FilesetChange.java
@@ -20,6 +20,8 @@ package org.apache.gravitino.file;
import java.util.Objects;
import org.apache.gravitino.annotation.Evolving;
+import org.apache.gravitino.secret.SecretBinding;
+import org.apache.gravitino.secret.SecretReference;
/**
* A fileset change is a change to a fileset. It can be used to rename a
fileset, update the comment
@@ -69,6 +71,28 @@ public interface FilesetChange {
return new RemoveProperty(property);
}
+ /**
+ * Creates a new fileset change to bind a write-through secret for a
property.
+ *
+ * @param property The property name to bind.
+ * @param binding The write-through binding ({@code provider} + {@code
plaintext}).
+ * @return The fileset change.
+ */
+ static FilesetChange setSecretBinding(String property, SecretBinding
binding) {
+ return new SetSecretBinding(property, binding);
+ }
+
+ /**
+ * Creates a new fileset change to bind an external secret reference for a
property.
+ *
+ * @param property The property name to bind.
+ * @param reference The external secret locator ({@code provider} + {@code
attributes}).
+ * @return The fileset change.
+ */
+ static FilesetChange setSecretReference(String property, SecretReference
reference) {
+ return new SetSecretReference(property, reference);
+ }
+
/**
* Creates a new fileset change to remove comment from the fileset.
*
@@ -312,6 +336,100 @@ public interface FilesetChange {
}
}
+ /** A fileset change to bind a write-through secret for a property. */
+ final class SetSecretBinding implements FilesetChange {
+ private final String property;
+ private final SecretBinding binding;
+
+ private SetSecretBinding(String property, SecretBinding binding) {
+ this.property = property;
+ this.binding = binding;
+ }
+
+ /**
+ * Retrieves the property name being bound.
+ *
+ * @return The property name.
+ */
+ public String getProperty() {
+ return property;
+ }
+
+ /**
+ * Retrieves the write-through binding.
+ *
+ * @return The secret binding.
+ */
+ public SecretBinding getBinding() {
+ return binding;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SetSecretBinding that = (SetSecretBinding) o;
+ return Objects.equals(property, that.property) &&
Objects.equals(binding, that.binding);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(property, binding);
+ }
+
+ @Override
+ public String toString() {
+ return "SETSECRETBINDING " + property + " " + binding;
+ }
+ }
+
+ /** A fileset change to bind an external secret reference for a property. */
+ final class SetSecretReference implements FilesetChange {
+ private final String property;
+ private final SecretReference reference;
+
+ private SetSecretReference(String property, SecretReference reference) {
+ this.property = property;
+ this.reference = reference;
+ }
+
+ /**
+ * Retrieves the property name being bound.
+ *
+ * @return The property name.
+ */
+ public String getProperty() {
+ return property;
+ }
+
+ /**
+ * Retrieves the external secret reference.
+ *
+ * @return The secret reference.
+ */
+ public SecretReference getReference() {
+ return reference;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SetSecretReference that = (SetSecretReference) o;
+ return Objects.equals(property, that.property) &&
Objects.equals(reference, that.reference);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(property, reference);
+ }
+
+ @Override
+ public String toString() {
+ return "SETSECRETREFERENCE " + property + " " + reference;
+ }
+ }
+
/**
* A fileset change to remove comment from the fileset. Use {@link
UpdateFilesetComment} with null
* value as the argument instead.
diff --git
a/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
b/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
index b681bb6dea..4c5408dfe7 100644
---
a/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
+++
b/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
@@ -263,6 +263,19 @@ class DTOConverters {
} else if (change instanceof FilesetChange.RemoveProperty) {
return new FilesetUpdateRequest.RemoveFilesetPropertiesRequest(
((FilesetChange.RemoveProperty) change).getProperty());
+ } else if (change instanceof FilesetChange.SetSecretBinding) {
+ FilesetChange.SetSecretBinding setSecretBinding =
(FilesetChange.SetSecretBinding) change;
+ return new FilesetUpdateRequest.SetFilesetSecretBindingRequest(
+ setSecretBinding.getProperty(),
+ setSecretBinding.getBinding().provider(),
+ setSecretBinding.getBinding().plaintext());
+ } else if (change instanceof FilesetChange.SetSecretReference) {
+ FilesetChange.SetSecretReference setSecretReference =
+ (FilesetChange.SetSecretReference) change;
+ return new FilesetUpdateRequest.SetFilesetSecretReferenceRequest(
+ setSecretReference.getProperty(),
+ setSecretReference.getReference().provider(),
+ setSecretReference.getReference().attributes());
} else {
throw new IllegalArgumentException(
"Unknown change type: " + change.getClass().getSimpleName());
diff --git a/clients/client-python/gravitino/api/file/fileset_change.py
b/clients/client-python/gravitino/api/file/fileset_change.py
index b723a8a29c..8ca6a6e4f1 100644
--- a/clients/client-python/gravitino/api/file/fileset_change.py
+++ b/clients/client-python/gravitino/api/file/fileset_change.py
@@ -20,6 +20,8 @@ from dataclasses import dataclass, field
from dataclasses_json import config
+from gravitino.api.secret import SecretBinding, SecretReference
+
class FilesetChange(ABC):
"""A fileset change is a change to a fileset. It can be used to rename a
fileset, update the comment
@@ -87,6 +89,16 @@ class FilesetChange(ABC):
"""
return FilesetChange.UpdateFilesetComment(None)
+ @staticmethod
+ def set_secret_binding(fileset_property, binding: SecretBinding):
+ """Creates a fileset change to bind a write-through secret for a
property."""
+ return FilesetChange.SetSecretBinding(fileset_property, binding)
+
+ @staticmethod
+ def set_secret_reference(fileset_property, reference: SecretReference):
+ """Creates a fileset change to bind an external secret reference for a
property."""
+ return FilesetChange.SetSecretReference(fileset_property, reference)
+
@dataclass
class RenameFileset:
"""A fileset change to rename the fileset."""
@@ -317,3 +329,57 @@ class FilesetChange(ABC):
A string summary of the comment removal operation.
"""
return "REMOVECOMMENT"
+
+ @dataclass
+ class SetSecretBinding:
+ """A fileset change to bind a write-through secret for a property."""
+
+ _property: str = field(metadata=config(field_name="property"))
+ _binding: SecretBinding = field(metadata=config(field_name="binding"))
+
+ def property(self):
+ return self._property
+
+ def binding(self):
+ return self._binding
+
+ def __eq__(self, other) -> bool:
+ if not isinstance(other, FilesetChange.SetSecretBinding):
+ return False
+ return (
+ self._property == other.property()
+ and self._binding == other.binding()
+ )
+
+ def __hash__(self):
+ return hash((self._property, self._binding))
+
+ def __str__(self):
+ return f"SETSECRETBINDING {self._property} {self._binding}"
+
+ @dataclass
+ class SetSecretReference:
+ """A fileset change to bind an external secret reference for a
property."""
+
+ _property: str = field(metadata=config(field_name="property"))
+ _reference: SecretReference =
field(metadata=config(field_name="reference"))
+
+ def property(self):
+ return self._property
+
+ def reference(self):
+ return self._reference
+
+ def __eq__(self, other) -> bool:
+ if not isinstance(other, FilesetChange.SetSecretReference):
+ return False
+ return (
+ self._property == other.property()
+ and self._reference == other.reference()
+ )
+
+ def __hash__(self):
+ return hash((self._property, self._reference))
+
+ def __str__(self):
+ return f"SETSECRETREFERENCE {self._property} {self._reference}"
diff --git a/clients/client-python/gravitino/client/fileset_catalog.py
b/clients/client-python/gravitino/client/fileset_catalog.py
index 9f4a0edafc..4d8fed43b9 100644
--- a/clients/client-python/gravitino/client/fileset_catalog.py
+++ b/clients/client-python/gravitino/client/fileset_catalog.py
@@ -394,6 +394,16 @@ class FilesetCatalog(
)
if isinstance(change, FilesetChange.RemoveProperty):
return
FilesetUpdateRequest.RemoveFilesetPropertyRequest(change.property())
+ if isinstance(change, FilesetChange.SetSecretBinding):
+ binding = change.binding()
+ return FilesetUpdateRequest.SetFilesetSecretBindingRequest(
+ change.property(), binding.provider, binding.plaintext
+ )
+ if isinstance(change, FilesetChange.SetSecretReference):
+ reference = change.reference()
+ return FilesetUpdateRequest.SetFilesetSecretReferenceRequest(
+ change.property(), reference.provider, reference.attributes
+ )
if isinstance(change, FilesetChange.RemoveComment):
return FilesetUpdateRequest.UpdateFilesetCommentRequest(None)
raise ValueError(f"Unknown change type: {type(change).__name__}")
diff --git
a/clients/client-python/gravitino/dto/requests/fileset_update_request.py
b/clients/client-python/gravitino/dto/requests/fileset_update_request.py
index da7f4d7035..182ab5d46c 100644
--- a/clients/client-python/gravitino/dto/requests/fileset_update_request.py
+++ b/clients/client-python/gravitino/dto/requests/fileset_update_request.py
@@ -17,10 +17,12 @@
from abc import abstractmethod
from dataclasses import dataclass, field
+from typing import Dict, Optional
from dataclasses_json import config
from gravitino.api.file.fileset_change import FilesetChange
+from gravitino.api.secret import SecretBinding, SecretReference
from gravitino.rest.rest_message import RESTRequest
@@ -162,3 +164,64 @@ class FilesetUpdateRequest:
def fileset_change(self):
return FilesetChange.remove_comment()
+
+ @dataclass
+ class SetFilesetSecretBindingRequest(FilesetUpdateRequestBase):
+ """Represents a request to bind a write-through secret for a fileset
property."""
+
+ _property: Optional[str] =
field(metadata=config(field_name="property"))
+ _provider: Optional[str] =
field(metadata=config(field_name="provider"))
+ _plaintext: Optional[str] =
field(metadata=config(field_name="plaintext"))
+
+ def __init__(self, fileset_property: str, provider: str, plaintext:
str):
+ super().__init__("setSecretBinding")
+ self._property = fileset_property
+ self._provider = provider
+ self._plaintext = plaintext
+
+ def validate(self):
+ if not self._property:
+ raise ValueError('"property" field is required and cannot be
empty')
+ if not self._provider:
+ raise ValueError('"provider" field is required and cannot be
empty')
+ if self._plaintext is None:
+ raise ValueError('"plaintext" field is required and cannot be
null')
+
+ def fileset_change(self):
+ return FilesetChange.set_secret_binding(
+ self._property,
+ SecretBinding(self._provider, self._plaintext),
+ )
+
+ @dataclass
+ class SetFilesetSecretReferenceRequest(FilesetUpdateRequestBase):
+ """Represents a request to bind an external secret reference for a
fileset property."""
+
+ _property: Optional[str] =
field(metadata=config(field_name="property"))
+ _provider: Optional[str] =
field(metadata=config(field_name="provider"))
+ _attributes: Optional[Dict[str, str]] = field(
+ metadata=config(field_name="attributes")
+ )
+
+ def __init__(
+ self,
+ fileset_property: str,
+ provider: str,
+ attributes: Optional[Dict[str, str]] = None,
+ ):
+ super().__init__("setSecretReference")
+ self._property = fileset_property
+ self._provider = provider
+ self._attributes = attributes
+
+ def validate(self):
+ if not self._property:
+ raise ValueError('"property" field is required and cannot be
empty')
+ if not self._provider:
+ raise ValueError('"provider" field is required and cannot be
empty')
+
+ def fileset_change(self):
+ return FilesetChange.set_secret_reference(
+ self._property,
+ SecretReference(self._provider, self._attributes or {}),
+ )
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/FilesetUpdateRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/FilesetUpdateRequest.java
index eddd7fff69..e811c5ca5b 100644
---
a/common/src/main/java/org/apache/gravitino/dto/requests/FilesetUpdateRequest.java
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/FilesetUpdateRequest.java
@@ -23,6 +23,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@@ -31,6 +33,8 @@ import lombok.ToString;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.file.FilesetChange;
import org.apache.gravitino.rest.RESTRequest;
+import org.apache.gravitino.secret.SecretBinding;
+import org.apache.gravitino.secret.SecretReference;
/** Request to update a fileset. */
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -48,7 +52,13 @@ import org.apache.gravitino.rest.RESTRequest;
name = "setProperty"),
@JsonSubTypes.Type(
value = FilesetUpdateRequest.RemoveFilesetPropertiesRequest.class,
- name = "removeProperty")
+ name = "removeProperty"),
+ @JsonSubTypes.Type(
+ value = FilesetUpdateRequest.SetFilesetSecretBindingRequest.class,
+ name = "setSecretBinding"),
+ @JsonSubTypes.Type(
+ value = FilesetUpdateRequest.SetFilesetSecretReferenceRequest.class,
+ name = "setSecretReference")
})
public interface FilesetUpdateRequest extends RESTRequest {
@@ -205,4 +215,74 @@ public interface FilesetUpdateRequest extends RESTRequest {
@Override
public void validate() throws IllegalArgumentException {}
}
+
+ /** The fileset update request for binding a write-through secret to a
property. */
+ @EqualsAndHashCode
+ @NoArgsConstructor(force = true)
+ @AllArgsConstructor
+ @ToString(exclude = "plaintext")
+ class SetFilesetSecretBindingRequest implements FilesetUpdateRequest {
+
+ @Getter
+ @JsonProperty("property")
+ private final String property;
+
+ @Getter
+ @JsonProperty("provider")
+ private final String provider;
+
+ @Getter
+ @JsonProperty("plaintext")
+ private final String plaintext;
+
+ @Override
+ public FilesetChange filesetChange() {
+ return FilesetChange.setSecretBinding(property, new
SecretBinding(provider, plaintext));
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(property), "\"property\" field is required
and cannot be empty");
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(provider), "\"provider\" field is required
and cannot be empty");
+ Preconditions.checkArgument(
+ plaintext != null, "\"plaintext\" field is required and cannot be
null");
+ }
+ }
+
+ /** The fileset update request for binding an external secret reference to a
property. */
+ @EqualsAndHashCode
+ @NoArgsConstructor(force = true)
+ @AllArgsConstructor
+ @ToString
+ class SetFilesetSecretReferenceRequest implements FilesetUpdateRequest {
+
+ @Getter
+ @JsonProperty("property")
+ private final String property;
+
+ @Getter
+ @JsonProperty("provider")
+ private final String provider;
+
+ @Getter
+ @JsonProperty("attributes")
+ private final Map<String, String> attributes;
+
+ @Override
+ public FilesetChange filesetChange() {
+ return FilesetChange.setSecretReference(
+ property,
+ new SecretReference(provider, attributes == null ? ImmutableMap.of()
: attributes));
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(property), "\"property\" field is required
and cannot be empty");
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(provider), "\"provider\" field is required
and cannot be empty");
+ }
+ }
}
diff --git
a/common/src/test/java/org/apache/gravitino/json/TestRequestJsonSerDe.java
b/common/src/test/java/org/apache/gravitino/json/TestRequestJsonSerDe.java
index 3dfe19275f..5ccd0a1cf1 100644
--- a/common/src/test/java/org/apache/gravitino/json/TestRequestJsonSerDe.java
+++ b/common/src/test/java/org/apache/gravitino/json/TestRequestJsonSerDe.java
@@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableMap;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.dto.requests.CatalogCreateRequest;
import org.apache.gravitino.dto.requests.CatalogUpdateRequest;
+import org.apache.gravitino.dto.requests.FilesetUpdateRequest;
import org.apache.gravitino.dto.requests.MetalakeCreateRequest;
import org.apache.gravitino.dto.requests.MetalakeUpdateRequest;
import org.apache.gravitino.dto.requests.MetalakeUpdatesRequest;
@@ -156,4 +157,22 @@ public class TestRequestJsonSerDe {
JsonUtils.objectMapper().readValue(serJson3,
CatalogUpdateRequest.class);
Assertions.assertEquals(req3, deserReq3);
}
+
+ @Test
+ public void testFilesetUpdateRequestSerDe() throws JsonProcessingException {
+ FilesetUpdateRequest req =
+ new FilesetUpdateRequest.SetFilesetSecretBindingRequest("password",
"env", "secret");
+ String serJson = JsonUtils.objectMapper().writeValueAsString(req);
+ FilesetUpdateRequest deserReq =
+ JsonUtils.objectMapper().readValue(serJson,
FilesetUpdateRequest.class);
+ Assertions.assertEquals(req, deserReq);
+
+ FilesetUpdateRequest req1 =
+ new FilesetUpdateRequest.SetFilesetSecretReferenceRequest(
+ "password", "vault", ImmutableMap.of("path",
"secret/data/my-password"));
+ String serJson1 = JsonUtils.objectMapper().writeValueAsString(req1);
+ FilesetUpdateRequest deserReq1 =
+ JsonUtils.objectMapper().readValue(serJson1,
FilesetUpdateRequest.class);
+ Assertions.assertEquals(req1, deserReq1);
+ }
}
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/FilesetOperationDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/FilesetOperationDispatcher.java
index 46a1deb644..00f7bbe907 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/FilesetOperationDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/FilesetOperationDispatcher.java
@@ -18,12 +18,17 @@
*/
package org.apache.gravitino.catalog;
+import static org.apache.gravitino.Entity.EntityType.FILESET;
import static
org.apache.gravitino.catalog.PropertiesMetadataHelpers.validatePropertyForCreate;
import static
org.apache.gravitino.utils.NameIdentifierUtil.getCatalogIdentifier;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.tuple.Pair;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
@@ -39,6 +44,7 @@ import org.apache.gravitino.file.Fileset;
import org.apache.gravitino.file.FilesetChange;
import org.apache.gravitino.lock.LockType;
import org.apache.gravitino.lock.TreeLockUtils;
+import org.apache.gravitino.meta.FilesetEntity;
import org.apache.gravitino.secret.SecretBinding;
import org.apache.gravitino.secret.SecretManager;
import org.apache.gravitino.secret.SecretMaterial;
@@ -227,7 +233,6 @@ public class FilesetOperationDispatcher extends
OperationDispatcher implements F
@Override
public Fileset alterFileset(NameIdentifier ident, FilesetChange... changes)
throws NoSuchFilesetException, IllegalArgumentException {
- validateAlterProperties(ident,
HasPropertyMetadata::filesetPropertiesMetadata, changes);
NameIdentifier catalogIdent = getCatalogIdentifier(ident);
boolean containsRenameFileset =
@@ -239,12 +244,7 @@ public class FilesetOperationDispatcher extends
OperationDispatcher implements F
TreeLockUtils.doWithTreeLock(
nameIdentifierForLock,
LockType.WRITE,
- () ->
- doWithCatalog(
- catalogIdent,
- c -> c.doWithFilesetOps(f -> f.alterFileset(ident,
changes)),
- NoSuchFilesetException.class,
- IllegalArgumentException.class));
+ () -> alterFilesetUnderLock(ident, catalogIdent, changes));
return EntityCombinedFileset.of(alteredFileset)
.withHiddenProperties(
@@ -254,6 +254,61 @@ public class FilesetOperationDispatcher extends
OperationDispatcher implements F
alteredFileset.properties()));
}
+ private Fileset alterFilesetUnderLock(
+ NameIdentifier ident, NameIdentifier catalogIdent, FilesetChange...
changes) {
+ Fileset currentFileset =
+ doWithCatalog(
+ catalogIdent,
+ c -> c.doWithFilesetOps(f -> f.loadFileset(ident)),
+ NoSuchFilesetException.class);
+ // Prefer FilesetEntity properties for secret URNs (catalog loadFileset
may omit them).
+ FilesetEntity filesetEntity = getEntity(ident, FILESET,
FilesetEntity.class);
+ Map<String, String> currentProperties;
+ if (filesetEntity != null
+ && filesetEntity.properties() != null
+ && !filesetEntity.properties().isEmpty()) {
+ currentProperties = new HashMap<>(filesetEntity.properties());
+ } else if (currentFileset.properties() != null) {
+ currentProperties = new HashMap<>(currentFileset.properties());
+ } else {
+ currentProperties = new HashMap<>();
+ }
+
+ validateAlterProperties(ident,
HasPropertyMetadata::filesetPropertiesMetadata, changes);
+
+ StringIdentifier currentStringId =
getStringIdFromProperties(currentProperties);
+ long filesetId;
+ if (currentStringId != null) {
+ filesetId = currentStringId.id();
+ } else if (filesetEntity != null) {
+ filesetId = filesetEntity.id();
+ } else {
+ filesetId = 0L;
+ }
+
+ List<SecretMaterial> writtenSecretMaterials = List.of();
+ boolean alterCommitted = false;
+ try {
+ Pair<FilesetChange[], List<SecretMaterial>> secretResult =
+ prepareFilesetSecretChanges(currentProperties, filesetId, changes);
+ writtenSecretMaterials = secretResult.getRight();
+ FilesetChange[] effectiveChanges = secretResult.getLeft();
+
+ Fileset altered =
+ doWithCatalog(
+ catalogIdent,
+ c -> c.doWithFilesetOps(f -> f.alterFileset(ident,
effectiveChanges)),
+ NoSuchFilesetException.class,
+ IllegalArgumentException.class);
+ alterCommitted = true;
+ return altered;
+ } finally {
+ if (!alterCommitted) {
+ secretManager.rollbackSecrets(writtenSecretMaterials);
+ }
+ }
+ }
+
/**
* Drop a fileset from the catalog.
*
@@ -318,4 +373,54 @@ public class FilesetOperationDispatcher extends
OperationDispatcher implements F
c -> c.doWithFilesetOps(f -> f.getFileLocation(ident, subPath,
locationName)),
NonEmptyEntityException.class));
}
+
+ /**
+ * Rewrites fileset changes that involve secrets into plain setProperty /
removeProperty, writing
+ * secrets as needed. Rolls back any written materials if preparation fails.
+ *
+ * @param currentProperties current fileset properties (may be null)
+ * @param entityId fileset entity id
+ * @param changes fileset changes
+ * @return effective changes and written write-through materials
+ */
+ private Pair<FilesetChange[], List<SecretMaterial>>
prepareFilesetSecretChanges(
+ @Nullable Map<String, String> currentProperties, long entityId,
FilesetChange... changes) {
+ Map<String, String> properties =
+ currentProperties == null ? new HashMap<>() : new
HashMap<>(currentProperties);
+ List<FilesetChange> out = new ArrayList<>(changes.length);
+ List<SecretMaterial> written = new ArrayList<>();
+ try {
+ for (FilesetChange change : changes) {
+ if (change instanceof FilesetChange.SetSecretBinding) {
+ FilesetChange.SetSecretBinding c = (FilesetChange.SetSecretBinding)
change;
+ String urn =
+ secretManager.alterSetSecretBinding(
+ properties, "fileset", entityId, c.getProperty(),
c.getBinding(), written);
+ out.add(FilesetChange.setProperty(c.getProperty(), urn));
+ } else if (change instanceof FilesetChange.SetSecretReference) {
+ FilesetChange.SetSecretReference c =
(FilesetChange.SetSecretReference) change;
+ String urn =
+ secretManager.alterSetSecretReference(
+ properties, "fileset", entityId, c.getProperty(),
c.getReference());
+ out.add(FilesetChange.setProperty(c.getProperty(), urn));
+ } else if (change instanceof FilesetChange.SetProperty) {
+ FilesetChange.SetProperty c = (FilesetChange.SetProperty) change;
+ String value =
+ secretManager.alterSetProperty(
+ properties, "fileset", entityId, c.getProperty(),
c.getValue());
+ out.add(FilesetChange.setProperty(c.getProperty(), value));
+ } else if (change instanceof FilesetChange.RemoveProperty) {
+ FilesetChange.RemoveProperty c = (FilesetChange.RemoveProperty)
change;
+ secretManager.alterRemoveProperty(properties, "fileset", entityId,
c.getProperty());
+ out.add(change);
+ } else {
+ out.add(change);
+ }
+ }
+ return Pair.of(out.toArray(new FilesetChange[0]), List.copyOf(written));
+ } catch (RuntimeException e) {
+ secretManager.rollbackSecrets(written);
+ throw e;
+ }
+ }
}
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/OperationDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/OperationDispatcher.java
index 7e86b8f6a3..31fb86b76e 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/OperationDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/OperationDispatcher.java
@@ -292,6 +292,13 @@ public abstract class OperationDispatcher {
} else if (item instanceof FilesetChange.SetProperty) {
FilesetChange.SetProperty setProperty = (FilesetChange.SetProperty)
item;
properties.put(setProperty.getProperty(), setProperty.getValue());
+ } else if (item instanceof FilesetChange.SetSecretBinding) {
+ FilesetChange.SetSecretBinding setSecretBinding =
(FilesetChange.SetSecretBinding) item;
+ properties.put(setSecretBinding.getProperty(),
setSecretBinding.getBinding().plaintext());
+ } else if (item instanceof FilesetChange.SetSecretReference) {
+ FilesetChange.SetSecretReference setSecretReference =
+ (FilesetChange.SetSecretReference) item;
+ properties.put(setSecretReference.getProperty(),
setSecretReference.getProperty());
} else if (item instanceof TopicChange.SetProperty) {
TopicChange.SetProperty setProperty = (TopicChange.SetProperty) item;
properties.put(setProperty.getProperty(), setProperty.getValue());
diff --git a/core/src/main/java/org/apache/gravitino/secret/SecretManager.java
b/core/src/main/java/org/apache/gravitino/secret/SecretManager.java
index 46abd86f4d..ec84c5e2ae 100644
--- a/core/src/main/java/org/apache/gravitino/secret/SecretManager.java
+++ b/core/src/main/java/org/apache/gravitino/secret/SecretManager.java
@@ -351,6 +351,140 @@ public class SecretManager implements Closeable {
deleteSecrets(writeThrough);
}
+ /**
+ * Writes a write-through secret for alter setSecretBinding; updates
properties with the URN.
+ * Appends written materials to {@code written} for caller rollback.
+ *
+ * @param properties mutable properties map updated as changes are applied
+ * @param entityType {@code catalog}, {@code schema}, or {@code fileset}
+ * @param entityId stable numeric entity id
+ * @param property property key
+ * @param binding write-through secret binding
+ * @param written list that receives newly written materials for rollback
+ * @return the URN string stored in properties
+ */
+ public String alterSetSecretBinding(
+ Map<String, String> properties,
+ String entityType,
+ long entityId,
+ String property,
+ SecretBinding binding,
+ List<SecretMaterial> written) {
+ Preconditions.checkArgument(StringUtils.isNotBlank(property), "property
must not be blank");
+ Preconditions.checkArgument(binding != null, "binding must not be null");
+
SecretPropertyUtils.validateAlterSecretBindingPlaintext(binding.plaintext());
+ Map<String, SecretBinding> bindings = ImmutableMap.of(property, binding);
+ List<SecretUrn> urns = buildSecretBindingUrns(entityType, entityId,
bindings);
+ String newUrn = urns.get(0).toString();
+ String current = properties.get(property);
+ if (current != null
+ && !current.equals(newUrn)
+ && SecretPropertyUtils.isWriteThroughForEntity(property, current,
entityType, entityId)) {
+ deleteSecretsFromProperties(Map.of(property, current));
+ }
+ List<SecretMaterial> materials = List.of(new SecretMaterial(urns.get(0),
binding.plaintext()));
+ writeSecrets(materials);
+ written.addAll(materials);
+ SecretPropertyUtils.putSecretUrns(properties, urns);
+ return properties.get(property);
+ }
+
+ /**
+ * Puts external-ref URN into properties; deletes prior write-through if
owned by this entity.
+ *
+ * @param properties mutable properties map updated as changes are applied
+ * @param entityType {@code catalog}, {@code schema}, or {@code fileset}
+ * @param entityId stable numeric entity id
+ * @param property property key
+ * @param reference external secret reference
+ * @return the URN string stored in properties
+ */
+ public String alterSetSecretReference(
+ Map<String, String> properties,
+ String entityType,
+ long entityId,
+ String property,
+ SecretReference reference) {
+ Preconditions.checkArgument(StringUtils.isNotBlank(property), "property
must not be blank");
+ Preconditions.checkArgument(reference != null, "reference must not be
null");
+ String current = properties.get(property);
+ if (SecretPropertyUtils.isWriteThroughForEntity(property, current,
entityType, entityId)) {
+ deleteSecretsFromProperties(Map.of(property, current));
+ }
+ Map<String, SecretReference> refs = ImmutableMap.of(property, reference);
+ List<SecretUrn> urns = buildSecretReferenceUrns(refs);
+ SecretPropertyUtils.putSecretUrns(properties, urns);
+ return properties.get(property);
+ }
+
+ /**
+ * Handles alter setProperty. If current value is a secret URN, rewrite via
provider/writeSecrets.
+ * Otherwise puts plaintext into properties.
+ *
+ * @param properties mutable properties map updated as changes are applied
+ * @param entityType {@code catalog}, {@code schema}, or {@code fileset}
+ * @param entityId stable numeric entity id
+ * @param property property key
+ * @param value new property value (plaintext when rewriting a secret, or
plain value)
+ * @return value to use in SetProperty change (URN or plaintext)
+ */
+ public String alterSetProperty(
+ Map<String, String> properties,
+ String entityType,
+ long entityId,
+ String property,
+ String value) {
+ SecretPropertyUtils.validateAlterSetPropertyValue(property, value);
+ String current = properties.get(property);
+ if (SecretPropertyUtils.isSecretProperty(property, current)) {
+ SecretUrn currentUrn = SecretUrn.parse(current);
+ SecretBinding binding = new SecretBinding(currentUrn.providerName(),
value);
+ if (SecretPropertyUtils.isWriteThroughForEntity(property, current,
entityType, entityId)) {
+ Map<String, SecretBinding> bindings = ImmutableMap.of(property,
binding);
+ List<SecretUrn> urns = buildSecretBindingUrns(entityType, entityId,
bindings);
+ List<SecretMaterial> materials = List.of(new
SecretMaterial(urns.get(0), value));
+ writeSecrets(materials);
+ SecretPropertyUtils.putSecretUrns(properties, urns);
+ return properties.get(property);
+ }
+ List<String> segments = currentUrn.identifierSegments();
+ Preconditions.checkArgument(
+ !segments.isEmpty(), "Secret URN must contain identifier segments:
%s", currentUrn);
+ Map<String, String> attributes = new HashMap<>();
+ if (segments.size() == 3) {
+ attributes.put(ATTR_ENTITY_TYPE, segments.get(0));
+ attributes.put(ATTR_ENTITY_ID, segments.get(1));
+ attributes.put(ATTR_PROPERTY_KEY, segments.get(2));
+ } else {
+ attributes.put(ATTR_PROPERTY_KEY, property);
+ }
+ SecretUrn writtenUrn =
+
getRegistry().getProvider(currentUrn.providerName()).writeSecret(value,
attributes);
+ properties.put(property, writtenUrn.toString());
+ return writtenUrn.toString();
+ }
+ properties.put(property, value);
+ return value;
+ }
+
+ /**
+ * Deletes write-through secret if owned by this entity; removes key from
properties.
+ *
+ * @param properties mutable properties map updated as changes are applied
+ * @param entityType {@code catalog}, {@code schema}, or {@code fileset}
+ * @param entityId stable numeric entity id
+ * @param property property key to remove
+ */
+ public void alterRemoveProperty(
+ Map<String, String> properties, String entityType, long entityId, String
property) {
+ Preconditions.checkArgument(StringUtils.isNotBlank(property), "property
must not be blank");
+ String current = properties.get(property);
+ if (SecretPropertyUtils.isWriteThroughForEntity(property, current,
entityType, entityId)) {
+ deleteSecretsFromProperties(Map.of(property, current));
+ }
+ properties.remove(property);
+ }
+
/**
* Reads plaintext for a secret URN via the provider named in the URN.
*
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 fc51329c76..c3eb4f7ddf 100644
--- a/core/src/main/java/org/apache/gravitino/secret/SecretPropertyUtils.java
+++ b/core/src/main/java/org/apache/gravitino/secret/SecretPropertyUtils.java
@@ -132,6 +132,63 @@ public final class SecretPropertyUtils {
return properties == null ? new HashMap<>() : new HashMap<>(properties);
}
+ /**
+ * Returns whether {@code value} is a write-through secret URN owned by this
entity property.
+ *
+ * <p>Write-through URNs use identifier segments {@code
entityType:entityId:propertyKey}.
+ *
+ * @param propertyKey the property key
+ * @param value the property value
+ * @param entityType {@code catalog}, {@code schema}, or {@code fileset}
+ * @param entityId the entity id
+ * @return true when the value is a write-through URN for this entity and
property
+ */
+ public static boolean isWriteThroughForEntity(
+ @Nullable String propertyKey, @Nullable String value, String entityType,
long entityId) {
+ if (!isSecretProperty(propertyKey, value)) {
+ return false;
+ }
+ try {
+ SecretUrn urn = SecretUrn.parse(value);
+ List<String> segments = urn.identifierSegments();
+ return segments.size() == 3
+ && entityType.equals(segments.get(0))
+ && String.valueOf(entityId).equals(segments.get(1))
+ && propertyKey.equals(segments.get(2));
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Validates alter {@code setProperty} plaintext: rejects blank, masked
placeholder, and raw URN
+ * strings.
+ *
+ * @param property the property key
+ * @param value the plaintext value
+ */
+ public static void validateAlterSetPropertyValue(String property, String
value) {
+ Preconditions.checkArgument(StringUtils.isNotBlank(property), "property
must not be blank");
+ Preconditions.checkArgument(StringUtils.isNotBlank(value), "value must not
be blank");
+ Preconditions.checkArgument(
+ !"******".equals(value), "setProperty value must not be the masked
placeholder ******");
+ Preconditions.checkArgument(
+ !value.startsWith(URN_PREFIX),
+ "setProperty value must not be a secret URN; use setSecretBinding or
setSecretReference");
+ }
+
+ /**
+ * Validates alter {@code setSecretBinding} plaintext.
+ *
+ * @param plaintext the plaintext from the binding
+ */
+ public static void validateAlterSecretBindingPlaintext(String plaintext) {
+ Preconditions.checkArgument(plaintext != null, "plaintext must not be
null");
+ Preconditions.checkArgument(
+ !"******".equals(plaintext),
+ "setSecretBinding plaintext must not be the masked placeholder
******");
+ }
+
/**
* Returns a mutable property map for create-time assembly, or {@code null}
when the caller
* supplied no properties and no secrets.
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestFilesetOperationDispatcher.java
b/core/src/test/java/org/apache/gravitino/catalog/TestFilesetOperationDispatcher.java
index 5cfa713bf2..bee22f6ecc 100644
---
a/core/src/test/java/org/apache/gravitino/catalog/TestFilesetOperationDispatcher.java
+++
b/core/src/test/java/org/apache/gravitino/catalog/TestFilesetOperationDispatcher.java
@@ -401,6 +401,51 @@ public class TestFilesetOperationDispatcher extends
TestOperationDispatcher {
"k3");
}
+ @Test
+ public void testAlterRemovePropertyDeletesWriteThroughSecret() throws
Exception {
+ try (SecretManager secrets = memorySecretManager()) {
+ AtomicLong nextId = new AtomicLong(9100L);
+ IdGenerator ids = nextId::getAndIncrement;
+ FilesetOperationDispatcher filesets =
+ new FilesetOperationDispatcher(catalogManager, entityStore, ids,
secrets);
+ new SchemaOperationDispatcher(catalogManager, entityStore, ids, secrets,
filesets)
+ .createSchema(
+ NameIdentifier.of(metalake, catalog,
"schema_secret_fileset_remove"),
+ "comment",
+ ImmutableMap.of("k1", "v1"));
+
+ NameIdentifier ident =
+ NameIdentifier.of(
+ metalake, catalog, "schema_secret_fileset_remove",
"fileset_secret_remove");
+ Map<String, SecretBinding> bindings = Map.of("k2", new
SecretBinding("memory", "s3cr3t"));
+ Map<String, String> locations = Map.of(Fileset.LOCATION_NAME_UNKNOWN,
"loc");
+ Map<String, String> props = ImmutableMap.of("k1", "v1");
+ long entityId = nextId.get();
+ filesets.createMultipleLocationFileset(
+ ident, "comment", Fileset.Type.MANAGED, locations, props, bindings,
Map.of());
+
+ SecretUrn urn =
+ SecretUrn.buildWriteThrough(
+ "memory",
+ Map.of(
+ SecretConstants.ATTR_ENTITY_TYPE, "fileset",
+ SecretConstants.ATTR_ENTITY_ID, String.valueOf(entityId),
+ SecretConstants.ATTR_PROPERTY_KEY, "k2"));
+ Assertions.assertEquals("s3cr3t", secrets.readSecret(urn));
+
+ filesets.alterFileset(ident, FilesetChange.removeProperty("k2"));
+
+ Fileset stored =
+ catalogManager
+ .loadCatalogAndWrap(NameIdentifier.of(metalake, catalog))
+ .doWithFilesetOps(ops -> ops.loadFileset(ident));
+ Assertions.assertFalse(stored.properties().containsKey("k2"));
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
secrets.readSecret(urn));
+
+ Assertions.assertTrue(filesets.dropFileset(ident));
+ }
+ }
+
private static SecretManager memorySecretManager() {
Config c = new Config(false) {};
Properties p = new Properties();
diff --git
a/core/src/test/java/org/apache/gravitino/secret/TestSecretManagerAlter.java
b/core/src/test/java/org/apache/gravitino/secret/TestSecretManagerAlter.java
new file mode 100644
index 0000000000..bd7ed63508
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/secret/TestSecretManagerAlter.java
@@ -0,0 +1,172 @@
+/*
+ * 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.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.secret.memory.InMemorySecretsProvider;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestSecretManagerAlter {
+
+ @Test
+ void testAlterSetSecretBindingWritesAndReturnsUrn() {
+ try (SecretManager secretManager = memorySecretManager()) {
+ Map<String, String> props = new HashMap<>(Map.of("jdbc-user", "root"));
+ List<SecretMaterial> written = new ArrayList<>();
+ String urn =
+ secretManager.alterSetSecretBinding(
+ props,
+ "catalog",
+ 42L,
+ "jdbc-password",
+ new SecretBinding("memory", "s3cr3t"),
+ written);
+
+ Assertions.assertTrue(
+ SecretPropertyUtils.isWriteThroughForEntity("jdbc-password", urn,
"catalog", 42L));
+ Assertions.assertEquals(urn, props.get("jdbc-password"));
+ Assertions.assertEquals(1, written.size());
+ Assertions.assertEquals(
+ "s3cr3t",
+
secretManager.getRegistry().getProvider("memory").readSecret(written.get(0).urn()));
+ }
+ }
+
+ @Test
+ void testAlterRemovePropertyDeletesWriteThroughSecret() {
+ try (SecretManager secretManager = memorySecretManager()) {
+ Map<String, String> props = new HashMap<>();
+ List<SecretMaterial> written = new ArrayList<>();
+ String urn =
+ secretManager.alterSetSecretBinding(
+ props, "catalog", 7L, "jdbc-password", new
SecretBinding("memory", "old"), written);
+
+ secretManager.alterRemoveProperty(props, "catalog", 7L, "jdbc-password");
+
+ Assertions.assertFalse(props.containsKey("jdbc-password"));
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
secretManager.getRegistry().getProvider("memory").readSecret(SecretUrn.parse(urn)));
+ }
+ }
+
+ @Test
+ void testAlterRemovePropertyKeepsExternalReferenceSecret() {
+ try (SecretManager secretManager = memorySecretManager()) {
+ // External-ref URNs are not entityType:entityId:propertyKey
write-through shapes.
+ String externalUrn =
"urn:gravitino-secret:memory:path:kv/jdbc-password:jdbc-password";
+
Assertions.assertTrue(SecretPropertyUtils.isSecretProperty("jdbc-password",
externalUrn));
+ Assertions.assertFalse(
+ SecretPropertyUtils.isWriteThroughForEntity("jdbc-password",
externalUrn, "catalog", 7L));
+
+ Map<String, String> props = new HashMap<>();
+ props.put("jdbc-password", externalUrn);
+
+ // Seed an unrelated write-through secret that must survive removing the
external-ref key.
+ List<SecretMaterial> written = new ArrayList<>();
+ String ownedUrn =
+ secretManager.alterSetSecretBinding(
+ new HashMap<>(),
+ "catalog",
+ 7L,
+ "other-secret",
+ new SecretBinding("memory", "keep-me"),
+ written);
+
+ secretManager.alterRemoveProperty(props, "catalog", 7L, "jdbc-password");
+
+ Assertions.assertEquals(
+ "keep-me",
+
secretManager.getRegistry().getProvider("memory").readSecret(SecretUrn.parse(ownedUrn)));
+ }
+ }
+
+ @Test
+ void testSchemaAlterRemovePropertyDeletesWriteThroughSecret() {
+ try (SecretManager secretManager = memorySecretManager()) {
+ Map<String, String> props = new HashMap<>();
+ List<SecretMaterial> written = new ArrayList<>();
+ String urn =
+ secretManager.alterSetSecretBinding(
+ props, "schema", 9L, "k2", new SecretBinding("memory", "old"),
written);
+
+ secretManager.alterRemoveProperty(props, "schema", 9L, "k2");
+
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
secretManager.getRegistry().getProvider("memory").readSecret(SecretUrn.parse(urn)));
+ }
+ }
+
+ @Test
+ void testFilesetAlterRemovePropertyDeletesWriteThroughSecret() {
+ try (SecretManager secretManager = memorySecretManager()) {
+ Map<String, String> props = new HashMap<>();
+ List<SecretMaterial> written = new ArrayList<>();
+ String urn =
+ secretManager.alterSetSecretBinding(
+ props, "fileset", 11L, "k2", new SecretBinding("memory", "old"),
written);
+
+ secretManager.alterRemoveProperty(props, "fileset", 11L, "k2");
+
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
secretManager.getRegistry().getProvider("memory").readSecret(SecretUrn.parse(urn)));
+ }
+ }
+
+ @Test
+ void testRejectMaskedSetPropertyAndRawUrn() {
+ try (SecretManager secretManager = memorySecretManager()) {
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ secretManager.alterSetProperty(
+ new HashMap<>(), "catalog", 1L, "jdbc-password", "******"));
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ secretManager.alterSetProperty(
+ new HashMap<>(),
+ "catalog",
+ 1L,
+ "jdbc-password",
+ "urn:gravitino-secret:memory:catalog:1:jdbc-password"));
+ }
+ }
+
+ private static SecretManager memorySecretManager() {
+ Config config = new Config(false) {};
+ Properties properties = new Properties();
+ properties.setProperty(SecretProviderRegistry.GRAVITINO_SECRET_PROVIDERS,
"memory");
+ properties.setProperty(
+ SecretProviderRegistry.GRAVITINO_SECRET_PROVIDER_PREFIX
+ + "memory."
+ + SecretProviderRegistry.CLASS_NAME,
+ InMemorySecretsProvider.class.getName());
+ config.loadFromProperties(properties);
+ return new SecretManager(config);
+ }
+}
diff --git a/docs/open-api/filesets.yaml b/docs/open-api/filesets.yaml
index 6ed8fe32bc..ae5f43b717 100644
--- a/docs/open-api/filesets.yaml
+++ b/docs/open-api/filesets.yaml
@@ -379,6 +379,8 @@ components:
- $ref: "#/components/schemas/SetFilesetPropertyRequest"
- $ref: "#/components/schemas/UpdateFilesetCommentRequest"
- $ref: "#/components/schemas/RemoveFilesetPropertyRequest"
+ - $ref: "#/components/schemas/SetFilesetSecretBindingRequest"
+ - $ref: "#/components/schemas/SetFilesetSecretReferenceRequest"
discriminator:
propertyName: "@type"
mapping:
@@ -386,6 +388,8 @@ components:
setProperty: "#/components/schemas/SetFilesetPropertyRequest"
updateComment: "#/components/schemas/UpdateFilesetCommentRequest"
removeProperty: "#/components/schemas/RemoveFilesetPropertyRequest"
+ setSecretBinding:
"#/components/schemas/SetFilesetSecretBindingRequest"
+ setSecretReference:
"#/components/schemas/SetFilesetSecretReferenceRequest"
RenameFilesetRequest:
type: object
@@ -468,6 +472,69 @@ components:
"property": "key1"
}
+ SetFilesetSecretBindingRequest:
+ type: object
+ required:
+ - "@type"
+ - property
+ - provider
+ - plaintext
+ properties:
+ "@type":
+ type: string
+ description: The type of the update
+ enum:
+ - setSecretBinding
+ property:
+ type: string
+ description: The property to bind
+ provider:
+ type: string
+ description: Registered secrets-provider instance name
+ plaintext:
+ type: string
+ description: Plaintext secret to write through
+ format: password
+ example: {
+ "@type": "setSecretBinding",
+ "property": "password",
+ "provider": "env",
+ "plaintext": "secret-value"
+ }
+
+ SetFilesetSecretReferenceRequest:
+ type: object
+ required:
+ - "@type"
+ - property
+ - provider
+ properties:
+ "@type":
+ type: string
+ description: The type of the update
+ enum:
+ - setSecretReference
+ property:
+ type: string
+ description: The property to bind
+ provider:
+ type: string
+ description: Registered secrets-provider instance name
+ attributes:
+ type: object
+ description: Provider-specific locator keys (empty object if none)
+ additionalProperties:
+ type: string
+ default: {}
+ example: {
+ "@type": "setSecretReference",
+ "property": "password",
+ "provider": "vault",
+ "attributes": {
+ "path": "secret/data/my-password"
+ }
+ }
+
RemoveFilesetCommentRequest:
type: object
required: