[#12176] feat(core): add policy-on-tag core support - #12718
Conversation
e2a5489 to
d753bce
Compare
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
This PR introduces policy-on-tag behavior by resolving enabled policies from effective tag assignments on metadata objects, while keeping direct object-policy associations and deduplicating by policy ID.
Changes:
- Add effective tag resolution and tag-derived policy resolution (ALL_VALUES / TAG_VALUE selectors).
- Add TagManager operations for listing/adding/removing policy-to-tag associations.
- Move selector JSON serde into
commonfor reuse across modules and add targeted tests.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| core/src/main/java/org/apache/gravitino/tag/TagManager.java | Adds core APIs to manage policy↔tag associations via relation operations. |
| core/src/main/java/org/apache/gravitino/tag/TagDispatcher.java | Exposes new dispatcher defaults for policy associations on tags. |
| core/src/main/java/org/apache/gravitino/tag/EffectiveTagResolver.java | Computes effective tags for an object with nearest-assignment override semantics. |
| core/src/main/java/org/apache/gravitino/policy/ObjectPolicyResolver.java | Resolves enabled policies for an object from effective tags + selectors. |
| core/src/main/java/org/apache/gravitino/policy/PolicyManager.java | Merges direct and tag-derived policies; adds API to list tag associations for a policy. |
| core/src/main/java/org/apache/gravitino/policy/PolicyDispatcher.java | Exposes new dispatcher defaults for tag associations on policies. |
| common/src/main/java/org/apache/gravitino/json/PolicyAssociationSelectorSerde.java | Centralizes selector JSON serialization/deserialization. |
| core/src/test/java/org/apache/gravitino/tag/TestTagManager.java | Adds tests for policy↔tag association APIs. |
| core/src/test/java/org/apache/gravitino/tag/TestEffectiveTagResolver.java | Adds unit tests for effective tag resolution ordering/override behavior. |
| core/src/test/java/org/apache/gravitino/policy/TestObjectPolicyResolver.java | Adds unit tests for selector evaluation, conflicts, and filtering. |
| core/src/test/java/org/apache/gravitino/policy/TestPolicyManager.java | Updates integration tests to include tag-derived policies in results. |
| common/src/test/java/org/apache/gravitino/json/TestPolicyAssociationSelectorSerde.java | Adds serde round-trip and rejection tests. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| import java.util.Map; | ||
| import javax.annotation.Nullable; | ||
| 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.policy.PolicyAssociationSelector; |
There was a problem hiding this comment.
Thanks. java.util.Arrays is already imported at line 21 in the current head, and the build passes, so no change is needed here.
| default String[] listPoliciesForTag(String metalake, String name) { | ||
| return Arrays.stream(listPolicyAssociationsForTag(metalake, name)) | ||
| .map(association -> association.targetEntity().name()) | ||
| .toArray(String[]::new); | ||
| } |
There was a problem hiding this comment.
Thanks. This duplicates the other import comment. java.util.Arrays is already imported in the current head, so no change is needed here.
| public RelationalEntity<?>[] listTagAssociationsForPolicy(String metalake, String policyName) { | ||
| NameIdentifier policyIdentifier = NameIdentifierUtil.ofPolicy(metalake, policyName); | ||
| checkMetalake(NameIdentifier.of(metalake), entityStore); | ||
|
|
||
| return TreeLockUtils.doWithTreeLock( | ||
| entityIdent, | ||
| policyIdentifier, | ||
| LockType.READ, | ||
| () -> { | ||
| getPolicyWithoutLock(metalake, policyName); | ||
| try { | ||
| return entityStore | ||
| .relationOperations() | ||
| .listEntitiesByRelation( | ||
| SupportsRelationOperations.Type.POLICY_METADATA_OBJECT_REL, | ||
| entityIdent, | ||
| entityType, | ||
| true /* allFields */) | ||
| .stream() | ||
| .map(entity -> (PolicyEntity) entity) | ||
| .toArray(PolicyEntity[]::new); | ||
| } catch (NoSuchEntityException e) { | ||
| throw new NoSuchMetadataObjectException( | ||
| e, | ||
| "Failed to list policies for metadata object %s due to not found", | ||
| metadataObject); | ||
| .batchListEntitiesByRelation( | ||
| SupportsRelationOperations.Type.POLICY_TAG_REL, | ||
| Collections.singletonList(policyIdentifier), | ||
| Entity.EntityType.POLICY) | ||
| .toArray(new RelationalEntity<?>[0]); |
There was a problem hiding this comment.
Thanks. batchListEntitiesByRelation supports either endpoint for POLICY_TAG_REL. With a POLICY anchor, PolicyTagRelService.listRelations calls listByPolicyNames and then tagTargets, so the returned target entities are tags. The current implementation is correct and no change is needed here.
| PolicyEntity policy = (PolicyEntity) relation.targetEntity(); | ||
| PolicyAssociationSelector selector = | ||
| PolicyAssociationSelectorSerde.deserialize(relation.relationValue().orElseThrow()); | ||
| TagAssignment assignment = tag.assignment().orElseGet(TagAssignment::noValue); | ||
| boolean matches = matches(selector, assignment); |
There was a problem hiding this comment.
Fixed in e533d2d. A missing relation payload is now treated as AllValuesSelector, preserving the tag-presence compatibility behavior, and a regression test was added.
| String type = node.get(TYPE).asText(); | ||
| if (AllValuesSelector.TYPE.equals(type)) { | ||
| return AllValuesSelector.get(); | ||
| } | ||
| if (TagValueSelector.TYPE.equals(type)) { | ||
| return TagValueSelector.of(node.get(VALUE).asText()); | ||
| } | ||
| throw JsonMappingException.from( | ||
| parser, "Unsupported policy association selector type: " + type); | ||
| } |
There was a problem hiding this comment.
Fixed in e533d2d. The deserializer now validates the required textual type and value fields and reports a clear JsonMappingException through the public deserialize helper. Tests cover missing and null fields.
Code Coverage Report
Files
|
mchades
left a comment
There was a problem hiding this comment.
Non-duplicate findings from a current-head review.
| Map<Long, PolicyEntity> policiesById = new LinkedHashMap<>(); | ||
| Arrays.stream(listDirectPoliciesForMetadataObject(entityIdent, entityType, metadataObject)) | ||
| .forEach(policy -> policiesById.putIfAbsent(policy.id(), policy)); | ||
| Arrays.stream(objectPolicyResolver.resolve(metalake, metadataObject)) |
There was a problem hiding this comment.
ObjectPolicyResolver.resolve already walks the requested object and all parents through EffectiveTagResolver, while MetadataObjectPolicyOperations still calls this method once for the object and again for each parent. A child override such as data_domain=risk over a parent data_domain=finance is therefore evaluated correctly here, but the later parent call adds a TAG_VALUE("finance") policy back. Policies selected from inherited tags are also returned in the first call and marked inherited=false by the REST layer. Please resolve tag-derived policies exactly once for the requested object, keep any direct-policy compatibility traversal separate, and add an end-to-end override regression test.
There was a problem hiding this comment.
Thanks. We agree this must be addressed together with the planned object-policy semantic change in the follow-up REST PR. That change will separate direct-policy compatibility traversal from effective tag-derived resolution and define inherited flags there. We are intentionally not changing the existing REST traversal in this core runtime PR.
There was a problem hiding this comment.
This PR already changes the existing user-facing list/get behavior by invoking ObjectPolicyResolver from PolicyManager. Those requests currently flow through MetadataObjectPolicyOperations, so merging this PR before the follow-up exposes the duplicate parent traversal immediately. Please either update the REST traversal and add override/inherited regression coverage here, or defer the PolicyManager resolver integration to the follow-up PR.
There was a problem hiding this comment.
Fixed in 9123233. The ObjectPolicyResolver integration is deferred to the follow-up REST PR. PolicyManager now returns direct policies only, preserving the existing REST parent traversal and avoiding duplicate effective-tag resolution and incorrect inherited flags. The resolver and its unit coverage remain in this PR.
| * @param name The name of the tag. | ||
| * @return The policy-to-tag associations. | ||
| */ | ||
| default RelationalEntity<?>[] listPolicyAssociationsForTag(String metalake, String name) { |
There was a problem hiding this comment.
GravitinoEnv exposes TagHookDispatcher(TagEventDispatcher(TagManager)), but neither wrapper overrides these new default methods, so calls through the production dispatcher stop at this UnsupportedOperationException instead of reaching TagManager. PolicyHookDispatcher/PolicyEventDispatcher have the same gap for listTagAssociationsForPolicy. Please delegate the new operations through both wrapper layers and test the composed runtime chain.
There was a problem hiding this comment.
Thanks. The REST entry points and the corresponding Tag/Policy Event and Hook dispatcher delegation will be added together in the follow-up REST integration PR. This core runtime PR intentionally does not expose these operations through the composed production chain yet.
| RelationEdgeTarget.of( | ||
| policyIdentifier, | ||
| Entity.EntityType.POLICY, | ||
| PolicyAssociationSelectorSerde.serialize(selector)) |
There was a problem hiding this comment.
This method persists the selector without validating it against the tag's TagValueConstraint. It accepts TAG_VALUE("engineering") for an ALLOWED_VALUES("finance", "risk") tag, or any TAG_VALUE for a NO_VALUE tag, creating an association that can never match. Please load the tag under the lock, validate the selector against its constraint, and add negative tests for unsupported values and constraint types.
There was a problem hiding this comment.
Thanks. Policy-tag association create, update, and delete operations enter through REST, so selector validation against TagValueConstraint will be implemented in the follow-up REST PR. TagValueConstraint is immutable after tag creation, and this keeps semantic request validation in the REST layer instead of duplicating it in core.
| try { | ||
| return entityStore | ||
| .relationOperations() | ||
| .batchListEntitiesByRelation( |
There was a problem hiding this comment.
batchListEntitiesByRelation returns an empty list when no matching relation exists and does not prove that the anchor tag exists. Because this method never loads the tag, a nonexistent tag is indistinguishable from an existing tag with no policies, so the planned GET endpoint cannot honor its 404 contract. Please check the tag under the read lock before querying and add a missing-tag test.
There was a problem hiding this comment.
Fixed in e533d2d. listPolicyAssociationsForTag now loads the anchor tag under the read lock before querying relations, and the regression test verifies that a missing tag raises NoSuchTagException.
mchades
left a comment
There was a problem hiding this comment.
Please update the PR title and description before merging, as they no longer match the current implementation.
Updated. |
What changes were proposed in this pull request?
ALL_VALUESandTAG_VALUEpolicy association selectors.Why are the changes needed?
These changes provide the Core runtime foundation for the policy-on-tag governance model while preserving the behavior of existing metadata-object policy APIs until the related public APIs and integration are ready.
Fix: #12176
Does this PR introduce any user-facing change?
No. The new capabilities are internal Core operations and resolvers. Existing metadata-object policy APIs continue to return directly associated policies only.
How was this patch tested?
./gradlew :common:test --tests org.apache.gravitino.json.TestPolicyAssociationSelectorSerde./gradlew :core:test --tests org.apache.gravitino.policy.TestObjectPolicyResolver --tests org.apache.gravitino.policy.TestPolicyManager --tests org.apache.gravitino.tag.TestEffectiveTagResolver --tests org.apache.gravitino.tag.TestTagManager