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