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:

Reply via email to