Skip to content

feat(policy): expose custom properties in SpEL policy rule conditions (#26774) #27033

Closed
SaaiAravindhRaja wants to merge 2 commits intoopen-metadata:mainfrom
SaaiAravindhRaja:feat/issue-26774-custom-props-policy-rules
Closed

feat(policy): expose custom properties in SpEL policy rule conditions (#26774) #27033
SaaiAravindhRaja wants to merge 2 commits intoopen-metadata:mainfrom
SaaiAravindhRaja:feat/issue-26774-custom-props-policy-rules

Conversation

@SaaiAravindhRaja
Copy link
Copy Markdown
Contributor

Fixes #26774

Exposes entity custom properties in the SpEL evaluation context so Policy Rule conditions can reference them (e.g. resource.customProperties.get('sensitivity') == 'PII').

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 4, 2026

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 4, 2026

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

@SaaiAravindhRaja SaaiAravindhRaja changed the title feat(policy): expose custom properties in SpEL policy rule conditions (#26774) feat(policy): expose custom properties in SpEL policy rule conditions (#26774) [WIP] Apr 4, 2026
@SaaiAravindhRaja SaaiAravindhRaja marked this pull request as ready for review April 4, 2026 11:16
Copilot AI review requested due to automatic review settings April 4, 2026 11:16
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to expose entity custom properties (stored in extension) to the SpEL evaluation context so Policy Rule conditions can reference them via resource.customProperties.

Changes:

  • Added getCustomProperties() default method to ResourceContextInterface.
  • Implemented getCustomProperties() in ResourceContext and attempted to ensure extension is fetched when resolving entities.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/ResourceContextInterface.java Adds a default getCustomProperties() API for SpEL access.
openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/ResourceContext.java Implements custom properties retrieval and requests extension during entity resolution.

if (entityRepository.isSupportsReviewers()) {
fields = EntityUtil.addField(fields, Entity.FIELD_REVIEWERS);
}
fields = EntityUtil.addField(fields, FIELD_EXTENSION);
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveEntity() always adds extension to the requested fields (EntityUtil.addField(fields, FIELD_EXTENSION)). For entity types that don't support extensions (e.g., User schema has no extension field), entityRepository.getFields(fields) will throw invalidField, which can break authorization checks for those resources. Guard this addition with a support check (e.g., entityRepository.getAllowedFields().contains(FIELD_EXTENSION)), or only request extension lazily when getCustomProperties() is actually invoked.

Suggested change
fields = EntityUtil.addField(fields, FIELD_EXTENSION);
if (entityRepository.getAllowedFields().contains(FIELD_EXTENSION)) {
fields = EntityUtil.addField(fields, FIELD_EXTENSION);
}

Copilot uses AI. Check for mistakes.
Comment on lines +198 to +202
Object extension = entity.getExtension();
if (extension instanceof Map) {
return (Map<String, Object>) extension;
}
return Collections.emptyMap();
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entity.getExtension() is typically returned as a Jackson ObjectNode (see EntityRepository#getExtension), not a Map, so the extension instanceof Map check will almost always be false and this will return an empty map even when custom properties exist. Use JsonUtils.getMap(entity.getExtension()) (or handle JsonNode explicitly) and consider returning an unmodifiable map to prevent SpEL expressions from mutating the backing object.

Copilot uses AI. Check for mistakes.
Comment on lines +191 to +199
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> getCustomProperties() {
resolveEntity();
if (entity == null) {
return Collections.emptyMap();
}
Object extension = entity.getExtension();
if (extension instanceof Map) {
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No tests currently cover referencing custom properties via SpEL (e.g., resource.customProperties.get('sensitivity') == 'PII'). Add a policy evaluator/rule evaluator unit test that sets an entity extension (custom properties) and asserts the expression evaluates correctly (including the empty-map case when no extension is present).

Copilot uses AI. Check for mistakes.
SaaiAravindhRaja added a commit to SaaiAravindhRaja/OpenMetadata that referenced this pull request Apr 4, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 4, 2026

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

@SaaiAravindhRaja SaaiAravindhRaja changed the title feat(policy): expose custom properties in SpEL policy rule conditions (#26774) [WIP] feat(policy): expose custom properties in SpEL policy rule conditions (#26774) Apr 5, 2026
Copilot AI review requested due to automatic review settings April 12, 2026 12:51
@SaaiAravindhRaja SaaiAravindhRaja force-pushed the feat/issue-26774-custom-props-policy-rules branch from a5edaac to b8cca80 Compare April 12, 2026 12:51
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

Comment on lines +1093 to +1100
parseExpression("customProperties.get('sensitivity') == 'PII'")
.getValue(context, Boolean.class);
assertTrue(result != null && result);

RuleEvaluator validationEvaluator = new RuleEvaluator(false);
StandardEvaluationContext validationContext = new StandardEvaluationContext(validationEvaluator);
Boolean validationResult =
parseExpression("customProperties.get('sensitivity') == 'PII'")
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CompiledRule.parseExpression() runs ExpressionValidator.validateExpressionSafety(), which rejects any method calls not in the allow-list. The expression customProperties.get('sensitivity') will be parsed as a get(...) function call and should fail validation (since get is not an allowed function), so this test (and the documented usage) won’t work as written. Consider switching to SpEL map index syntax (e.g., customProperties['sensitivity'] == 'PII') or adding a dedicated @Function helper on RuleEvaluator (e.g., customProperty('sensitivity')) so it’s allow-listed.

Suggested change
parseExpression("customProperties.get('sensitivity') == 'PII'")
.getValue(context, Boolean.class);
assertTrue(result != null && result);
RuleEvaluator validationEvaluator = new RuleEvaluator(false);
StandardEvaluationContext validationContext = new StandardEvaluationContext(validationEvaluator);
Boolean validationResult =
parseExpression("customProperties.get('sensitivity') == 'PII'")
parseExpression("customProperties['sensitivity'] == 'PII'")
.getValue(context, Boolean.class);
assertTrue(result != null && result);
RuleEvaluator validationEvaluator = new RuleEvaluator(false);
StandardEvaluationContext validationContext = new StandardEvaluationContext(validationEvaluator);
Boolean validationResult =
parseExpression("customProperties['sensitivity'] == 'PII'")

Copilot uses AI. Check for mistakes.
Comment on lines +420 to +426
@SuppressWarnings("unused")
public Map<String, Object> getCustomProperties() {
if (resourceContext == null) {
return Collections.emptyMap();
}
return resourceContext.getCustomProperties();
}
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exposes customProperties as a Map, but with the current ExpressionValidator any customProperties.get(...) usage will fail allow-list checks (it sees get(...) as a function call). To make custom properties usable in policy conditions without weakening validation, consider either documenting/supporting index access (customProperties['key']) or adding an @Function helper (e.g., customProperty(String key) / matchCustomProperty(...)) that can be allow-listed.

Copilot uses AI. Check for mistakes.
Comment on lines +230 to +232
if (supportsExtension()) {
fields = EntityUtil.addField(fields, FIELD_EXTENSION);
}
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding FIELD_EXTENSION to the default fetched fields means every lazy resolveEntity() call will now load the entity extension for all entities that support it, even when no policy condition references custom properties. This can increase DB payload/serialization overhead for all authorization checks. Consider loading the extension lazily only when getCustomProperties() is actually used (e.g., a flag to include extension in the first fetch, or a targeted refetch of only the extension when needed).

Suggested change
if (supportsExtension()) {
fields = EntityUtil.addField(fields, FIELD_EXTENSION);
}
// Do not include FIELD_EXTENSION in the default lazy fetch path.
// Extension/custom properties should only be loaded when explicitly requested
// or when repository-specific PATCH/PUT field sets require them.

Copilot uses AI. Check for mistakes.
try {
return JsonUtils.getMap(entity.getExtension());
} catch (Exception e) {
LOG.warn("Failed to get custom properties: {}", e.getMessage());
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCustomProperties() logs only e.getMessage() and drops the stack trace, which makes diagnosing malformed extension payloads difficult in production. Consider logging the exception itself (or at least at debug level) so the root cause is preserved.

Suggested change
LOG.warn("Failed to get custom properties: {}", e.getMessage());
LOG.warn("Failed to get custom properties", e);

Copilot uses AI. Check for mistakes.
try {
entityRepository.getFields(FIELD_EXTENSION);
return true;
} catch (Exception e) {
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supportsExtension() uses catch (Exception) to detect whether FIELD_EXTENSION is allowed. This can mask unexpected failures (e.g., repository initialization issues) and silently skip loading extensions. Consider catching the specific exception thrown for invalid fields (e.g., IllegalArgumentException) and letting other exceptions propagate or be logged.

Suggested change
} catch (Exception e) {
} catch (IllegalArgumentException e) {

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

@gitar-bot
Copy link
Copy Markdown

gitar-bot Bot commented Apr 12, 2026

Code Review ✅ Approved 5 resolved / 5 findings

Exposes custom properties in SpEL policy rule conditions via getCustomProperties() on the RuleEvaluator root object, with fixes for null safety, lazy extension field loading, and control flow improvements. All findings resolved.

✅ 5 resolved
Bug: PR contains no functional changes — only a trailing newline

📄 openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/ResourceContext.java:255
The PR is titled "expose custom properties in SpEL policy rule conditions" but the only change in the diff is adding a blank line at the end of ResourceContext.java. No code was added to expose customProperties in the SpEL evaluation context.

This appears to be a WIP commit (the commit message says "WIP") that was accidentally opened as a PR with no implementation. The feature described in the title and issue #26774 is entirely missing.

Bug: getCustomProperties() not exposed via RuleEvaluator SpEL root object

📄 openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/ResourceContext.java:191-203 📄 openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/ResourceContextInterface.java:31-35
The PR adds getCustomProperties() to ResourceContextInterface and implements it in ResourceContext, but SpEL policy expressions are evaluated with RuleEvaluator as the root object (see CompiledRule.java line ~238: .withRootObject(ruleEvaluator)). Since RuleEvaluator has no method that delegates to resourceContext.getCustomProperties(), SpEL expressions like resource.customProperties.get('sensitivity') will fail at runtime.

All other ResourceContextInterface methods (e.g., getOwners(), getTags()) are exposed through explicit wrapper methods in RuleEvaluator (e.g., noOwner(), matchAllTags()). The same pattern is needed here.

Without a corresponding RuleEvaluator method, this feature is non-functional.

Bug: getCustomProperties() returns null, risks NPE in SpEL expressions

📄 openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/RuleEvaluator.java:376-381
In RuleEvaluator.getCustomProperties(), when resourceContext is null the method returns null. However, ResourceContext.getCustomProperties() consistently returns Collections.emptyMap() for all failure cases. A SpEL expression like getCustomProperties().get('sensitivity') will throw a NullPointerException when resourceContext is null (e.g., during expression validation via the no-arg constructor). Returning an empty map instead of null keeps behavior consistent and avoids NPE.

Performance: Extension field fetched for all entities, not just when needed

📄 openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/ResourceContext.java:230-232
The FIELD_EXTENSION is now unconditionally added to the field list in resolveEntity() (line 230-232), meaning every policy evaluation that resolves an entity will fetch extension data from the database — even if no policy rule references customProperties. For entities with large custom property payloads, this adds unnecessary data transfer on every authorization check.

Consider lazy-loading extension data only when getCustomProperties() is actually called, similar to how the entity itself is lazily resolved.

Quality: supportsExtension() uses exceptions for control flow

📄 openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/ResourceContext.java:275-282
The supportsExtension() method (lines 275-282) calls entityRepository.getFields(FIELD_EXTENSION) and relies on catching an exception to determine if the field is supported. Using exceptions for normal control flow is an anti-pattern — it's less readable and has minor overhead.

Consider checking the repository's allowed fields set directly instead.

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@PubChimps PubChimps added the safe to test Add this label to run secure Github workflows on PRs label Apr 16, 2026
@github-actions
Copy link
Copy Markdown
Contributor

The Java checkstyle failed.

Please run mvn spotless:apply in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Java code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

@PubChimps
Copy link
Copy Markdown
Contributor

Hi @SaaiAravindhRaja, thank you for your pr, but this issue has been assigned to a fix here, please make sure that you are assigned to an issue before submitting a fix for it, we will reopen this pr if necessary.

@PubChimps PubChimps closed this Apr 16, 2026
@sonarqubecloud
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown
Contributor

🟡 Playwright Results — all passed (25 flaky)

✅ 3622 passed · ❌ 0 failed · 🟡 25 flaky · ⏭️ 209 skipped

Shard Passed Failed Flaky Skipped
🟡 Shard 1 475 0 5 4
🟡 Shard 2 641 0 2 32
🟡 Shard 3 644 0 5 26
🟡 Shard 4 619 0 8 47
🟡 Shard 5 608 0 1 67
🟡 Shard 6 635 0 4 33
🟡 25 flaky test(s) (passed on retry)
  • Flow/SearchRBAC.spec.ts › User without permission (shard 1, 1 retry)
  • Features/CustomizeDetailPage.spec.ts › Data Product - customization should work (shard 1, 1 retry)
  • Features/Pagination.spec.ts › should test Database Schemas normal pagination (shard 1, 1 retry)
  • Features/Pagination.spec.ts › should test Spreadsheets normal pagination (shard 1, 1 retry)
  • Pages/UserCreationWithPersona.spec.ts › Create user with persona and verify on profile (shard 1, 1 retry)
  • Features/BulkEditEntity.spec.ts › Glossary (shard 2, 1 retry)
  • Features/ChangeSummaryBadge.spec.ts › Automated badge should appear on entity description with Automated source (shard 2, 1 retry)
  • Features/Permissions/EntityPermissions.spec.ts › Table deny common operations permissions (shard 3, 1 retry)
  • Features/Permissions/GlossaryPermissions.spec.ts › Team-based permissions work correctly (shard 3, 1 retry)
  • Features/Table.spec.ts › Tags term should be consistent for search (shard 3, 1 retry)
  • Flow/ExploreDiscovery.spec.ts › Should display deleted assets when showDeleted is checked and deleted is not present in queryFilter (shard 3, 1 retry)
  • Flow/PersonaDeletionUserProfile.spec.ts › User profile loads correctly before and after persona deletion (shard 3, 1 retry)
  • Pages/Customproperties-part2.spec.ts › entityReferenceList shows item count, scrollable list, no expand toggle (shard 4, 1 retry)
  • Pages/DataContracts.spec.ts › Create Data Contract and validate for File (shard 4, 1 retry)
  • Pages/Domains.spec.ts › Rename domain with data products attached at domain and subdomain levels (shard 4, 1 retry)
  • Pages/Domains.spec.ts › Comprehensive domain rename with ALL relationships preserved (shard 4, 1 retry)
  • Pages/Domains.spec.ts › Multiple consecutive domain renames preserve all associations (shard 4, 1 retry)
  • Pages/Entity.spec.ts › Glossary Term Add, Update and Remove for child entities (shard 4, 1 retry)
  • Pages/Entity.spec.ts › Inactive Announcement create & delete (shard 4, 1 retry)
  • Pages/Entity.spec.ts › Glossary Term Add, Update and Remove (shard 4, 1 retry)
  • Pages/ExploreTree.spec.ts › Verify Database and Database Schema available in explore tree (shard 5, 1 retry)
  • Pages/Lineage/DataAssetLineage.spec.ts › verify create lineage for entity - Table (shard 6, 1 retry)
  • Pages/Lineage/LineageFilters.spec.ts › Verify lineage schema filter selection (shard 6, 1 retry)
  • Pages/ServiceEntity.spec.ts › Tag Add, Update and Remove (shard 6, 1 retry)
  • Pages/Users.spec.ts › Permissions for table details page for Data Consumer (shard 6, 1 retry)

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Support for Custom Properties in Policy Rule Conditions

3 participants