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 0840b024e9 [#12548] feat(api): add policy-on-tag APIs (#12549)
0840b024e9 is described below

commit 0840b024e977e5f86eeb52a9a049d8c89727101e
Author: roryqi <[email protected]>
AuthorDate: Tue Aug 25 21:23:19 2026 +0800

    [#12548] feat(api): add policy-on-tag APIs (#12549)
    
    ### What changes were proposed in this pull request?
    
    Add the public Java API contracts for policy-on-tag:
    
    1. Add `PolicyTagAssociation`.
    2. Add the extensible `PolicySelector` interface and the basic
    `TagValuePolicySelector` implementation.
    3. Add policy-to-tag association operations to `PolicyOperations` and
    `TagOperations`.
    4. Add `VIEW_POLICY` and `VIEW_TAG` privileges.
    5. Add tests for tag-value selector matching and privilege binding
    scopes.
    
    `PolicySelector.type()` returns a string identifier so future selector
    shapes can be added
    without extending a closed enum.
    
    The new operation methods provide default implementations that throw
    `UnsupportedOperationException`, keeping existing implementations
    source-compatible
    until the implementation PRs override them.
    
    ### Why are the changes needed?
    
    These APIs establish the public contracts required by the policy-on-tag
    governance
    model and allow the implementation to be reviewed in smaller follow-up
    PRs.
    
    Fix: #12548
    
    Part of #12176.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. It adds evolving Java APIs for policy-to-tag associations,
    extensible policy
    selectors, and policy/tag view privileges. This PR does not add runtime
    implementation
    behavior.
    
    ### How was this patch tested?
    
    - `./gradlew spotlessApply`
    - `./gradlew :api:test :clients:client-java:compileJava`
    - `git diff --check`
---
 .../apache/gravitino/authorization/Privilege.java  |  6 +-
 .../apache/gravitino/authorization/Privileges.java | 70 ++++++++++++++++++
 .../PolicyAlreadyAssociatedException.java          |  4 +-
 .../apache/gravitino/policy/AllValuesSelector.java | 47 ++++++++++++
 .../policy/PolicyAssociationSelector.java          | 33 +++++++++
 .../apache/gravitino/policy/PolicyOperations.java  | 16 +++++
 .../gravitino/policy/PolicyTagAssociation.java     | 42 +++++++++++
 .../apache/gravitino/policy/SupportsPolicies.java  | 14 ++--
 .../apache/gravitino/policy/TagValueSelector.java  | 83 ++++++++++++++++++++++
 .../org/apache/gravitino/tag/TagOperations.java    | 62 ++++++++++++++++
 .../authorization/TestSecurableObjects.java        |  9 +++
 .../gravitino/policy/TestAllValuesSelector.java    | 33 +++++++++
 .../gravitino/policy/TestTagValueSelector.java     | 40 +++++++++++
 design-docs/policy-on-tag.md                       | 40 ++++++-----
 14 files changed, 470 insertions(+), 29 deletions(-)

diff --git 
a/api/src/main/java/org/apache/gravitino/authorization/Privilege.java 
b/api/src/main/java/org/apache/gravitino/authorization/Privilege.java
index 9122a8d25b..b004909913 100644
--- a/api/src/main/java/org/apache/gravitino/authorization/Privilege.java
+++ b/api/src/main/java/org/apache/gravitino/authorization/Privilege.java
@@ -153,7 +153,11 @@ public interface Privilege {
     /** The privilege to alter a function's metadata. */
     MODIFY_FUNCTION(0L, 1L << 32),
     /** The privilege to probe whether a table-like object exists. */
-    PROBE_TABLE_LIKE(0L, 1L << 33);
+    PROBE_TABLE_LIKE(0L, 1L << 33),
+    /** The privilege to view a tag. */
+    VIEW_TAG(0L, 1L << 34),
+    /** The privilege to view a policy. */
+    VIEW_POLICY(0L, 1L << 35);
 
     private final long highBits;
     private final long lowBits;
diff --git 
a/api/src/main/java/org/apache/gravitino/authorization/Privileges.java 
b/api/src/main/java/org/apache/gravitino/authorization/Privileges.java
index 5c217bc5cd..abe1c3f88f 100644
--- a/api/src/main/java/org/apache/gravitino/authorization/Privileges.java
+++ b/api/src/main/java/org/apache/gravitino/authorization/Privileges.java
@@ -184,10 +184,14 @@ public class Privileges {
         return CreateTag.allow();
       case APPLY_TAG:
         return ApplyTag.allow();
+      case VIEW_TAG:
+        return ViewTag.allow();
 
         // Policy
       case APPLY_POLICY:
         return ApplyPolicy.allow();
+      case VIEW_POLICY:
+        return ViewPolicy.allow();
       case CREATE_POLICY:
         return CreatePolicy.allow();
 
@@ -308,10 +312,14 @@ public class Privileges {
         return CreateTag.deny();
       case APPLY_TAG:
         return ApplyTag.deny();
+      case VIEW_TAG:
+        return ViewTag.deny();
 
         // Policy
       case APPLY_POLICY:
         return ApplyPolicy.deny();
+      case VIEW_POLICY:
+        return ViewPolicy.deny();
       case CREATE_POLICY:
         return CreatePolicy.deny();
 
@@ -1183,6 +1191,36 @@ public class Privileges {
     }
   }
 
+  /** The privilege to view tag metadata and associations. */
+  public static final class ViewTag extends GenericPrivilege<ViewTag> {
+
+    private static final ViewTag ALLOW_INSTANCE = new ViewTag(Condition.ALLOW, 
Name.VIEW_TAG);
+    private static final ViewTag DENY_INSTANCE = new ViewTag(Condition.DENY, 
Name.VIEW_TAG);
+
+    private ViewTag(Condition condition, Name name) {
+      super(condition, name);
+    }
+
+    /**
+     * @return The instance with allow condition of the privilege.
+     */
+    public static ViewTag allow() {
+      return ALLOW_INSTANCE;
+    }
+
+    /**
+     * @return The instance with deny condition of the privilege.
+     */
+    public static ViewTag deny() {
+      return DENY_INSTANCE;
+    }
+
+    @Override
+    public boolean canBindTo(MetadataObject.Type type) {
+      return type == MetadataObject.Type.METALAKE || type == 
MetadataObject.Type.TAG;
+    }
+  }
+
   /** The privilege to create a tag */
   public static class CreatePolicy extends GenericPrivilege<CreatePolicy> {
     private static final CreatePolicy ALLOW_INSTANCE =
@@ -1286,6 +1324,38 @@ public class Privileges {
     }
   }
 
+  /** The privilege to view policy metadata, content, and associations. */
+  public static final class ViewPolicy extends GenericPrivilege<ViewPolicy> {
+
+    private static final ViewPolicy ALLOW_INSTANCE =
+        new ViewPolicy(Condition.ALLOW, Name.VIEW_POLICY);
+    private static final ViewPolicy DENY_INSTANCE =
+        new ViewPolicy(Condition.DENY, Name.VIEW_POLICY);
+
+    private ViewPolicy(Condition condition, Name name) {
+      super(condition, name);
+    }
+
+    /**
+     * @return The instance with allow condition of the privilege.
+     */
+    public static ViewPolicy allow() {
+      return ALLOW_INSTANCE;
+    }
+
+    /**
+     * @return The instance with deny condition of the privilege.
+     */
+    public static ViewPolicy deny() {
+      return DENY_INSTANCE;
+    }
+
+    @Override
+    public boolean canBindTo(MetadataObject.Type type) {
+      return type == MetadataObject.Type.METALAKE || type == 
MetadataObject.Type.POLICY;
+    }
+  }
+
   /** The privilege to register a job template. */
   public static class RegisterJobTemplate extends 
GenericPrivilege<RegisterJobTemplate> {
     private static final RegisterJobTemplate ALLOW_INSTANCE =
diff --git 
a/api/src/main/java/org/apache/gravitino/exceptions/PolicyAlreadyAssociatedException.java
 
b/api/src/main/java/org/apache/gravitino/exceptions/PolicyAlreadyAssociatedException.java
index 96f3d36582..579819c5fe 100644
--- 
a/api/src/main/java/org/apache/gravitino/exceptions/PolicyAlreadyAssociatedException.java
+++ 
b/api/src/main/java/org/apache/gravitino/exceptions/PolicyAlreadyAssociatedException.java
@@ -20,9 +20,7 @@ package org.apache.gravitino.exceptions;
 
 import com.google.errorprone.annotations.FormatMethod;
 
-/**
- * Exception thrown when a policy with specified name is already associated 
with a metadata object.
- */
+/** Exception thrown when a policy is already associated with a metadata 
object or tag. */
 public class PolicyAlreadyAssociatedException extends AlreadyExistsException {
 
   /**
diff --git 
a/api/src/main/java/org/apache/gravitino/policy/AllValuesSelector.java 
b/api/src/main/java/org/apache/gravitino/policy/AllValuesSelector.java
new file mode 100644
index 0000000000..75e2105b2c
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/policy/AllValuesSelector.java
@@ -0,0 +1,47 @@
+/*
+ * 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.policy;
+
+import org.apache.gravitino.annotation.Evolving;
+
+/** A policy association selector that matches whenever the effective tag 
assignment exists. */
+@Evolving
+public final class AllValuesSelector implements PolicyAssociationSelector {
+
+  /** The selector type for tag-presence matching regardless of assignment 
values. */
+  public static final String TYPE = "ALL_VALUES";
+
+  private static final AllValuesSelector INSTANCE = new AllValuesSelector();
+
+  private AllValuesSelector() {}
+
+  /**
+   * Returns the selector that matches by tag presence, regardless of 
assignment values.
+   *
+   * @return The all-values selector.
+   */
+  public static AllValuesSelector get() {
+    return INSTANCE;
+  }
+
+  @Override
+  public String type() {
+    return TYPE;
+  }
+}
diff --git 
a/api/src/main/java/org/apache/gravitino/policy/PolicyAssociationSelector.java 
b/api/src/main/java/org/apache/gravitino/policy/PolicyAssociationSelector.java
new file mode 100644
index 0000000000..3aa1a37faa
--- /dev/null
+++ 
b/api/src/main/java/org/apache/gravitino/policy/PolicyAssociationSelector.java
@@ -0,0 +1,33 @@
+/*
+ * 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.policy;
+
+import org.apache.gravitino.annotation.Evolving;
+
+/** A selector attached to a policy-to-tag association that restricts when it 
applies. */
+@Evolving
+public interface PolicyAssociationSelector {
+
+  /**
+   * Returns the selector type identifier.
+   *
+   * @return The selector type.
+   */
+  String type();
+}
diff --git 
a/api/src/main/java/org/apache/gravitino/policy/PolicyOperations.java 
b/api/src/main/java/org/apache/gravitino/policy/PolicyOperations.java
index 1bbd8062ac..9a52b2cf57 100644
--- a/api/src/main/java/org/apache/gravitino/policy/PolicyOperations.java
+++ b/api/src/main/java/org/apache/gravitino/policy/PolicyOperations.java
@@ -106,4 +106,20 @@ public interface PolicyOperations {
    * @return True if the policy is deleted, false if the policy does not exist.
    */
   boolean deletePolicy(String name);
+
+  /**
+   * Lists all direct tag associations for a policy.
+   *
+   * <p>The returned associations are not filtered by their selectors. A 
selector is evaluated
+   * against the effective tag assignment of a specific metadata object only 
when resolving policies
+   * for that object.
+   *
+   * @param policyName The policy name.
+   * @return The direct tag associations, including their selectors.
+   * @throws UnsupportedOperationException If listing policy-to-tag 
associations is not supported.
+   */
+  default PolicyTagAssociation[] listTagAssociationsForPolicy(String 
policyName) {
+    throw new UnsupportedOperationException(
+        "Listing tag associations for a policy is not supported");
+  }
 }
diff --git 
a/api/src/main/java/org/apache/gravitino/policy/PolicyTagAssociation.java 
b/api/src/main/java/org/apache/gravitino/policy/PolicyTagAssociation.java
new file mode 100644
index 0000000000..ebfc46acb1
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/policy/PolicyTagAssociation.java
@@ -0,0 +1,42 @@
+/*
+ * 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.policy;
+
+import org.apache.gravitino.annotation.Evolving;
+import org.apache.gravitino.tag.Tag;
+
+/** A policy-to-tag association and its required assignment selector. */
+@Evolving
+public interface PolicyTagAssociation {
+
+  /**
+   * @return The associated policy.
+   */
+  Policy policy();
+
+  /**
+   * @return The associated tag.
+   */
+  Tag tag();
+
+  /**
+   * @return The non-null selector for this association.
+   */
+  PolicyAssociationSelector selector();
+}
diff --git 
a/api/src/main/java/org/apache/gravitino/policy/SupportsPolicies.java 
b/api/src/main/java/org/apache/gravitino/policy/SupportsPolicies.java
index 65356fd34b..21af7de6be 100644
--- a/api/src/main/java/org/apache/gravitino/policy/SupportsPolicies.java
+++ b/api/src/main/java/org/apache/gravitino/policy/SupportsPolicies.java
@@ -23,27 +23,29 @@ import 
org.apache.gravitino.exceptions.NoSuchPolicyException;
 import org.apache.gravitino.exceptions.PolicyAlreadyAssociatedException;
 
 /**
- * The interface for supporting getting or associating policies with a 
metadata object. This
- * interface will be mixed with metadata objects to provide policy operations.
+ * Interface for policy operations on a metadata object.
+ *
+ * <p>The lookup methods return policies derived from the object's effective 
tag assignments and
+ * matching policy-to-tag associations.
  */
 @Evolving
 public interface SupportsPolicies {
   /**
-   * @return List all the policy names for the specific object.
+   * @return The names of the policies that apply to the metadata object.
    */
   String[] listPolicies();
 
   /**
-   * @return List all the policies with details for the specific object.
+   * @return The policies that apply to the metadata object.
    */
   Policy[] listPolicyInfos();
 
   /**
-   * Get a policy by its name for the specific object.
+   * Get an applicable policy by its name for the metadata object.
    *
    * @param name The name of the policy.
    * @return The policy.
-   * @throws NoSuchPolicyException If the policy does not associate with the 
object.
+   * @throws NoSuchPolicyException If the policy does not apply to the object.
    */
   Policy getPolicy(String name) throws NoSuchPolicyException;
 
diff --git 
a/api/src/main/java/org/apache/gravitino/policy/TagValueSelector.java 
b/api/src/main/java/org/apache/gravitino/policy/TagValueSelector.java
new file mode 100644
index 0000000000..e499c00a73
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/policy/TagValueSelector.java
@@ -0,0 +1,83 @@
+/*
+ * 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.policy;
+
+import com.google.common.base.Preconditions;
+import java.util.Objects;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.annotation.Evolving;
+
+/** A policy association selector that matches one exact tag assignment value. 
*/
+@Evolving
+public final class TagValueSelector implements PolicyAssociationSelector {
+
+  /** The selector type for exact tag assignment value matching. */
+  public static final String TYPE = "TAG_VALUE";
+
+  private final String value;
+
+  private TagValueSelector(String value) {
+    this.value = value;
+  }
+
+  /**
+   * Creates a selector that matches one exact tag assignment value.
+   *
+   * @param value The tag assignment value to match.
+   * @return The selector.
+   */
+  public static TagValueSelector of(String value) {
+    Preconditions.checkArgument(StringUtils.isNotBlank(value), "Selector value 
cannot be blank");
+    return new TagValueSelector(value);
+  }
+
+  @Override
+  public String type() {
+    return TYPE;
+  }
+
+  /**
+   * @return The exact tag assignment value matched by this selector.
+   */
+  public String value() {
+    return value;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof TagValueSelector)) {
+      return false;
+    }
+    TagValueSelector that = (TagValueSelector) o;
+    return Objects.equals(value, that.value);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(value);
+  }
+
+  @Override
+  public String toString() {
+    return "TagValueSelector{value='" + value + "'}";
+  }
+}
diff --git a/api/src/main/java/org/apache/gravitino/tag/TagOperations.java 
b/api/src/main/java/org/apache/gravitino/tag/TagOperations.java
index ea9045d92b..4f0f41b561 100644
--- a/api/src/main/java/org/apache/gravitino/tag/TagOperations.java
+++ b/api/src/main/java/org/apache/gravitino/tag/TagOperations.java
@@ -23,7 +23,11 @@ import java.util.Map;
 import org.apache.gravitino.annotation.Evolving;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
 import org.apache.gravitino.exceptions.NoSuchTagException;
+import org.apache.gravitino.exceptions.PolicyAlreadyAssociatedException;
 import org.apache.gravitino.exceptions.TagAlreadyExistsException;
+import org.apache.gravitino.policy.AllValuesSelector;
+import org.apache.gravitino.policy.PolicyAssociationSelector;
+import org.apache.gravitino.policy.PolicyTagAssociation;
 
 /**
  * Interface for supporting global tag operations. This interface will provide 
tag listing, getting,
@@ -115,4 +119,62 @@ public interface TagOperations {
    * @return True if the tag is deleted, false if the tag does not exist.
    */
   boolean deleteTag(String name);
+
+  /**
+   * Lists all direct policy associations for a tag.
+   *
+   * <p>The returned associations are not filtered by their selectors. A 
selector is evaluated
+   * against the effective tag assignment of a specific metadata object only 
when resolving policies
+   * for that object.
+   *
+   * @param tagName The tag name.
+   * @return The direct policy associations, including their selectors.
+   * @throws UnsupportedOperationException If listing policy-to-tag 
associations is not supported.
+   */
+  default PolicyTagAssociation[] listPolicyAssociationsForTag(String tagName) {
+    throw new UnsupportedOperationException(
+        "Listing policy associations for a tag is not supported");
+  }
+
+  /**
+   * Adds one policy association for a tag using {@link AllValuesSelector}. It 
matches by tag
+   * presence regardless of assignment values and does not replace an existing 
association.
+   *
+   * @param tagName The tag name.
+   * @param policyName The policy name.
+   * @return The resulting association.
+   * @throws PolicyAlreadyAssociatedException If the policy is already 
associated with the tag.
+   * @throws UnsupportedOperationException If adding policy-to-tag 
associations is not supported.
+   */
+  default PolicyTagAssociation addPolicyForTag(String tagName, String 
policyName)
+      throws PolicyAlreadyAssociatedException {
+    return addPolicyForTag(tagName, policyName, AllValuesSelector.get());
+  }
+
+  /**
+   * Adds one policy association for a tag. It does not replace an existing 
association.
+   *
+   * @param tagName The tag name.
+   * @param policyName The policy name.
+   * @param selector The non-null policy association selector.
+   * @return The resulting association.
+   * @throws PolicyAlreadyAssociatedException If the policy is already 
associated with the tag.
+   * @throws UnsupportedOperationException If adding policy-to-tag 
associations is not supported.
+   */
+  default PolicyTagAssociation addPolicyForTag(
+      String tagName, String policyName, PolicyAssociationSelector selector)
+      throws PolicyAlreadyAssociatedException {
+    throw new UnsupportedOperationException("Adding a policy for a tag is not 
supported");
+  }
+
+  /**
+   * Removes one policy association from a tag.
+   *
+   * @param tagName The tag name.
+   * @param policyName The policy name.
+   * @throws UnsupportedOperationException If removing policy-to-tag 
associations is not supported.
+   */
+  default void removePolicyFromTag(String tagName, String policyName) {
+    throw new UnsupportedOperationException("Removing a policy from a tag is 
not supported");
+  }
 }
diff --git 
a/api/src/test/java/org/apache/gravitino/authorization/TestSecurableObjects.java
 
b/api/src/test/java/org/apache/gravitino/authorization/TestSecurableObjects.java
index 061f421fe7..5463a4c3bd 100644
--- 
a/api/src/test/java/org/apache/gravitino/authorization/TestSecurableObjects.java
+++ 
b/api/src/test/java/org/apache/gravitino/authorization/TestSecurableObjects.java
@@ -204,8 +204,10 @@ public class TestSecurableObjects {
     Privilege useModel = Privileges.UseModel.allow();
     Privilege createTag = Privileges.CreateTag.allow();
     Privilege applyTag = Privileges.ApplyTag.allow();
+    Privilege viewTag = Privileges.ViewTag.allow();
     Privilege createPolicy = Privileges.CreatePolicy.allow();
     Privilege applyPolicy = Privileges.ApplyPolicy.allow();
+    Privilege viewPolicy = Privileges.ViewPolicy.allow();
     Privilege registerJobTemplate = Privileges.RegisterJobTemplate.allow();
     Privilege runJob = Privileges.RunJob.allow();
     Privilege useJobTemplate = Privileges.UseJobTemplate.allow();
@@ -215,6 +217,13 @@ public class TestSecurableObjects {
     Privilege executeFunction = Privileges.ExecuteFunction.allow();
     Privilege modifyFunction = Privileges.ModifyFunction.allow();
 
+    Assertions.assertTrue(viewTag.canBindTo(MetadataObject.Type.METALAKE));
+    Assertions.assertTrue(viewTag.canBindTo(MetadataObject.Type.TAG));
+    Assertions.assertFalse(viewTag.canBindTo(MetadataObject.Type.POLICY));
+    Assertions.assertTrue(viewPolicy.canBindTo(MetadataObject.Type.METALAKE));
+    Assertions.assertTrue(viewPolicy.canBindTo(MetadataObject.Type.POLICY));
+    Assertions.assertFalse(viewPolicy.canBindTo(MetadataObject.Type.TAG));
+
     // Test create catalog
     
Assertions.assertTrue(createCatalog.canBindTo(MetadataObject.Type.METALAKE));
     
Assertions.assertFalse(createCatalog.canBindTo(MetadataObject.Type.CATALOG));
diff --git 
a/api/src/test/java/org/apache/gravitino/policy/TestAllValuesSelector.java 
b/api/src/test/java/org/apache/gravitino/policy/TestAllValuesSelector.java
new file mode 100644
index 0000000000..bf2895f25f
--- /dev/null
+++ b/api/src/test/java/org/apache/gravitino/policy/TestAllValuesSelector.java
@@ -0,0 +1,33 @@
+/*
+ * 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.policy;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestAllValuesSelector {
+
+  @Test
+  public void testAllValuesSelector() {
+    AllValuesSelector selector = AllValuesSelector.get();
+
+    Assertions.assertEquals("ALL_VALUES", selector.type());
+    Assertions.assertSame(selector, AllValuesSelector.get());
+  }
+}
diff --git 
a/api/src/test/java/org/apache/gravitino/policy/TestTagValueSelector.java 
b/api/src/test/java/org/apache/gravitino/policy/TestTagValueSelector.java
new file mode 100644
index 0000000000..e6456cb6b3
--- /dev/null
+++ b/api/src/test/java/org/apache/gravitino/policy/TestTagValueSelector.java
@@ -0,0 +1,40 @@
+/*
+ * 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.policy;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestTagValueSelector {
+
+  @Test
+  public void testTagValueSelector() {
+    TagValueSelector selector = TagValueSelector.of("finance");
+
+    Assertions.assertEquals("TAG_VALUE", selector.type());
+    Assertions.assertEquals("finance", selector.value());
+    Assertions.assertEquals(selector, TagValueSelector.of("finance"));
+  }
+
+  @Test
+  public void testRejectBlankSelectorValue() {
+    Assertions.assertThrows(IllegalArgumentException.class, () -> 
TagValueSelector.of(" "));
+    Assertions.assertThrows(IllegalArgumentException.class, () -> 
TagValueSelector.of(null));
+  }
+}
diff --git a/design-docs/policy-on-tag.md b/design-docs/policy-on-tag.md
index 00920bec44..f3f5515ca7 100644
--- a/design-docs/policy-on-tag.md
+++ b/design-docs/policy-on-tag.md
@@ -24,8 +24,8 @@
 ## Core Design
 
 The core model is that policies are associated with tag names, not directly 
with tag assignment
-values. A policy-to-tag relation may include a simple `selector` JSON object 
that decides whether
-the relation matches a metadata object's effective tag assignment values. The 
selector refines when
+values. A policy-to-tag relation contains a required `selector` JSON object 
that decides whether
+the relation matches a metadata object's effective tag assignment values. The 
selector defines when
 a policy bound to a tag name applies; it does not make policy objects bind 
directly to individual
 tag values.
 
@@ -145,7 +145,7 @@ Consumer -> ObjectPolicyResolver -> object policies
 | Policy on tag with selector | `policyA -> data_domain` with selector 
`{"type": "TAG_VALUE", "value": "finance"}` | Supports tag presence and simple 
valued-tag matching without an expression language | **Chosen for phase 1** |
 
 Phase 1 therefore makes an explicit behavior decision: a policy-to-tag 
relation is keyed by a tag
-definition and carries one optional `selector`. If `selector` is omitted or 
null, the relation
+definition and carries one required `selector`. If `selector.type` is 
`ALL_VALUES`, the relation
 matches by tag presence. If `selector.type` is `TAG_VALUE`, the relation 
matches only when the
 object's effective tag assignment contains `selector.value`.
 
@@ -161,7 +161,7 @@ The target model has four concepts:
 |---------|-------------|
 | Policy | A metalake-scoped governance rule with typed content, enabled 
state, audit information, and version history. |
 | Tag | A flat metalake-scoped classification object associated with metadata 
objects. Tags do not have parent or child tags. |
-| Policy-tag relation | A relation that binds one policy to one tag in the 
same metalake, optionally scoped by a simple selector. |
+| Policy-tag relation | A relation that binds one policy to one tag in the 
same metalake, scoped by one required selector. |
 | Object policy | A read-only policy result for a metadata object, derived 
from effective tags and matching policy-tag relations. |
 
 Object-side governance becomes:
@@ -212,7 +212,7 @@ table iceberg.db.orders
 
 If the table has a direct assignment `data_domain = ["risk", "ml"]`, the 
direct assignment wins.
 The `retention_finance` policy is not selected because `TAG_VALUE("finance")` 
does not match the
-effective values `["risk", "ml"]`. A policy-to-tag relation without a selector 
would match
+effective values `["risk", "ml"]`. An `ALL_VALUES` selector would match
 either assignment by tag presence.
 
 ### Policy Supported Object Types
@@ -311,22 +311,23 @@ Constraints and indexes:
 
 `selector` rules:
 
-1. An omitted or null `selector` matches the tag by presence.
-2. `TAG_VALUE` matches when the effective tag assignment contains the same 
value as
+1. `selector` is required and must not be null.
+2. `ALL_VALUES` matches whenever the effective tag assignment exists.
+3. `TAG_VALUE` matches when the effective tag assignment contains the same 
value as
    `selector.value`.
-3. `TAG_VALUE` contains one non-blank `value` string.
-4. If the tag defines allowed values, the selector value must be one of the 
allowed values.
-5. Selector JSON is canonicalized before storage and comparison.
-6. The first version supports only `TAG_VALUE`. It does not support value 
absence,
+4. `TAG_VALUE` contains one non-blank `value` string.
+5. If the tag defines allowed values, the selector value must be one of the 
allowed values.
+6. Selector JSON is canonicalized before storage and comparison.
+7. The first version supports `ALL_VALUES` and `TAG_VALUE`. It does not 
support value absence,
    negative matching, principals, scopes, or general expressions. Future 
versions can add new
    selector types in the same `selector` JSON field without changing the 
policy-to-tag relation
    model.
 
-### Selector Evolution Examples
+### Selector Types and Evolution Examples
 
-`TAG_VALUE` remains the basic selector for exact value matching. Future 
selector versions can make
-the current tag-presence behavior explicit through `ALL_VALUES` and use 
`EXPRESSION` for conditions
-that cannot be represented by one tag value.
+Phase 1 uses `ALL_VALUES` for explicit tag-presence matching and `TAG_VALUE` 
for exact value
+matching. Future selector versions can use `EXPRESSION` for conditions that 
cannot be represented
+by one tag value.
 
 ```json
 {
@@ -373,7 +374,8 @@ A future expression selector can combine values from 
multiple effective tags:
 This expression matches effective tag assignments `classification = ["pii"]` 
and
 `data_domain = ["finance"]`. It does not match if either tag is absent or its 
effective assignment
 does not contain the required value. These examples describe selector shapes 
only. Phase 1 supports
-only `TAG_VALUE`; the expression language and cross-tag lookup contract 
require a separate design.
+`ALL_VALUES` and `TAG_VALUE`; the expression language and cross-tag lookup 
contract require a
+separate design.
 
 ### ABAC Column Masking Example
 
@@ -459,7 +461,7 @@ Returns `404 Not Found` if the tag does not exist.
 
 | Field | Type | Required | Description |
 |-------|------|----------|-------------|
-| `selector` | object or null | no | Tag selector. Missing or null matches by 
tag presence. |
+| `selector` | object | yes | Required selector. `ALL_VALUES` matches by tag 
presence; `TAG_VALUE` matches one value. |
 
 ```json
 {
@@ -765,8 +767,8 @@ Rules:
 1. If no system iceberg compaction policy appears in the object policy result, 
TMS does not generate
    a compaction strategy from policy-on-tag.
 2. If a system iceberg compaction policy appears in the object policy result, 
TMS uses its content.
-3. A system iceberg compaction policy can omit `selector` for tag-presence 
behavior or use a
-   simple `TAG_VALUE` selector when maintenance behavior should depend on 
assignment values.
+3. A system iceberg compaction policy uses `ALL_VALUES` for tag-presence 
behavior or a simple
+   `TAG_VALUE` selector when maintenance behavior should depend on assignment 
values.
 4. TMS does not read direct object policy relations.
 
 ### User Process

Reply via email to