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 099acc8545 [#12176] feat(core): add policy-tag relation events (#12855)
099acc8545 is described below
commit 099acc8545dfb35271f0c18ff4d62287743e19fa
Author: roryqi <[email protected]>
AuthorDate: Mon Sep 7 15:56:57 2026 +0800
[#12176] feat(core): add policy-tag relation events (#12855)
### What changes were proposed in this pull request?
Add lifecycle events for create-only policy-to-tag relation changes:
1. Add pre, success, and failure events for adding a policy to a tag.
2. Add pre, success, and failure events for removing a policy from a
tag.
3. Keep pre-events intent-only and emit them before any business access.
4. Include the tag and policy identity in all events, plus the requested
selector for add events.
5. Delegate policy-tag operations through the hook and event dispatcher
layers and test the composed runtime chains.
6. Add v1 and v2 audit operation mappings, include relation details in
v2 audit output, and document the deprecated v1 limitation.
7. Keep add create-only and remove idempotent, and align the design
document with those contracts.
### Why are the changes needed?
Policy-to-tag mutations need listener and audit extension points. This
follow-up builds on the Core support merged in #12718.
Fix: #12176
### Does this PR introduce _any_ user-facing change?
No. This PR adds Core listener and audit events. REST and client APIs
are handled separately.
### How was this patch tested?
- ./gradlew :core:spotlessApply --no-build-cache
- ./gradlew :core:test --tests org.apache.gravitino.tag.TestTagManager
--tests org.apache.gravitino.audit.TestOperation --tests
org.apache.gravitino.audit.v2.TestCompatibilityUtils --tests
org.apache.gravitino.audit.v2.TestSimpleAuditLogV2 --tests
org.apache.gravitino.listener.TestPolicyTagDispatcherChain --tests
org.apache.gravitino.listener.api.event.TestPolicyTagRelationEvent
--no-build-cache
- git diff --check
---
.../java/org/apache/gravitino/audit/AuditLog.java | 14 ++
.../gravitino/audit/v2/CompatibilityUtils.java | 2 +
.../gravitino/hook/PolicyHookDispatcher.java | 6 +
.../apache/gravitino/hook/TagHookDispatcher.java | 18 ++
.../gravitino/listener/PolicyEventDispatcher.java | 6 +
.../gravitino/listener/TagEventDispatcher.java | 44 +++++
.../listener/api/event/AddPolicyForTagEvent.java | 100 +++++++++++
.../api/event/AddPolicyForTagFailureEvent.java | 102 +++++++++++
.../api/event/AddPolicyForTagPreEvent.java | 100 +++++++++++
.../listener/api/event/OperationType.java | 2 +
.../api/event/RemovePolicyFromTagEvent.java | 82 +++++++++
.../api/event/RemovePolicyFromTagFailureEvent.java | 81 +++++++++
.../api/event/RemovePolicyFromTagPreEvent.java | 80 +++++++++
.../org/apache/gravitino/audit/TestOperation.java | 44 +++++
.../gravitino/audit/v2/TestCompatibilityUtils.java | 2 +
.../gravitino/audit/v2/TestSimpleAuditLogV2.java | 27 +++
.../listener/TestPolicyTagDispatcherChain.java | 122 +++++++++++++
.../api/event/TestPolicyTagRelationEvent.java | 188 +++++++++++++++++++++
.../org/apache/gravitino/tag/TestTagManager.java | 3 +
design-docs/policy-on-tag.md | 34 ++--
20 files changed, 1043 insertions(+), 14 deletions(-)
diff --git a/core/src/main/java/org/apache/gravitino/audit/AuditLog.java
b/core/src/main/java/org/apache/gravitino/audit/AuditLog.java
index 7ebc32012e..070941a750 100644
--- a/core/src/main/java/org/apache/gravitino/audit/AuditLog.java
+++ b/core/src/main/java/org/apache/gravitino/audit/AuditLog.java
@@ -21,6 +21,8 @@ package org.apache.gravitino.audit;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
+import org.apache.gravitino.listener.api.event.AddPolicyForTagEvent;
+import org.apache.gravitino.listener.api.event.AddPolicyForTagFailureEvent;
import org.apache.gravitino.listener.api.event.AlterCatalogEvent;
import org.apache.gravitino.listener.api.event.AlterCatalogFailureEvent;
import org.apache.gravitino.listener.api.event.AlterFilesetEvent;
@@ -105,6 +107,8 @@ import
org.apache.gravitino.listener.api.event.PurgePartitionEvent;
import org.apache.gravitino.listener.api.event.PurgePartitionFailureEvent;
import org.apache.gravitino.listener.api.event.PurgeTableEvent;
import org.apache.gravitino.listener.api.event.PurgeTableFailureEvent;
+import org.apache.gravitino.listener.api.event.RemovePolicyFromTagEvent;
+import org.apache.gravitino.listener.api.event.RemovePolicyFromTagFailureEvent;
import
org.apache.gravitino.listener.api.event.server.AuthorizationDenialFailureEvent;
import org.apache.gravitino.listener.api.event.view.AlterViewEvent;
import org.apache.gravitino.listener.api.event.view.AlterViewFailureEvent;
@@ -330,6 +334,10 @@ public interface AuditLog {
LIST_TAGS_INFO,
+ ADD_POLICY_FOR_TAG,
+
+ REMOVE_POLICY_FROM_TAG,
+
CREATE_VIEW,
ALTER_VIEW,
@@ -616,6 +624,12 @@ public interface AuditLog {
return LOAD_FILESET;
} else if (event instanceof ListFilesetEvent || event instanceof
ListFilesetFailureEvent) {
return LIST_FILESET;
+ } else if (event instanceof AddPolicyForTagEvent
+ || event instanceof AddPolicyForTagFailureEvent) {
+ return ADD_POLICY_FOR_TAG;
+ } else if (event instanceof RemovePolicyFromTagEvent
+ || event instanceof RemovePolicyFromTagFailureEvent) {
+ return REMOVE_POLICY_FROM_TAG;
} else if (event instanceof AuthorizationDenialFailureEvent) {
return AUTHORIZATION_DENIAL;
} else {
diff --git
a/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
b/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
index 725bb5dba6..c968dc3c6e 100644
--- a/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
+++ b/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
@@ -75,6 +75,8 @@ public class CompatibilityUtils {
Operation.LIST_TAGS_INFO_FOR_METADATA_OBJECT)
.put(OperationType.LIST_METADATA_OBJECTS_FOR_TAG,
Operation.LIST_METADATA_OBJECTS_FOR_TAG)
.put(OperationType.LIST_TAGS_INFO, Operation.LIST_TAGS_INFO)
+ .put(OperationType.ADD_POLICY_FOR_TAG, Operation.ADD_POLICY_FOR_TAG)
+ .put(OperationType.REMOVE_POLICY_FROM_TAG,
Operation.REMOVE_POLICY_FROM_TAG)
.put(OperationType.DROP_FILESET, Operation.DROP_FILESET)
.put(OperationType.ALTER_FILESET, Operation.ALTER_FILESET)
.put(OperationType.CREATE_FILESET, Operation.CREATE_FILESET)
diff --git
a/core/src/main/java/org/apache/gravitino/hook/PolicyHookDispatcher.java
b/core/src/main/java/org/apache/gravitino/hook/PolicyHookDispatcher.java
index 57164c3af0..0b2ee2e34b 100644
--- a/core/src/main/java/org/apache/gravitino/hook/PolicyHookDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/hook/PolicyHookDispatcher.java
@@ -20,6 +20,7 @@ package org.apache.gravitino.hook;
import org.apache.gravitino.Entity;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.RelationalEntity;
import org.apache.gravitino.authorization.Owner;
import org.apache.gravitino.authorization.OwnerDispatcher;
import org.apache.gravitino.exceptions.NoSuchPolicyException;
@@ -104,6 +105,11 @@ public class PolicyHookDispatcher implements
PolicyDispatcher {
return dispatcher.listMetadataObjectsForPolicy(metalake, policyName);
}
+ @Override
+ public RelationalEntity<?>[] listTagAssociationsForPolicy(String metalake,
String policyName) {
+ return dispatcher.listTagAssociationsForPolicy(metalake, policyName);
+ }
+
@Override
public PolicyEntity[] listPolicyInfosForMetadataObject(
String metalake, MetadataObject metadataObject) {
diff --git
a/core/src/main/java/org/apache/gravitino/hook/TagHookDispatcher.java
b/core/src/main/java/org/apache/gravitino/hook/TagHookDispatcher.java
index 0596d4bead..0662767e3e 100644
--- a/core/src/main/java/org/apache/gravitino/hook/TagHookDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/hook/TagHookDispatcher.java
@@ -21,10 +21,12 @@ import java.util.Map;
import org.apache.gravitino.Entity;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.RelationalEntity;
import org.apache.gravitino.authorization.Owner;
import org.apache.gravitino.authorization.OwnerDispatcher;
import org.apache.gravitino.exceptions.NoSuchTagException;
import org.apache.gravitino.exceptions.TagAlreadyExistsException;
+import org.apache.gravitino.policy.PolicyAssociationSelector;
import org.apache.gravitino.tag.Tag;
import org.apache.gravitino.tag.TagChange;
import org.apache.gravitino.tag.TagDispatcher;
@@ -106,6 +108,22 @@ public class TagHookDispatcher implements TagDispatcher {
return dispatcher.listMetadataObjectsForTag(metalake, name, value);
}
+ @Override
+ public RelationalEntity<?>[] listPolicyAssociationsForTag(String metalake,
String name) {
+ return dispatcher.listPolicyAssociationsForTag(metalake, name);
+ }
+
+ @Override
+ public void addPolicyForTag(
+ String metalake, String tagName, String policyName,
PolicyAssociationSelector selector) {
+ dispatcher.addPolicyForTag(metalake, tagName, policyName, selector);
+ }
+
+ @Override
+ public void removePolicyFromTag(String metalake, String tagName, String
policyName) {
+ dispatcher.removePolicyFromTag(metalake, tagName, policyName);
+ }
+
@Override
public String[] listTagsForMetadataObject(String metalake, MetadataObject
metadataObject) {
return dispatcher.listTagsForMetadataObject(metalake, metadataObject);
diff --git
a/core/src/main/java/org/apache/gravitino/listener/PolicyEventDispatcher.java
b/core/src/main/java/org/apache/gravitino/listener/PolicyEventDispatcher.java
index 80308dc476..1ac5d5d153 100644
---
a/core/src/main/java/org/apache/gravitino/listener/PolicyEventDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/listener/PolicyEventDispatcher.java
@@ -20,6 +20,7 @@ package org.apache.gravitino.listener;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.RelationalEntity;
import org.apache.gravitino.exceptions.NoSuchPolicyException;
import org.apache.gravitino.listener.api.event.policy.AlterPolicyEvent;
import org.apache.gravitino.listener.api.event.policy.AlterPolicyFailureEvent;
@@ -288,6 +289,11 @@ public class PolicyEventDispatcher implements
PolicyDispatcher {
}
}
+ @Override
+ public RelationalEntity<?>[] listTagAssociationsForPolicy(String metalake,
String policyName) {
+ return dispatcher.listTagAssociationsForPolicy(metalake, policyName);
+ }
+
@Override
public PolicyEntity[] listPolicyInfosForMetadataObject(
String metalake, MetadataObject metadataObject) {
diff --git
a/core/src/main/java/org/apache/gravitino/listener/TagEventDispatcher.java
b/core/src/main/java/org/apache/gravitino/listener/TagEventDispatcher.java
index 2df08a2d38..bc19a674fc 100644
--- a/core/src/main/java/org/apache/gravitino/listener/TagEventDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/listener/TagEventDispatcher.java
@@ -20,8 +20,12 @@ package org.apache.gravitino.listener;
import java.util.Map;
import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.RelationalEntity;
import org.apache.gravitino.exceptions.NoSuchTagException;
import org.apache.gravitino.exceptions.TagAlreadyExistsException;
+import org.apache.gravitino.listener.api.event.AddPolicyForTagEvent;
+import org.apache.gravitino.listener.api.event.AddPolicyForTagFailureEvent;
+import org.apache.gravitino.listener.api.event.AddPolicyForTagPreEvent;
import org.apache.gravitino.listener.api.event.AlterTagEvent;
import org.apache.gravitino.listener.api.event.AlterTagFailureEvent;
import org.apache.gravitino.listener.api.event.AlterTagPreEvent;
@@ -55,7 +59,11 @@ import
org.apache.gravitino.listener.api.event.ListTagsInfoForMetadataObjectFail
import
org.apache.gravitino.listener.api.event.ListTagsInfoForMetadataObjectPreEvent;
import org.apache.gravitino.listener.api.event.ListTagsInfoPreEvent;
import org.apache.gravitino.listener.api.event.ListTagsPreEvent;
+import org.apache.gravitino.listener.api.event.RemovePolicyFromTagEvent;
+import org.apache.gravitino.listener.api.event.RemovePolicyFromTagFailureEvent;
+import org.apache.gravitino.listener.api.event.RemovePolicyFromTagPreEvent;
import org.apache.gravitino.listener.api.info.TagInfo;
+import org.apache.gravitino.policy.PolicyAssociationSelector;
import org.apache.gravitino.tag.Tag;
import org.apache.gravitino.tag.TagChange;
import org.apache.gravitino.tag.TagDispatcher;
@@ -238,6 +246,42 @@ public class TagEventDispatcher implements TagDispatcher {
}
}
+ @Override
+ public RelationalEntity<?>[] listPolicyAssociationsForTag(String metalake,
String name) {
+ return dispatcher.listPolicyAssociationsForTag(metalake, name);
+ }
+
+ @Override
+ public void addPolicyForTag(
+ String metalake, String tagName, String policyName,
PolicyAssociationSelector selector) {
+ String user = PrincipalUtils.getCurrentUserName();
+ eventBus.dispatchEvent(
+ new AddPolicyForTagPreEvent(user, metalake, tagName, policyName,
selector));
+ try {
+ dispatcher.addPolicyForTag(metalake, tagName, policyName, selector);
+ eventBus.dispatchEvent(
+ new AddPolicyForTagEvent(user, metalake, tagName, policyName,
selector));
+ } catch (Exception e) {
+ eventBus.dispatchEvent(
+ new AddPolicyForTagFailureEvent(user, metalake, tagName, policyName,
selector, e));
+ throw e;
+ }
+ }
+
+ @Override
+ public void removePolicyFromTag(String metalake, String tagName, String
policyName) {
+ String user = PrincipalUtils.getCurrentUserName();
+ eventBus.dispatchEvent(new RemovePolicyFromTagPreEvent(user, metalake,
tagName, policyName));
+ try {
+ dispatcher.removePolicyFromTag(metalake, tagName, policyName);
+ eventBus.dispatchEvent(new RemovePolicyFromTagEvent(user, metalake,
tagName, policyName));
+ } catch (Exception e) {
+ eventBus.dispatchEvent(
+ new RemovePolicyFromTagFailureEvent(user, metalake, tagName,
policyName, e));
+ throw e;
+ }
+ }
+
@Override
public String[] listTagsForMetadataObject(String metalake, MetadataObject
metadataObject) {
eventBus.dispatchEvent(
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/AddPolicyForTagEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/AddPolicyForTagEvent.java
new file mode 100644
index 0000000000..90285ebe7e
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/AddPolicyForTagEvent.java
@@ -0,0 +1,100 @@
+/*
+ * 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.listener.api.event;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.json.PolicyAssociationSelectorSerde;
+import org.apache.gravitino.policy.PolicyAssociationSelector;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Represents an event triggered after successfully adding a policy to a tag.
*/
+@DeveloperApi
+public final class AddPolicyForTagEvent extends TagEvent {
+ private final String metalake;
+ private final String tagName;
+ private final String policyName;
+ private final PolicyAssociationSelector requestedSelector;
+
+ /**
+ * Constructs an event triggered after successfully adding a policy to a tag.
+ *
+ * @param user The user who initiated the operation.
+ * @param metalake The metalake containing the tag and policy.
+ * @param tagName The tag name.
+ * @param policyName The policy name.
+ * @param requestedSelector The requested policy association selector.
+ */
+ public AddPolicyForTagEvent(
+ String user,
+ String metalake,
+ String tagName,
+ String policyName,
+ PolicyAssociationSelector requestedSelector) {
+ super(user, NameIdentifierUtil.ofTag(metalake, tagName));
+ this.metalake = metalake;
+ this.tagName = tagName;
+ this.policyName = policyName;
+ this.requestedSelector = requestedSelector;
+ }
+
+ /**
+ * @return The metalake containing the tag and policy.
+ */
+ public String metalake() {
+ return metalake;
+ }
+
+ /**
+ * @return The tag name.
+ */
+ public String tagName() {
+ return tagName;
+ }
+
+ /**
+ * @return The policy name.
+ */
+ public String policyName() {
+ return policyName;
+ }
+
+ /**
+ * @return The requested policy association selector.
+ */
+ public PolicyAssociationSelector requestedSelector() {
+ return requestedSelector;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Map<String, String> ownCustomInfo() {
+ return ImmutableMap.of(
+ "policyName",
+ policyName,
+ "selector",
+ PolicyAssociationSelectorSerde.serialize(requestedSelector));
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.ADD_POLICY_FOR_TAG;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/AddPolicyForTagFailureEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/AddPolicyForTagFailureEvent.java
new file mode 100644
index 0000000000..bc0d19d12f
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/AddPolicyForTagFailureEvent.java
@@ -0,0 +1,102 @@
+/*
+ * 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.listener.api.event;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.json.PolicyAssociationSelectorSerde;
+import org.apache.gravitino.policy.PolicyAssociationSelector;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Represents an event triggered when adding a policy to a tag fails. */
+@DeveloperApi
+public final class AddPolicyForTagFailureEvent extends TagFailureEvent {
+ private final String metalake;
+ private final String tagName;
+ private final String policyName;
+ private final PolicyAssociationSelector requestedSelector;
+
+ /**
+ * Constructs an event triggered when adding a policy to a tag fails.
+ *
+ * @param user The user who initiated the operation.
+ * @param metalake The metalake containing the tag and policy.
+ * @param tagName The tag name.
+ * @param policyName The policy name.
+ * @param requestedSelector The requested policy association selector.
+ * @param exception The exception encountered during the operation.
+ */
+ public AddPolicyForTagFailureEvent(
+ String user,
+ String metalake,
+ String tagName,
+ String policyName,
+ PolicyAssociationSelector requestedSelector,
+ Exception exception) {
+ super(user, NameIdentifierUtil.ofTag(metalake, tagName), exception);
+ this.metalake = metalake;
+ this.tagName = tagName;
+ this.policyName = policyName;
+ this.requestedSelector = requestedSelector;
+ }
+
+ /**
+ * @return The metalake containing the tag and policy.
+ */
+ public String metalake() {
+ return metalake;
+ }
+
+ /**
+ * @return The tag name.
+ */
+ public String tagName() {
+ return tagName;
+ }
+
+ /**
+ * @return The policy name.
+ */
+ public String policyName() {
+ return policyName;
+ }
+
+ /**
+ * @return The requested policy association selector.
+ */
+ public PolicyAssociationSelector requestedSelector() {
+ return requestedSelector;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Map<String, String> ownCustomInfo() {
+ return ImmutableMap.of(
+ "policyName",
+ policyName,
+ "selector",
+ PolicyAssociationSelectorSerde.serialize(requestedSelector));
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.ADD_POLICY_FOR_TAG;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/AddPolicyForTagPreEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/AddPolicyForTagPreEvent.java
new file mode 100644
index 0000000000..b0300f58ab
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/AddPolicyForTagPreEvent.java
@@ -0,0 +1,100 @@
+/*
+ * 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.listener.api.event;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.json.PolicyAssociationSelectorSerde;
+import org.apache.gravitino.policy.PolicyAssociationSelector;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Represents an event triggered before adding a policy to a tag. */
+@DeveloperApi
+public final class AddPolicyForTagPreEvent extends TagPreEvent {
+ private final String metalake;
+ private final String tagName;
+ private final String policyName;
+ private final PolicyAssociationSelector requestedSelector;
+
+ /**
+ * Constructs an event triggered before adding a policy to a tag.
+ *
+ * @param user The user who initiated the operation.
+ * @param metalake The metalake containing the tag and policy.
+ * @param tagName The tag name.
+ * @param policyName The policy name.
+ * @param requestedSelector The requested policy association selector.
+ */
+ public AddPolicyForTagPreEvent(
+ String user,
+ String metalake,
+ String tagName,
+ String policyName,
+ PolicyAssociationSelector requestedSelector) {
+ super(user, NameIdentifierUtil.ofTag(metalake, tagName));
+ this.metalake = metalake;
+ this.tagName = tagName;
+ this.policyName = policyName;
+ this.requestedSelector = requestedSelector;
+ }
+
+ /**
+ * @return The metalake containing the tag and policy.
+ */
+ public String metalake() {
+ return metalake;
+ }
+
+ /**
+ * @return The tag name.
+ */
+ public String tagName() {
+ return tagName;
+ }
+
+ /**
+ * @return The policy name.
+ */
+ public String policyName() {
+ return policyName;
+ }
+
+ /**
+ * @return The requested policy association selector.
+ */
+ public PolicyAssociationSelector requestedSelector() {
+ return requestedSelector;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Map<String, String> customInfo() {
+ return ImmutableMap.of(
+ "policyName",
+ policyName,
+ "selector",
+ PolicyAssociationSelectorSerde.serialize(requestedSelector));
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.ADD_POLICY_FOR_TAG;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
index 5350c18562..d3bd1aa697 100644
---
a/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
@@ -45,6 +45,8 @@ public enum OperationType {
LIST_TAGS_INFO_FOR_METADATA_OBJECT,
LIST_METADATA_OBJECTS_FOR_TAG,
LIST_TAGS_INFO,
+ ADD_POLICY_FOR_TAG,
+ REMOVE_POLICY_FROM_TAG,
// Schema operations
CREATE_SCHEMA,
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/RemovePolicyFromTagEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/RemovePolicyFromTagEvent.java
new file mode 100644
index 0000000000..0cd0d7cbdc
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/RemovePolicyFromTagEvent.java
@@ -0,0 +1,82 @@
+/*
+ * 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.listener.api.event;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Represents an event triggered after successfully removing a policy from a
tag. */
+@DeveloperApi
+public final class RemovePolicyFromTagEvent extends TagEvent {
+ private final String metalake;
+ private final String tagName;
+ private final String policyName;
+
+ /**
+ * Constructs an event triggered after successfully removing a policy from a
tag.
+ *
+ * <p>A successful event means the idempotent remove operation completed. It
does not indicate
+ * whether an association existed before the operation.
+ *
+ * @param user The user who initiated the operation.
+ * @param metalake The metalake containing the tag and policy.
+ * @param tagName The tag name.
+ * @param policyName The policy name.
+ */
+ public RemovePolicyFromTagEvent(String user, String metalake, String
tagName, String policyName) {
+ super(user, NameIdentifierUtil.ofTag(metalake, tagName));
+ this.metalake = metalake;
+ this.tagName = tagName;
+ this.policyName = policyName;
+ }
+
+ /**
+ * @return The metalake containing the tag and policy.
+ */
+ public String metalake() {
+ return metalake;
+ }
+
+ /**
+ * @return The tag name.
+ */
+ public String tagName() {
+ return tagName;
+ }
+
+ /**
+ * @return The policy name.
+ */
+ public String policyName() {
+ return policyName;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Map<String, String> ownCustomInfo() {
+ return ImmutableMap.of("policyName", policyName);
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.REMOVE_POLICY_FROM_TAG;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/RemovePolicyFromTagFailureEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/RemovePolicyFromTagFailureEvent.java
new file mode 100644
index 0000000000..401ce037a1
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/RemovePolicyFromTagFailureEvent.java
@@ -0,0 +1,81 @@
+/*
+ * 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.listener.api.event;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Represents an event triggered when removing a policy from a tag fails. */
+@DeveloperApi
+public final class RemovePolicyFromTagFailureEvent extends TagFailureEvent {
+ private final String metalake;
+ private final String tagName;
+ private final String policyName;
+
+ /**
+ * Constructs an event triggered when removing a policy from a tag fails.
+ *
+ * @param user The user who initiated the operation.
+ * @param metalake The metalake containing the tag and policy.
+ * @param tagName The tag name.
+ * @param policyName The policy name.
+ * @param exception The exception encountered during the operation.
+ */
+ public RemovePolicyFromTagFailureEvent(
+ String user, String metalake, String tagName, String policyName,
Exception exception) {
+ super(user, NameIdentifierUtil.ofTag(metalake, tagName), exception);
+ this.metalake = metalake;
+ this.tagName = tagName;
+ this.policyName = policyName;
+ }
+
+ /**
+ * @return The metalake containing the tag and policy.
+ */
+ public String metalake() {
+ return metalake;
+ }
+
+ /**
+ * @return The tag name.
+ */
+ public String tagName() {
+ return tagName;
+ }
+
+ /**
+ * @return The policy name.
+ */
+ public String policyName() {
+ return policyName;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Map<String, String> ownCustomInfo() {
+ return ImmutableMap.of("policyName", policyName);
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.REMOVE_POLICY_FROM_TAG;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/RemovePolicyFromTagPreEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/RemovePolicyFromTagPreEvent.java
new file mode 100644
index 0000000000..38e09643cf
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/RemovePolicyFromTagPreEvent.java
@@ -0,0 +1,80 @@
+/*
+ * 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.listener.api.event;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Represents an event triggered before removing a policy from a tag. */
+@DeveloperApi
+public final class RemovePolicyFromTagPreEvent extends TagPreEvent {
+ private final String metalake;
+ private final String tagName;
+ private final String policyName;
+
+ /**
+ * Constructs an event triggered before removing a policy from a tag.
+ *
+ * @param user The user who initiated the operation.
+ * @param metalake The metalake containing the tag and policy.
+ * @param tagName The tag name.
+ * @param policyName The policy name.
+ */
+ public RemovePolicyFromTagPreEvent(
+ String user, String metalake, String tagName, String policyName) {
+ super(user, NameIdentifierUtil.ofTag(metalake, tagName));
+ this.metalake = metalake;
+ this.tagName = tagName;
+ this.policyName = policyName;
+ }
+
+ /**
+ * @return The metalake containing the tag and policy.
+ */
+ public String metalake() {
+ return metalake;
+ }
+
+ /**
+ * @return The tag name.
+ */
+ public String tagName() {
+ return tagName;
+ }
+
+ /**
+ * @return The policy name.
+ */
+ public String policyName() {
+ return policyName;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Map<String, String> customInfo() {
+ return ImmutableMap.of("policyName", policyName);
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.REMOVE_POLICY_FROM_TAG;
+ }
+}
diff --git a/core/src/test/java/org/apache/gravitino/audit/TestOperation.java
b/core/src/test/java/org/apache/gravitino/audit/TestOperation.java
index 7dca8dfa49..df785a1b72 100644
--- a/core/src/test/java/org/apache/gravitino/audit/TestOperation.java
+++ b/core/src/test/java/org/apache/gravitino/audit/TestOperation.java
@@ -30,6 +30,8 @@ import org.apache.gravitino.Namespace;
import org.apache.gravitino.SchemaChange;
import org.apache.gravitino.file.Fileset;
import org.apache.gravitino.file.FilesetChange;
+import org.apache.gravitino.listener.api.event.AddPolicyForTagEvent;
+import org.apache.gravitino.listener.api.event.AddPolicyForTagFailureEvent;
import org.apache.gravitino.listener.api.event.AlterCatalogEvent;
import org.apache.gravitino.listener.api.event.AlterCatalogFailureEvent;
import org.apache.gravitino.listener.api.event.AlterFilesetEvent;
@@ -108,6 +110,8 @@ import
org.apache.gravitino.listener.api.event.PartitionExistsEvent;
import org.apache.gravitino.listener.api.event.PurgePartitionEvent;
import org.apache.gravitino.listener.api.event.PurgePartitionFailureEvent;
import org.apache.gravitino.listener.api.event.PurgeTableEvent;
+import org.apache.gravitino.listener.api.event.RemovePolicyFromTagEvent;
+import org.apache.gravitino.listener.api.event.RemovePolicyFromTagFailureEvent;
import
org.apache.gravitino.listener.api.event.server.AuthorizationDenialFailureEvent;
import org.apache.gravitino.listener.api.event.server.HttpRequestFailureEvent;
import org.apache.gravitino.listener.api.event.view.AlterViewEvent;
@@ -130,6 +134,7 @@ import org.apache.gravitino.listener.api.info.ViewInfo;
import org.apache.gravitino.listener.api.info.partitions.IdentityPartitionInfo;
import org.apache.gravitino.listener.api.info.partitions.PartitionInfo;
import org.apache.gravitino.messaging.TopicChange;
+import org.apache.gravitino.policy.AllValuesSelector;
import org.apache.gravitino.rel.Column;
import org.apache.gravitino.rel.Representation;
import org.apache.gravitino.rel.SQLRepresentation;
@@ -607,6 +612,45 @@ public class TestOperation {
AuditLog.Operation.fromEvent(authzDenialNullIdentifier));
}
+ @Test
+ @SuppressWarnings("deprecation")
+ public void testPolicyTagOperation() {
+ Event addEvent =
+ new AddPolicyForTagEvent(USER, "metalake", "tag", "policy",
AllValuesSelector.get());
+ Event addFailureEvent =
+ new AddPolicyForTagFailureEvent(
+ USER,
+ "metalake",
+ "tag",
+ "policy",
+ AllValuesSelector.get(),
+ new Exception("add failed"));
+ Event removeEvent = new RemovePolicyFromTagEvent(USER, "metalake", "tag",
"policy");
+ Event removeFailureEvent =
+ new RemovePolicyFromTagFailureEvent(
+ USER, "metalake", "tag", "policy", new Exception("remove failed"));
+
+ Assertions.assertEquals(
+ AuditLog.Operation.ADD_POLICY_FOR_TAG,
AuditLog.Operation.fromEvent(addEvent));
+ Assertions.assertEquals(
+ AuditLog.Operation.ADD_POLICY_FOR_TAG,
AuditLog.Operation.fromEvent(addFailureEvent));
+ Assertions.assertEquals(
+ AuditLog.Operation.REMOVE_POLICY_FROM_TAG,
AuditLog.Operation.fromEvent(removeEvent));
+ Assertions.assertEquals(
+ AuditLog.Operation.REMOVE_POLICY_FROM_TAG,
+ AuditLog.Operation.fromEvent(removeFailureEvent));
+
+ // The deprecated v1 schema records the tag identifier but has no
custom-info field for policy
+ // or selector details. The v2 formatter covers those relation-specific
fields.
+ SimpleFormatter formatter = new SimpleFormatter();
+ SimpleAuditLog addLog = formatter.format(addEvent);
+ SimpleAuditLog removeLog = formatter.format(removeEvent);
+ Assertions.assertEquals(AuditLog.Operation.ADD_POLICY_FOR_TAG,
addLog.operation());
+ Assertions.assertEquals(AuditLog.Operation.REMOVE_POLICY_FROM_TAG,
removeLog.operation());
+ Assertions.assertEquals("metalake.system.tag.tag", addLog.identifier());
+ Assertions.assertEquals("metalake.system.tag.tag", removeLog.identifier());
+ }
+
/**
* {@code Operation.fromEvent} dispatches on event class, and the extras
support added a second
* constructor to each table event. Pins that events built through the new
constructor are still
diff --git
a/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
b/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
index 94840fed0f..8cb300de4f 100644
---
a/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
+++
b/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
@@ -83,6 +83,8 @@ public class TestCompatibilityUtils {
},
{OperationType.LIST_METADATA_OBJECTS_FOR_TAG,
Operation.LIST_METADATA_OBJECTS_FOR_TAG},
{OperationType.LIST_TAGS_INFO, Operation.LIST_TAGS_INFO},
+ {OperationType.ADD_POLICY_FOR_TAG, Operation.ADD_POLICY_FOR_TAG},
+ {OperationType.REMOVE_POLICY_FROM_TAG, Operation.REMOVE_POLICY_FROM_TAG},
{OperationType.ADD_PARTITION, Operation.ADD_PARTITION},
{OperationType.DROP_PARTITION, Operation.DROP_PARTITION},
{OperationType.PURGE_PARTITION, Operation.PURGE_PARTITION},
diff --git
a/core/src/test/java/org/apache/gravitino/audit/v2/TestSimpleAuditLogV2.java
b/core/src/test/java/org/apache/gravitino/audit/v2/TestSimpleAuditLogV2.java
index 7c2d89d420..6bdb3ab324 100644
--- a/core/src/test/java/org/apache/gravitino/audit/v2/TestSimpleAuditLogV2.java
+++ b/core/src/test/java/org/apache/gravitino/audit/v2/TestSimpleAuditLogV2.java
@@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableMap;
import java.util.Map;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
+import org.apache.gravitino.listener.api.event.AddPolicyForTagEvent;
import org.apache.gravitino.listener.api.event.Event;
import org.apache.gravitino.listener.api.event.EventSource;
import org.apache.gravitino.listener.api.event.GrantUserRolesFailureEvent;
@@ -33,8 +34,10 @@ import
org.apache.gravitino.listener.api.event.ListSchemaEvent;
import org.apache.gravitino.listener.api.event.ListTableEvent;
import org.apache.gravitino.listener.api.event.OperationStatus;
import org.apache.gravitino.listener.api.event.OperationType;
+import org.apache.gravitino.listener.api.event.RemovePolicyFromTagEvent;
import
org.apache.gravitino.listener.api.event.server.AuthorizationDenialFailureEvent;
import org.apache.gravitino.listener.api.event.server.HttpRequestFailureEvent;
+import org.apache.gravitino.policy.AllValuesSelector;
import org.apache.gravitino.utils.RequestContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
@@ -123,6 +126,30 @@ public class TestSimpleAuditLogV2 {
Assertions.assertTrue(output.contains("SUCCESS"));
}
+ @Test
+ public void testPolicyTagRelationEventFormatIncludesRelationTarget() {
+ AddPolicyForTagEvent addEvent =
+ new AddPolicyForTagEvent(
+ "alice", "metalake", "data_domain", "retention",
AllValuesSelector.get());
+ SimpleAuditLogV2 addLog = new SimpleAuditLogV2(addEvent);
+
+ Assertions.assertEquals("metalake.system.tag.data_domain",
addLog.identifier());
+ Assertions.assertEquals("retention",
addLog.customInfo().get("policyName"));
+ Assertions.assertEquals("{\"type\":\"ALL_VALUES\"}",
addLog.customInfo().get("selector"));
+ String addOutput = addLog.toString();
+ Assertions.assertTrue(addOutput.contains("policyName=retention"),
addOutput);
+
Assertions.assertTrue(addOutput.contains("selector={\"type\":\"ALL_VALUES\"}"),
addOutput);
+
+ RemovePolicyFromTagEvent removeEvent =
+ new RemovePolicyFromTagEvent("alice", "metalake", "data_domain",
"retention");
+ SimpleAuditLogV2 removeLog = new SimpleAuditLogV2(removeEvent);
+
+ Assertions.assertEquals("metalake.system.tag.data_domain",
removeLog.identifier());
+ Assertions.assertEquals("retention",
removeLog.customInfo().get("policyName"));
+ Assertions.assertEquals(1, removeLog.customInfo().size());
+
Assertions.assertTrue(removeLog.toString().contains("policyName=retention"));
+ }
+
// ---- list event tests ----
@Test
public void testListTableEventFormat() {
diff --git
a/core/src/test/java/org/apache/gravitino/listener/TestPolicyTagDispatcherChain.java
b/core/src/test/java/org/apache/gravitino/listener/TestPolicyTagDispatcherChain.java
new file mode 100644
index 0000000000..7957099040
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/listener/TestPolicyTagDispatcherChain.java
@@ -0,0 +1,122 @@
+/*
+ * 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.listener;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.RelationalEntity;
+import org.apache.gravitino.SupportsRelationOperations;
+import org.apache.gravitino.hook.PolicyHookDispatcher;
+import org.apache.gravitino.hook.TagHookDispatcher;
+import org.apache.gravitino.listener.api.event.AddPolicyForTagEvent;
+import org.apache.gravitino.listener.api.event.AddPolicyForTagPreEvent;
+import org.apache.gravitino.listener.api.event.RemovePolicyFromTagEvent;
+import org.apache.gravitino.listener.api.event.RemovePolicyFromTagPreEvent;
+import org.apache.gravitino.meta.PolicyEntity;
+import org.apache.gravitino.meta.TagEntity;
+import org.apache.gravitino.policy.AllValuesSelector;
+import org.apache.gravitino.policy.PolicyDispatcher;
+import org.apache.gravitino.tag.TagDispatcher;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestPolicyTagDispatcherChain {
+ private static final String METALAKE = "metalake";
+ private static final String TAG = "tag";
+ private static final String POLICY = "policy";
+
+ @Test
+ void testTagDispatcherChainDelegatesPolicyTagLookups() {
+ TagDispatcher delegate = mock(TagDispatcher.class);
+ PolicyEntity policy = mock(PolicyEntity.class);
+ when(policy.name()).thenReturn(POLICY);
+ RelationalEntity<PolicyEntity> association =
+ new RelationalEntity<>(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ NameIdentifier.of(METALAKE, TAG),
+ Entity.EntityType.TAG,
+ policy);
+ when(delegate.listPolicyAssociationsForTag(METALAKE, TAG))
+ .thenReturn(new RelationalEntity<?>[] {association});
+
+ TagDispatcher dispatcher =
+ new TagHookDispatcher(
+ new TagEventDispatcher(new EventBus(Collections.emptyList()),
delegate));
+
+ Assertions.assertArrayEquals(
+ new String[] {POLICY}, dispatcher.listPoliciesForTag(METALAKE, TAG));
+ Assertions.assertArrayEquals(
+ new RelationalEntity<?>[] {association},
+ dispatcher.listPolicyAssociationsForTag(METALAKE, TAG));
+ verify(delegate, times(2)).listPolicyAssociationsForTag(METALAKE, TAG);
+ }
+
+ @Test
+ void testTagDispatcherChainDelegatesMutationsAndEmitsEvents() {
+ TagDispatcher delegate = mock(TagDispatcher.class);
+ DummyEventListener listener = new DummyEventListener();
+ TagDispatcher dispatcher =
+ new TagHookDispatcher(
+ new TagEventDispatcher(new
EventBus(Collections.singletonList(listener)), delegate));
+
+ dispatcher.addPolicyForTag(METALAKE, TAG, POLICY, AllValuesSelector.get());
+ dispatcher.removePolicyFromTag(METALAKE, TAG, POLICY);
+
+ verify(delegate).addPolicyForTag(METALAKE, TAG, POLICY,
AllValuesSelector.get());
+ verify(delegate).removePolicyFromTag(METALAKE, TAG, POLICY);
+ verify(delegate, never()).listPolicyAssociationsForTag(METALAKE, TAG);
+ Assertions.assertInstanceOf(AddPolicyForTagPreEvent.class,
listener.getPreEvents().get(0));
+ Assertions.assertInstanceOf(RemovePolicyFromTagPreEvent.class,
listener.getPreEvents().get(1));
+ Assertions.assertInstanceOf(AddPolicyForTagEvent.class,
listener.getPostEvents().get(0));
+ Assertions.assertInstanceOf(RemovePolicyFromTagEvent.class,
listener.getPostEvents().get(1));
+ }
+
+ @Test
+ void testPolicyDispatcherChainDelegatesPolicyTagLookups() {
+ PolicyDispatcher delegate = mock(PolicyDispatcher.class);
+ TagEntity tag = mock(TagEntity.class);
+ when(tag.name()).thenReturn(TAG);
+ RelationalEntity<TagEntity> association =
+ new RelationalEntity<>(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ NameIdentifier.of(METALAKE, POLICY),
+ Entity.EntityType.POLICY,
+ tag);
+ when(delegate.listTagAssociationsForPolicy(METALAKE, POLICY))
+ .thenReturn(new RelationalEntity<?>[] {association});
+
+ PolicyDispatcher dispatcher =
+ new PolicyHookDispatcher(
+ new PolicyEventDispatcher(new EventBus(Collections.emptyList()),
delegate));
+
+ Assertions.assertArrayEquals(
+ new String[] {TAG}, dispatcher.listTagsForPolicy(METALAKE, POLICY));
+ Assertions.assertArrayEquals(
+ new RelationalEntity<?>[] {association},
+ dispatcher.listTagAssociationsForPolicy(METALAKE, POLICY));
+ verify(delegate, times(2)).listTagAssociationsForPolicy(METALAKE, POLICY);
+ }
+}
diff --git
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestPolicyTagRelationEvent.java
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestPolicyTagRelationEvent.java
new file mode 100644
index 0000000000..19381f75fc
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestPolicyTagRelationEvent.java
@@ -0,0 +1,188 @@
+/*
+ * 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.listener.api.event;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import java.util.Arrays;
+import java.util.Collections;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.gravitino.exceptions.GravitinoRuntimeException;
+import org.apache.gravitino.json.PolicyAssociationSelectorSerde;
+import org.apache.gravitino.listener.DummyEventListener;
+import org.apache.gravitino.listener.EventBus;
+import org.apache.gravitino.listener.TagEventDispatcher;
+import org.apache.gravitino.listener.api.EventListenerPlugin;
+import org.apache.gravitino.policy.PolicyAssociationSelector;
+import org.apache.gravitino.policy.TagValueSelector;
+import org.apache.gravitino.tag.TagDispatcher;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
+
+public class TestPolicyTagRelationEvent {
+ private static final String METALAKE = "metalake";
+ private static final String TAG = "data_domain";
+ private static final String POLICY = "retention";
+
+ private DummyEventListener listener;
+ private TagDispatcher delegate;
+ private TagEventDispatcher dispatcher;
+
+ @BeforeEach
+ void setUp() {
+ listener = new DummyEventListener();
+ delegate = mock(TagDispatcher.class);
+ dispatcher = new TagEventDispatcher(new EventBus(Arrays.asList(listener)),
delegate);
+ }
+
+ @Test
+ void testAddPolicyForTagEventsContainMutationIntent() {
+ PolicyAssociationSelector selector = TagValueSelector.of("finance");
+
+ dispatcher.addPolicyForTag(METALAKE, TAG, POLICY, selector);
+
+ AddPolicyForTagPreEvent preEvent = (AddPolicyForTagPreEvent)
listener.popPreEvent();
+ assertAddEventFields(preEvent.metalake(), preEvent.tagName(),
preEvent.policyName());
+ Assertions.assertEquals(OperationType.ADD_POLICY_FOR_TAG,
preEvent.operationType());
+ Assertions.assertEquals(selector, preEvent.requestedSelector());
+
+ AddPolicyForTagEvent event = (AddPolicyForTagEvent)
listener.popPostEvent();
+ assertAddEventFields(event.metalake(), event.tagName(),
event.policyName());
+ Assertions.assertEquals(OperationStatus.SUCCESS, event.operationStatus());
+ Assertions.assertEquals(selector, event.requestedSelector());
+ Assertions.assertEquals(POLICY, event.customInfo().get("policyName"));
+ Assertions.assertEquals(
+ PolicyAssociationSelectorSerde.serialize(selector),
event.customInfo().get("selector"));
+ verify(delegate).addPolicyForTag(METALAKE, TAG, POLICY, selector);
+ verify(delegate, never()).listPolicyAssociationsForTag(METALAKE, TAG);
+ }
+
+ @Test
+ void testAddPolicyForTagFailureEvent() {
+ PolicyAssociationSelector selector = TagValueSelector.of("finance");
+ GravitinoRuntimeException exception = new GravitinoRuntimeException("add
failed");
+ doThrow(exception).when(delegate).addPolicyForTag(METALAKE, TAG, POLICY,
selector);
+
+ Assertions.assertSame(
+ exception,
+ Assertions.assertThrowsExactly(
+ GravitinoRuntimeException.class,
+ () -> dispatcher.addPolicyForTag(METALAKE, TAG, POLICY,
selector)));
+
+ AddPolicyForTagPreEvent preEvent = (AddPolicyForTagPreEvent)
listener.popPreEvent();
+ Assertions.assertEquals(selector, preEvent.requestedSelector());
+ AddPolicyForTagFailureEvent event = (AddPolicyForTagFailureEvent)
listener.popPostEvent();
+ Assertions.assertSame(exception, event.exception());
+ Assertions.assertEquals(OperationStatus.FAILURE, event.operationStatus());
+ Assertions.assertEquals(selector, event.requestedSelector());
+ Assertions.assertEquals(POLICY, event.customInfo().get("policyName"));
+ verify(delegate, never()).listPolicyAssociationsForTag(METALAKE, TAG);
+ }
+
+ @Test
+ void testRemovePolicyFromTagEventsContainMutationIntent() {
+ dispatcher.removePolicyFromTag(METALAKE, TAG, POLICY);
+
+ RemovePolicyFromTagPreEvent preEvent = (RemovePolicyFromTagPreEvent)
listener.popPreEvent();
+ assertRemoveEventFields(preEvent.metalake(), preEvent.tagName(),
preEvent.policyName());
+ Assertions.assertEquals(OperationType.REMOVE_POLICY_FROM_TAG,
preEvent.operationType());
+
+ RemovePolicyFromTagEvent event = (RemovePolicyFromTagEvent)
listener.popPostEvent();
+ assertRemoveEventFields(event.metalake(), event.tagName(),
event.policyName());
+ Assertions.assertEquals(OperationStatus.SUCCESS, event.operationStatus());
+ Assertions.assertEquals(POLICY, event.customInfo().get("policyName"));
+ verify(delegate).removePolicyFromTag(METALAKE, TAG, POLICY);
+ verify(delegate, never()).listPolicyAssociationsForTag(METALAKE, TAG);
+ }
+
+ @Test
+ void testRemovePolicyFromTagFailureEvent() {
+ GravitinoRuntimeException exception = new
GravitinoRuntimeException("remove failed");
+ doThrow(exception).when(delegate).removePolicyFromTag(METALAKE, TAG,
POLICY);
+
+ Assertions.assertSame(
+ exception,
+ Assertions.assertThrowsExactly(
+ GravitinoRuntimeException.class,
+ () -> dispatcher.removePolicyFromTag(METALAKE, TAG, POLICY)));
+
+ RemovePolicyFromTagFailureEvent event =
+ (RemovePolicyFromTagFailureEvent) listener.popPostEvent();
+ Assertions.assertSame(exception, event.exception());
+ Assertions.assertEquals(OperationStatus.FAILURE, event.operationStatus());
+ Assertions.assertEquals(POLICY, event.policyName());
+ verify(delegate, never()).listPolicyAssociationsForTag(METALAKE, TAG);
+ }
+
+ @Test
+ void testPreEventIsDispatchedBeforeMutation() {
+ EventListenerPlugin orderedListener = mock(EventListenerPlugin.class);
+ TagEventDispatcher orderedDispatcher =
+ new TagEventDispatcher(new
EventBus(Collections.singletonList(orderedListener)), delegate);
+ PolicyAssociationSelector selector = TagValueSelector.of("finance");
+
+ orderedDispatcher.addPolicyForTag(METALAKE, TAG, POLICY, selector);
+
+ InOrder ordered = inOrder(orderedListener, delegate);
+
ordered.verify(orderedListener).onPreEvent(any(AddPolicyForTagPreEvent.class));
+ ordered.verify(delegate).addPolicyForTag(METALAKE, TAG, POLICY, selector);
+
ordered.verify(orderedListener).onPostEvent(any(AddPolicyForTagEvent.class));
+ }
+
+ @Test
+ void testPreEventVetoPreventsBusinessAccess() {
+ EventListenerPlugin vetoListener = mock(EventListenerPlugin.class);
+ ForbiddenException forbidden = new ForbiddenException("denied");
+
doThrow(forbidden).when(vetoListener).onPreEvent(any(AddPolicyForTagPreEvent.class));
+ TagEventDispatcher vetoDispatcher =
+ new TagEventDispatcher(new
EventBus(Collections.singletonList(vetoListener)), delegate);
+
+ Assertions.assertSame(
+ forbidden,
+ Assertions.assertThrowsExactly(
+ ForbiddenException.class,
+ () ->
+ vetoDispatcher.addPolicyForTag(
+ METALAKE, TAG, POLICY, TagValueSelector.of("finance"))));
+ verify(delegate, never())
+ .addPolicyForTag(
+ any(String.class),
+ any(String.class),
+ any(String.class),
+ any(PolicyAssociationSelector.class));
+ verify(delegate, never()).listPolicyAssociationsForTag(any(String.class),
any(String.class));
+ }
+
+ private static void assertAddEventFields(String metalake, String tagName,
String policyName) {
+ Assertions.assertEquals(METALAKE, metalake);
+ Assertions.assertEquals(TAG, tagName);
+ Assertions.assertEquals(POLICY, policyName);
+ }
+
+ private static void assertRemoveEventFields(String metalake, String tagName,
String policyName) {
+ assertAddEventFields(metalake, tagName, policyName);
+ }
+}
diff --git a/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
b/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
index 41d7d65b07..c3f3a8b20a 100644
--- a/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
+++ b/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
@@ -1134,6 +1134,9 @@ public class TestTagManager {
PolicyAssociationSelectorSerde.deserialize(
associations[0].relationValue().orElseThrow()));
+ Assertions.assertThrows(
+ PolicyAlreadyAssociatedException.class,
+ () -> tagManager.addPolicyForTag(METALAKE, tagName, policyName,
AllValuesSelector.get()));
Assertions.assertThrows(
PolicyAlreadyAssociatedException.class,
() ->
diff --git a/design-docs/policy-on-tag.md b/design-docs/policy-on-tag.md
index f3f5515ca7..475f96ba28 100644
--- a/design-docs/policy-on-tag.md
+++ b/design-docs/policy-on-tag.md
@@ -299,7 +299,8 @@ policy_tag_relation_meta
Constraints and indexes:
1. Active rows are unique by `(policy_id, tag_id)`. A policy can be associated
with a tag at most
- once, and updating `selector` updates that relation.
+ once. Adding an existing pair conflicts regardless of its selector. To
change the selector,
+ clients must remove the existing relation and then create a new one.
2. Index `tag_id` for object policy lookup from tags.
3. Index `policy_id` for impact analysis from policies.
4. Policy and tag must belong to the same metalake.
@@ -455,7 +456,7 @@ policy-to-tag relation. The caller must have `VIEW_TAG` on
the requested tag. Re
filtered by `VIEW_POLICY`; `APPLY_TAG` and `APPLY_POLICY` imply their
corresponding view privileges.
Returns `404 Not Found` if the tag does not exist.
-#### New: `PUT /api/metalakes/{metalake}/tags/{tag}/policies/{policy}`
+#### New: `POST /api/metalakes/{metalake}/tags/{tag}/policies/{policy}`
**Request:**
@@ -472,7 +473,7 @@ Returns `404 Not Found` if the tag does not exist.
}
```
-**Response:** `200 OK`
+**Response:** `201 Created`
```json
{
@@ -485,9 +486,9 @@ Returns `404 Not Found` if the tag does not exist.
}
```
-**Behavior:** Creates one policy-to-tag relation or replaces the selector on
the existing relation.
-The policy and tag must exist in the same metalake, and the selector must be
valid for the tag.
-Repeating the same request is an idempotent no-op.
+**Behavior:** Creates one policy-to-tag relation. The policy and tag must
exist in the same
+metalake, and the selector must be valid for the tag. If the relation already
exists, the request
+returns `409 Conflict` regardless of whether the existing selector matches the
requested selector.
#### New: `DELETE /api/metalakes/{metalake}/tags/{tag}/policies/{policy}`
@@ -618,7 +619,7 @@ calls must be replaced with tag assignment calls.
|------|---------|---------|
| Object policy association | `SupportsPolicies.associatePolicies(String[]
add, String[] remove)` | Removed from metadata object mixins in the target
model |
| Object policy listing | `SupportsPolicies.listPolicies()` | Reinterpreted as
read-only derived object policy lookup |
-| Tag policy association | None | New single-relation APIs such as
setPolicyForTag(tagName, policyName, selector) and removePolicyFromTag(tagName,
policyName) |
+| Tag policy association | None | New single-relation APIs such as
addPolicyForTag(tagName, policyName, selector) and removePolicyFromTag(tagName,
policyName) |
| Policy impact analysis | `Policy.associatedObjects()` | Replaced or
supplemented by Policy.associatedTags() |
The exact Java and Python method names can be finalized during implementation,
but the API shape
@@ -705,12 +706,17 @@ assignment changes.
### Event Listener
-Policy-to-tag relation creation, selector update, and deletion must be exposed
through the event
-listener framework, matching the existing policy and tag lifecycle event
pattern. Each operation
-has pre-event, success-event, and failure-event types. Event payloads include
the metalake, tag
-name, policy name, previous and requested selector as applicable, actor, and
request context. A
-successful PUT event includes the resulting relation, and a successful DELETE
event includes the
-removed relation.
+Policy-to-tag relation creation and deletion must be exposed through the event
listener framework,
+matching the existing policy and tag lifecycle event pattern. Each operation
has pre-event,
+success-event, and failure-event types. Pre-events describe only the mutation
intent and run before
+business access. Add events include the metalake, tag name, policy name,
requested selector, actor,
+and request context. Remove events include the metalake, tag name, policy
name, actor, and request
+context; because removal is idempotent, a success event does not distinguish a
deleted association
+from a missing-association no-op.
+
+The v2 audit payload includes `policyName` for both operations and the
requested `selector` for
+add. The deprecated v1 audit format has no custom-information field and
therefore identifies only
+the tag resource.
Object policy lookup is a read-only derived operation and does not create
policy-relation events. It
may still be covered by normal REST access logs or audit logs if the project
records read events.
@@ -865,7 +871,7 @@ objectTagMappings:
resolution, and plan its removal according to the API evolution policy.
- [ ] Implement `ObjectPolicyResolver` to resolve object policies from
effective tags and value
selectors.
-- [ ] Add single-relation PUT and DELETE endpoints for tag policy association,
plus policy tag
+- [ ] Add single-relation POST and DELETE endpoints for tag policy
association, plus policy tag
listing with selector payloads.
- [ ] Change object policy REST APIs to read-only derived lookup and remove
direct object policy
mutation behavior.