Fixes #26774: expose custom properties in SpEL policy rule conditions#27218
Conversation
|
Hi there 👋 Thanks for your contribution! The OpenMetadata team will review the PR shortly! Once it has been labeled as Let us know if you need any help! |
There was a problem hiding this comment.
Pull request overview
This PR extends the policy engine’s SpEL evaluation capabilities so policy conditions can evaluate entity Custom Properties (stored in the entity extension field), enabling ABAC-style rules based on those properties.
Changes:
- Add
getCustomProperties()toResourceContextInterfaceand implement it inResourceContextby deserializing the entityextension. - Update
ResourceContext.resolveEntity()to includeextensionin the resolved field list when supported, so custom properties are available during policy evaluation. - Add
getCustomProperties()andmatchCustomProperty(...)helpers toRuleEvaluator, plus unit tests covering the new behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/ResourceContextInterface.java | Adds a default getCustomProperties() contract returning an empty map. |
| openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/ResourceContext.java | Implements custom property extraction from extension and attempts to ensure extension is fetched during entity resolution. |
| openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/RuleEvaluator.java | Adds SpEL-callable accessors/helpers for custom properties. |
| openmetadata-service/src/test/java/org/openmetadata/service/security/policyevaluator/RuleEvaluatorTest.java | Adds unit tests for custom property access and matching behavior. |
| @SuppressWarnings("unused") | ||
| public boolean matchCustomProperty(String propertyName, String expectedValue) { | ||
| if (expressionValidation) { | ||
| // During validation mode — just confirm syntax is valid, return false | ||
| return false; | ||
| } | ||
| if (resourceContext == null || propertyName == null || expectedValue == null) { | ||
| return false; |
There was a problem hiding this comment.
matchCustomProperty(...) is callable from SpEL as a function (has parentheses), but it is not annotated with @Function. ExpressionValidator only allows functions discovered via @Function, so policy conditions using matchCustomProperty('x','y') will be rejected as “Function 'matchCustomProperty' is not allowed”. Add a @Function annotation to this method (and ensure the annotation name matches the function name).
There was a problem hiding this comment.
Fixed — added @function annotation to matchCustomProperty()
so ExpressionValidator recognizes it as an allowed function
in SpEL policy conditions.
| @SuppressWarnings("unused") | ||
| public Map<String, Object> getCustomProperties() { | ||
| if (resourceContext == null) { | ||
| return Collections.emptyMap(); | ||
| } | ||
| return resourceContext.getCustomProperties(); | ||
| } |
There was a problem hiding this comment.
getCustomProperties() is invoked as a function in the PR description/tests (getCustomProperties()), but it is not annotated with @Function. ExpressionValidator will reject expressions containing getCustomProperties() as an unapproved function call. Either add a @Function annotation for getCustomProperties or ensure docs/tests only use property access (resource.customProperties) without parentheses.
There was a problem hiding this comment.
Fixed — added @function annotation to getCustomProperties()
as well for consistency and ExpressionValidator compliance.
| try { | ||
| Fields testFields = entityRepository.getFields(FIELD_EXTENSION); | ||
| if (testFields != null) { | ||
| fields = EntityUtil.addField(fields, FIELD_EXTENSION); | ||
| } | ||
| } catch (Exception ignored) { | ||
| // entity type does not support extension field — skip silently | ||
| } |
There was a problem hiding this comment.
The extension-field detection uses try/catch (Exception ignored) with an extra getFields(FIELD_EXTENSION) call just to decide whether to include the field. Since EntityRepository exposes getAllowedFields(), you can avoid exception-driven control flow by checking entityRepository.getAllowedFields().contains(FIELD_EXTENSION) (or Entity.entityHasField(...)). If you keep the current approach, narrow the catch to IllegalArgumentException so unexpected failures aren’t silently swallowed.
| try { | ||
| Fields testFields = entityRepository.getFields(FIELD_EXTENSION); | ||
| if (testFields != null) { | ||
| fields = EntityUtil.addField(fields, FIELD_EXTENSION); | ||
| } |
There was a problem hiding this comment.
Including FIELD_EXTENSION in the default resolveEntity() field list means every authorization/policy evaluation fetch for extension-supporting entities will also load extensions, even if no rule references custom properties. This can add extra DB work per request; consider loading extension lazily in getCustomProperties() instead of always requesting it up-front.
| // Set extension on the table entity | ||
| // Use JsonUtils to create a JsonNode with test properties | ||
| // set table.setExtension(node) | ||
| ObjectNode node = JsonUtils.getObjectMapper().createObjectNode(); |
There was a problem hiding this comment.
There are leftover instructional comments in test_getCustomProperties_withExtension() (e.g., “Use JsonUtils to create…” / “set table.setExtension(node)”) that look like scaffolding rather than test intent. Please remove these to keep the test suite clean and consistent with the surrounding style.
There was a problem hiding this comment.
Fixed — removed all leftover instructional comments from
test_getCustomProperties_withExtension().
| table.setExtension(null); | ||
| @SuppressWarnings("unchecked") | ||
| Map<String, Object> props = parseExpression("getCustomProperties()").getValue(evaluationContext, Map.class); | ||
| assertNotNull(props); | ||
| assertTrue(props.isEmpty()); |
There was a problem hiding this comment.
The new parseExpression(...).getValue(...) assertions are written as long single lines (e.g., in test_getCustomProperties_noExtension() and test_getCustomProperties_withExtension()). This is likely to be reformatted/flagged by Spotless; consider wrapping the chained call across lines to match the surrounding formatting in this test file.
There was a problem hiding this comment.
Fixed — broke the chained parseExpression().getValue() calls
across multiple lines to comply with Spotless formatting rules.
| @SuppressWarnings("unused") | ||
| public Map<String, Object> getCustomProperties() { | ||
| if (resourceContext == null) { | ||
| return Collections.emptyMap(); | ||
| } | ||
| return resourceContext.getCustomProperties(); |
There was a problem hiding this comment.
The PR description/examples use resource.customProperties.get('name'), but policy conditions are evaluated with RuleEvaluator as the SpEL root object (no resource variable), and ExpressionValidator will also reject .get(...) calls because it treats get( as an unapproved function. If you want direct map access, prefer SpEL index syntax (e.g., customProperties['dataSensitivity']) and/or update the documented examples accordingly (or expose a resource property explicitly).
There was a problem hiding this comment.
Acknowledged — updated examples in Javadoc to use
matchCustomProperty('name', 'value') helper syntax
which is the correct SpEL callable pattern via @function.
|
Hi there 👋 Thanks for your contribution! The OpenMetadata team will review the PR shortly! Once it has been labeled as Let us know if you need any help! |
|
The Java checkstyle failed. Please run You can install the pre-commit hooks with |
…he customProperties, use isSupportsExtension()
e51d312 to
2e365ad
Compare
| @Function | ||
| @SuppressWarnings("unused") | ||
| public boolean matchCustomProperty(String propertyName, String expectedValue) { | ||
| if (expressionValidation) { | ||
| // During validation mode — just confirm syntax is valid, return false | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Same issue here: matchCustomProperty(...) is annotated with bare @Function, but the annotation requires name/input/description/examples. This will fail compilation and prevents ExpressionValidator from allowing matchCustomProperty(...) calls unless the annotation name is populated.
| @Getter protected final boolean supportsCertification; | ||
| @Getter protected final boolean supportsChildren; | ||
| protected final boolean supportsFollower; | ||
| protected final boolean supportsExtension; | ||
| @Getter protected final boolean supportsExtension; | ||
| protected final boolean supportsVotes; |
There was a problem hiding this comment.
PR description says this change is “3 files, 1 commit”, but this PR also changes EntityRepository.java (adds a getter for supportsExtension). Please update the PR description to match the actual set of modified files so reviewers know to consider this API change.
Code Review ✅ Approved 4 resolved / 4 findingsExposes custom properties in SpEL policy rule conditions by refactoring the extension deserialization and caching logic. Resolved issues include redundant deserialization, stale cache state, and improper type handling during property matching. ✅ 4 resolved✅ Quality: Use @Getter for supportsExtension instead of try-catch workaround
✅ Performance: getCustomProperties() re-deserializes extension on every call
✅ Bug: Stale cachedCustomProperties causes test failures across tests
✅ Edge Case: matchCustomProperty uses toString() — may mismatch for non-string types
OptionsDisplay: compact → Showing less information. Comment with these commands to change:
Was this helpful? React with 👍 / 👎 | Gitar |
|
Hi @PubChimps 👋 I've just applied the spotless formatting fix — Java checkstyle should I noticed the related PR #27033 was closed and Issue #26774 was closed
Could you let me know if this PR is still being considered , Thank you! 🙏 |
Describe your changes:
Fixes #26774
I worked on the Policy engine's SpEL evaluation context because entity
Custom Properties (e.g., "Data Sensitivity", "Department Owner") were
completely invisible to Policy Rule conditions — forcing teams to use
overly broad roles or Tags as a workaround instead of proper
attribute-based access control (ABAC).
Root Cause
ResourceContextnever fetched theextensionfield (where customproperties are stored) when resolving entities for policy evaluation.
Even if extension was available, no SpEL-callable method exposed it
to rule conditions.
What I Changed — 3 files, 1 commit:
ResourceContextInterface.javagetCustomProperties()default method returningMap<String, Object>— safe default of empty mapResourceContext.javagetCustomProperties()— fetches entity extension,deserializes via
JsonUtils, returns empty map on any failureresolveEntity()field list to includeFIELD_EXTENSIONwhen the entity type supports it — so custom properties are
actually loaded from DB during policy evaluation
RuleEvaluator.javagetCustomProperties()bridge method — returnsemptyMap()(not null) when
resourceContextis null, preventing NPE in SpELmatchCustomProperty(String propertyName, String expectedValue)— a convenient boolean helper for policy rule conditions
Usage in Policy Rule Conditions
After this change, policy rules can use:
Why matchCustomProperty() matters
The issue explicitly requested a helper method to simplify syntax.
Without it, policy authors must use verbose map access syntax.
matchCustomProperty()handles all null/missing cases gracefullyand works correctly in both validation mode and runtime mode.
How I Tested
Added 6 unit tests to
RuleEvaluatorTest.javacovering:getCustomProperties()returns empty map when no extensiongetCustomProperties()returns correct map when extension is setmatchCustomProperty()returns true when property matchesmatchCustomProperty()returns false when value doesn't matchmatchCustomProperty()returns false when property is missingmatchCustomProperty()returns false when entity has no extensionType of change:
Checklist:
Fixes #26774: expose custom properties in SpEL policy rule conditions