This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/ws-neethi.git
commit 7a38f2be139d70fbf879fbb54eebe1fde633cc32 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Tue Apr 21 10:22:32 2026 +0100 Detect circular policy references --- .../org/apache/neethi/AbstractPolicyOperator.java | 21 +++++++--- .../org/apache/neethi/PolicyReferenceTest.java | 49 ++++++++++++++++++++++ 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/apache/neethi/AbstractPolicyOperator.java b/src/main/java/org/apache/neethi/AbstractPolicyOperator.java index b9064e0..015c8b2 100644 --- a/src/main/java/org/apache/neethi/AbstractPolicyOperator.java +++ b/src/main/java/org/apache/neethi/AbstractPolicyOperator.java @@ -20,8 +20,10 @@ package org.apache.neethi; import java.util.ArrayList; +import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Set; import org.apache.neethi.util.PolicyComparator; @@ -84,14 +86,15 @@ public abstract class AbstractPolicyOperator implements PolicyOperator { } - result.addPolicyComponent(normalizeOperator(policy, policy, reg, deep)); + result.addPolicyComponent(normalizeOperator(policy, policy, reg, deep, new HashSet<String>())); return result; } private static PolicyComponent normalizeOperator(Policy policy, PolicyOperator operator, PolicyRegistry reg, - boolean deep) { + boolean deep, + Set<String> resolving) { short type = operator.getType(); @@ -142,20 +145,26 @@ public abstract class AbstractPolicyOperator implements PolicyOperator { if (policyComponent == null) { throw new RuntimeException(uri + " can't be resolved"); } - + String resolvedId = ((Policy) policyComponent).getId(); + if (resolvedId != null && !resolving.add(resolvedId)) { + throw new RuntimeException("Circular PolicyReference detected: " + resolvedId); + } All all = new All(); all.addPolicyComponents(((Policy) policyComponent).getPolicyComponents()); - childComponentsList.add(AbstractPolicyOperator.normalizeOperator(policy, all, reg, deep)); + childComponentsList.add(AbstractPolicyOperator.normalizeOperator(policy, all, reg, deep, resolving)); + if (resolvedId != null) { + resolving.remove(resolvedId); + } } else if (policyComponent.getType() == Constants.TYPE_POLICY) { All all = new All(); all.addPolicyComponents(((Policy) policyComponent).getPolicyComponents()); - childComponentsList.add(AbstractPolicyOperator.normalizeOperator(policy, all, reg, deep)); + childComponentsList.add(AbstractPolicyOperator.normalizeOperator(policy, all, reg, deep, resolving)); } else { childComponentsList.add(AbstractPolicyOperator .normalizeOperator(policy, - (PolicyOperator)policyComponent, reg, deep)); + (PolicyOperator)policyComponent, reg, deep, resolving)); } } diff --git a/src/test/java/org/apache/neethi/PolicyReferenceTest.java b/src/test/java/org/apache/neethi/PolicyReferenceTest.java index 6633264..e0d0c46 100644 --- a/src/test/java/org/apache/neethi/PolicyReferenceTest.java +++ b/src/test/java/org/apache/neethi/PolicyReferenceTest.java @@ -162,4 +162,53 @@ public class PolicyReferenceTest extends PolicyTestCase { e.getMessage().contains("Unsupported URI scheme")); } } + + /** + * Test two policies that mutually reference each other cause unbounded recursion in + * AbstractPolicyOperator.normalizeOperator() when normalize(true) is called. + * + * Policy A contains a PolicyReference to "B". + * Policy B contains a PolicyReference to "A". + * Both are registered in the PolicyRegistry before normalization begins. + * + * normalizeOperator() resolves each PolicyReference via reg.lookup() and + * recurses into the resolved policy's components with no cycle detection, + * ultimately throwing StackOverflowError. + */ + @Test + public void testCircularPolicyReferenceThrowsStackOverflow() { + // Build Policy A: ExactlyOne > All > PolicyReference("B") + PolicyReference refToB = new PolicyReference(); + refToB.setURI("B"); + All allA = new All(); + allA.addPolicyComponent(refToB); + ExactlyOne eoA = new ExactlyOne(); + eoA.addPolicyComponent(allA); + Policy policyA = new Policy(registry); + policyA.setId("A"); + policyA.addPolicyComponent(eoA); + + // Build Policy B: ExactlyOne > All > PolicyReference("A") + PolicyReference refToA = new PolicyReference(); + refToA.setURI("A"); + All allB = new All(); + allB.addPolicyComponent(refToA); + ExactlyOne eoB = new ExactlyOne(); + eoB.addPolicyComponent(allB); + Policy policyB = new Policy(registry); + policyB.setId("B"); + policyB.addPolicyComponent(eoB); + + registry.register("A", policyA); + registry.register("B", policyB); + + try { + policyA.normalize(registry, true); + fail("Expected StackOverflowError or RuntimeException due to circular reference"); + } catch (RuntimeException e) { + assertTrue( + "Expected a cycle-detection message but got: " + e.getMessage(), + e.getMessage() != null && e.getMessage().toLowerCase().contains("circular")); + } + } }
