From 96be76b1bac7e4d95b994a4e3656532f283971be Mon Sep 17 00:00:00 2001 From: Enrico Risa Date: Fri, 23 Aug 2024 14:42:08 +0200 Subject: [PATCH] feat: adds policy validation in policy engine (#4428) * feat: adds policy validation in policy engine * chore: dependencies file * pr suggestions --- .../connector/core/CoreServicesExtension.java | 7 +- .../edc/policy/engine/PolicyEngineImpl.java | 21 +- .../engine/RuleBindingRegistryImpl.java | 17 +- .../engine/validation/PolicyValidator.java | 255 +++++++++++++++ .../engine/validation/RuleValidator.java | 36 +++ .../engine/PolicyEngineImplScenariosTest.java | 5 +- .../policy/engine/PolicyEngineImplTest.java | 3 +- .../PolicyEngineImplValidationTest.java | 291 ++++++++++++++++++ ...ractExpiryCheckFunctionEvaluationTest.java | 3 +- .../engine/spi/AtomicConstraintFunction.java | 19 +- .../spi/DynamicAtomicConstraintFunction.java | 15 + .../edc/policy/engine/spi/PolicyEngine.java | 5 + .../engine/spi/RuleBindingRegistry.java | 6 + 13 files changed, 665 insertions(+), 18 deletions(-) create mode 100644 core/common/lib/policy-engine-lib/src/main/java/org/eclipse/edc/policy/engine/validation/PolicyValidator.java create mode 100644 core/common/lib/policy-engine-lib/src/main/java/org/eclipse/edc/policy/engine/validation/RuleValidator.java create mode 100644 core/common/lib/policy-engine-lib/src/test/java/org/eclipse/edc/policy/engine/PolicyEngineImplValidationTest.java diff --git a/core/common/connector-core/src/main/java/org/eclipse/edc/connector/core/CoreServicesExtension.java b/core/common/connector-core/src/main/java/org/eclipse/edc/connector/core/CoreServicesExtension.java index c2d2f7f93fe..184bbaf89dc 100644 --- a/core/common/connector-core/src/main/java/org/eclipse/edc/connector/core/CoreServicesExtension.java +++ b/core/common/connector-core/src/main/java/org/eclipse/edc/connector/core/CoreServicesExtension.java @@ -31,6 +31,7 @@ import org.eclipse.edc.policy.engine.ScopeFilter; import org.eclipse.edc.policy.engine.spi.PolicyEngine; import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry; +import org.eclipse.edc.policy.engine.validation.RuleValidator; import org.eclipse.edc.policy.model.PolicyRegistrationTypes; import org.eclipse.edc.query.CriterionOperatorRegistryImpl; import org.eclipse.edc.runtime.metamodel.annotation.BaseExtension; @@ -59,14 +60,11 @@ public class CoreServicesExtension implements ServiceExtension { public static final String NAME = "Core Services"; - private static final String DEFAULT_EDC_HOSTNAME = "localhost"; - @Setting(value = "Connector hostname, which e.g. is used in referer urls", defaultValue = DEFAULT_EDC_HOSTNAME) public static final String EDC_HOSTNAME = "edc.hostname"; @Setting(value = "The name of the claim key used to determine the participant identity", defaultValue = DEFAULT_IDENTITY_CLAIM_KEY) public static final String EDC_AGENT_IDENTITY_KEY = "edc.agent.identity.key"; - @Inject private EventExecutorServiceContainer eventExecutorServiceContainer; @@ -142,7 +140,8 @@ public RuleBindingRegistry ruleBindingRegistry() { @Provider public PolicyEngine policyEngine() { var scopeFilter = new ScopeFilter(ruleBindingRegistry); - return new PolicyEngineImpl(scopeFilter); + var ruleValidator = new RuleValidator(ruleBindingRegistry); + return new PolicyEngineImpl(scopeFilter, ruleValidator); } @Provider diff --git a/core/common/lib/policy-engine-lib/src/main/java/org/eclipse/edc/policy/engine/PolicyEngineImpl.java b/core/common/lib/policy-engine-lib/src/main/java/org/eclipse/edc/policy/engine/PolicyEngineImpl.java index 646065380c8..0578250739c 100644 --- a/core/common/lib/policy-engine-lib/src/main/java/org/eclipse/edc/policy/engine/PolicyEngineImpl.java +++ b/core/common/lib/policy-engine-lib/src/main/java/org/eclipse/edc/policy/engine/PolicyEngineImpl.java @@ -19,6 +19,8 @@ import org.eclipse.edc.policy.engine.spi.PolicyContext; import org.eclipse.edc.policy.engine.spi.PolicyEngine; import org.eclipse.edc.policy.engine.spi.RuleFunction; +import org.eclipse.edc.policy.engine.validation.PolicyValidator; +import org.eclipse.edc.policy.engine.validation.RuleValidator; import org.eclipse.edc.policy.evaluator.PolicyEvaluator; import org.eclipse.edc.policy.evaluator.RuleProblem; import org.eclipse.edc.policy.model.Duty; @@ -30,6 +32,7 @@ import org.jetbrains.annotations.NotNull; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -55,9 +58,11 @@ public class PolicyEngineImpl implements PolicyEngine { private final Map>> preValidators = new HashMap<>(); private final Map>> postValidators = new HashMap<>(); private final ScopeFilter scopeFilter; + private final RuleValidator ruleValidator; - public PolicyEngineImpl(ScopeFilter scopeFilter) { + public PolicyEngineImpl(ScopeFilter scopeFilter, RuleValidator ruleValidator) { this.scopeFilter = scopeFilter; + this.ruleValidator = ruleValidator; } @Override @@ -129,6 +134,20 @@ public Result evaluate(String scope, Policy policy, PolicyContext context) } } + @Override + public Result validate(Policy policy) { + var validatorBuilder = PolicyValidator.Builder.newInstance() + .ruleValidator(ruleValidator); + + constraintFunctions.values().stream() + .flatMap(Collection::stream) + .forEach(entry -> validatorBuilder.evaluationFunction(entry.key, entry.type, entry.function)); + + dynamicConstraintFunctions.forEach(entry -> validatorBuilder.dynamicEvaluationFunction(entry.type, entry.function)); + + return validatorBuilder.build().validate(policy); + } + @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public void registerFunction(String scope, Class type, String key, AtomicConstraintFunction function) { diff --git a/core/common/lib/policy-engine-lib/src/main/java/org/eclipse/edc/policy/engine/RuleBindingRegistryImpl.java b/core/common/lib/policy-engine-lib/src/main/java/org/eclipse/edc/policy/engine/RuleBindingRegistryImpl.java index 6ad946a831f..46f1d1fbe52 100644 --- a/core/common/lib/policy-engine-lib/src/main/java/org/eclipse/edc/policy/engine/RuleBindingRegistryImpl.java +++ b/core/common/lib/policy-engine-lib/src/main/java/org/eclipse/edc/policy/engine/RuleBindingRegistryImpl.java @@ -44,12 +44,7 @@ public void dynamicBind(Function> binder) { @Override public boolean isInScope(String ruleType, String scope) { - var boundScopes = ruleBindings.get(ruleType); - if (boundScopes == null) { - boundScopes = dynamicBinders.stream() - .flatMap(binder -> binder.apply(ruleType).stream()) - .collect(Collectors.toSet()); - } + var boundScopes = bindings(ruleType); if (boundScopes.contains(DELIMITED_ALL)) { return true; } @@ -62,4 +57,14 @@ public boolean isInScope(String ruleType, String scope) { return false; } + @Override + public Set bindings(String ruleType) { + var boundScopes = ruleBindings.get(ruleType); + if (boundScopes == null) { + boundScopes = dynamicBinders.stream() + .flatMap(binder -> binder.apply(ruleType).stream()) + .collect(Collectors.toSet()); + } + return boundScopes; + } } diff --git a/core/common/lib/policy-engine-lib/src/main/java/org/eclipse/edc/policy/engine/validation/PolicyValidator.java b/core/common/lib/policy-engine-lib/src/main/java/org/eclipse/edc/policy/engine/validation/PolicyValidator.java new file mode 100644 index 00000000000..3ad3876e2e5 --- /dev/null +++ b/core/common/lib/policy-engine-lib/src/main/java/org/eclipse/edc/policy/engine/validation/PolicyValidator.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.policy.engine.validation; + +import org.eclipse.edc.policy.engine.spi.AtomicConstraintFunction; +import org.eclipse.edc.policy.engine.spi.DynamicAtomicConstraintFunction; +import org.eclipse.edc.policy.engine.spi.PolicyContext; +import org.eclipse.edc.policy.model.AndConstraint; +import org.eclipse.edc.policy.model.AtomicConstraint; +import org.eclipse.edc.policy.model.Constraint; +import org.eclipse.edc.policy.model.Duty; +import org.eclipse.edc.policy.model.LiteralExpression; +import org.eclipse.edc.policy.model.MultiplicityConstraint; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.policy.model.OrConstraint; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.policy.model.Prohibition; +import org.eclipse.edc.policy.model.Rule; +import org.eclipse.edc.policy.model.XoneConstraint; +import org.eclipse.edc.spi.result.Result; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Stack; +import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Validate a policy. + *

+ * The policy validator is used to validate policies against a set of configured rule bindings, {@link AtomicConstraintFunction} and {@link DynamicAtomicConstraintFunction} + *

+ * The validation will fail under the following conditions: + * + *

    + *
  • If a rule action is not bound to a scope.
  • + *
  • If an {@link AtomicConstraint}'s left-operand is not bound to a scope. + *
  • If an {@link AtomicConstraint}'s left-operand is not bound to a function. + *
+ */ +public class PolicyValidator implements Policy.Visitor>, Rule.Visitor>, Constraint.Visitor> { + + private final Stack ruleContext = new Stack<>(); + + private final Map>> constraintFunctions = new TreeMap<>(); + private final List> dynamicConstraintFunctions = new ArrayList<>(); + private RuleValidator ruleValidator; + + public Result validate(Policy policy) { + return policy.accept(this); + } + + @Override + public Result visitAndConstraint(AndConstraint constraint) { + return validateMultiplicityConstraint(constraint); + } + + @Override + public Result visitOrConstraint(OrConstraint constraint) { + return validateMultiplicityConstraint(constraint); + } + + @Override + public Result visitXoneConstraint(XoneConstraint constraint) { + return validateMultiplicityConstraint(constraint); + } + + @Override + public Result visitAtomicConstraint(AtomicConstraint constraint) { + var currentRule = currentRule(); + var leftValue = constraint.getLeftExpression().accept(s -> s.getValue().toString()); + var rightValue = constraint.getRightExpression().accept(LiteralExpression::getValue); + + return validateLeftExpression(currentRule, leftValue) + .merge(validateConstraint(leftValue, constraint.getOperator(), rightValue, currentRule)); + } + + @Override + public Result visitPolicy(Policy policy) { + return Stream.of(policy.getPermissions(), policy.getProhibitions(), policy.getObligations()) + .flatMap(Collection::stream) + .map(rule -> rule.accept(this)) + .reduce(Result.success(), Result::merge); + } + + @Override + public Result visitPermission(Permission policy) { + var result = policy.getDuties().stream() + .map(duty -> duty.accept(this)) + .reduce(Result.success(), Result::merge); + return result.merge(validateRule(policy)); + } + + @Override + public Result visitProhibition(Prohibition prohibition) { + return validateRule(prohibition); + } + + @Override + public Result visitDuty(Duty duty) { + return validateRule(duty); + } + + private Result validateMultiplicityConstraint(MultiplicityConstraint multiplicityConstraint) { + return multiplicityConstraint.getConstraints().stream() + .map(c -> c.accept(this)) + .reduce(Result.success(), Result::merge); + } + + private Result validateLeftExpression(Rule rule, String leftOperand) { + if (!ruleValidator.isBounded(leftOperand)) { + return Result.failure("leftOperand '%s' is not bound to any scopes: Rule { %s } ".formatted(leftOperand, rule)); + } else { + return Result.success(); + } + } + + private Result validateConstraint(String leftOperand, Operator operator, Object rightOperand, Rule rule) { + var functions = getFunctions(leftOperand, rule.getClass()); + if (functions.isEmpty()) { + return Result.failure("left operand '%s' is not bound to any functions: Rule { %s }".formatted(leftOperand, rule)); + } else { + return functions.stream() + .map(f -> f.validate(operator, rightOperand, rule)) + .reduce(Result.success(), Result::merge); + } + } + + private Result validateRule(Rule rule) { + var initialResult = validateAction(rule); + try { + ruleContext.push(rule); + return rule.getConstraints().stream() + .map(constraint -> constraint.accept(this)) + .reduce(initialResult, Result::merge); + + } finally { + ruleContext.pop(); + } + } + + private Result validateAction(Rule rule) { + if (rule.getAction() != null && !ruleValidator.isBounded(rule.getAction().getType())) { + return Result.failure("action '%s' is not bound to any scopes: Rule { %s }".formatted(rule.getAction().getType(), rule)); + } else { + return Result.success(); + } + } + + private List> getFunctions(String key, Class ruleKind) { + // first look-up for an exact match + var functions = constraintFunctions.getOrDefault(key, new ArrayList<>()) + .stream() + .filter(entry -> ruleKind.isAssignableFrom(entry.type())) + .map(entry -> entry.function) + .collect(Collectors.toList()); + + // if not found inspect the dynamic functions + if (functions.isEmpty()) { + functions = dynamicConstraintFunctions + .stream() + .filter(f -> f.function.canHandle(key)) + .map(entry -> wrapDynamicFunction(key, entry.function)) + .toList(); + } + + return functions; + } + + private AtomicConstraintFunction wrapDynamicFunction(String key, DynamicAtomicConstraintFunction function) { + return new AtomicConstraintFunctionWrapper<>(key, function); + } + + private Rule currentRule() { + return ruleContext.peek(); + } + + public static class Builder { + private final PolicyValidator validator; + + private Builder() { + validator = new PolicyValidator(); + } + + public static PolicyValidator.Builder newInstance() { + return new PolicyValidator.Builder(); + } + + public Builder ruleValidator(RuleValidator ruleValidator) { + validator.ruleValidator = ruleValidator; + return this; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Builder evaluationFunction(String key, Class ruleKind, AtomicConstraintFunction function) { + validator.constraintFunctions.computeIfAbsent(key, k -> new ArrayList<>()) + .add(new ConstraintFunctionEntry(ruleKind, function)); + return this; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Builder dynamicEvaluationFunction(Class ruleKind, DynamicAtomicConstraintFunction function) { + validator.dynamicConstraintFunctions.add(new DynamicAtomicConstraintFunctionEntry(ruleKind, function)); + return this; + } + + public PolicyValidator build() { + Objects.requireNonNull(validator.ruleValidator, "Rule validator should not be null"); + return validator; + } + + } + + private record ConstraintFunctionEntry( + Class type, + AtomicConstraintFunction function) { + } + + private record DynamicAtomicConstraintFunctionEntry( + Class type, + DynamicAtomicConstraintFunction function) { + } + + private record AtomicConstraintFunctionWrapper( + String leftOperand, + DynamicAtomicConstraintFunction inner) implements AtomicConstraintFunction { + + @Override + public boolean evaluate(Operator operator, Object rightValue, R rule, PolicyContext context) { + throw new UnsupportedOperationException("Evaluation is not supported"); + } + + @Override + public Result validate(Operator operator, Object rightValue, R rule) { + return inner.validate(leftOperand, operator, rightValue, rule); + } + } +} diff --git a/core/common/lib/policy-engine-lib/src/main/java/org/eclipse/edc/policy/engine/validation/RuleValidator.java b/core/common/lib/policy-engine-lib/src/main/java/org/eclipse/edc/policy/engine/validation/RuleValidator.java new file mode 100644 index 00000000000..91d3238c51d --- /dev/null +++ b/core/common/lib/policy-engine-lib/src/main/java/org/eclipse/edc/policy/engine/validation/RuleValidator.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.policy.engine.validation; + +import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry; + +/** + * Validates rules in a {@link RuleBindingRegistry} + */ +public class RuleValidator { + + private final RuleBindingRegistry registry; + + public RuleValidator(RuleBindingRegistry registry) { + this.registry = registry; + } + + /** + * Checks if the input ruleType is bound to any scope + */ + boolean isBounded(String ruleType) { + return !registry.bindings(ruleType).isEmpty(); + } +} diff --git a/core/common/lib/policy-engine-lib/src/test/java/org/eclipse/edc/policy/engine/PolicyEngineImplScenariosTest.java b/core/common/lib/policy-engine-lib/src/test/java/org/eclipse/edc/policy/engine/PolicyEngineImplScenariosTest.java index 5907a84dab9..43cc162de45 100644 --- a/core/common/lib/policy-engine-lib/src/test/java/org/eclipse/edc/policy/engine/PolicyEngineImplScenariosTest.java +++ b/core/common/lib/policy-engine-lib/src/test/java/org/eclipse/edc/policy/engine/PolicyEngineImplScenariosTest.java @@ -16,6 +16,7 @@ import org.eclipse.edc.policy.engine.spi.PolicyContextImpl; import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry; +import org.eclipse.edc.policy.engine.validation.RuleValidator; import org.eclipse.edc.policy.model.Action; import org.eclipse.edc.policy.model.AtomicConstraint; import org.eclipse.edc.policy.model.LiteralExpression; @@ -49,7 +50,7 @@ public class PolicyEngineImplScenariosTest { @BeforeEach void setUp() { - policyEngine = new PolicyEngineImpl(new ScopeFilter(bindingRegistry)); + policyEngine = new PolicyEngineImpl(new ScopeFilter(bindingRegistry), new RuleValidator(bindingRegistry)); } /** @@ -141,5 +142,5 @@ void verifyConnectorUse() { assertThat(result.succeeded()).isTrue(); } - + } diff --git a/core/common/lib/policy-engine-lib/src/test/java/org/eclipse/edc/policy/engine/PolicyEngineImplTest.java b/core/common/lib/policy-engine-lib/src/test/java/org/eclipse/edc/policy/engine/PolicyEngineImplTest.java index 5a337f697b3..a4ba66348f0 100644 --- a/core/common/lib/policy-engine-lib/src/test/java/org/eclipse/edc/policy/engine/PolicyEngineImplTest.java +++ b/core/common/lib/policy-engine-lib/src/test/java/org/eclipse/edc/policy/engine/PolicyEngineImplTest.java @@ -18,6 +18,7 @@ import org.eclipse.edc.policy.engine.spi.PolicyContextImpl; import org.eclipse.edc.policy.engine.spi.PolicyEngine; import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry; +import org.eclipse.edc.policy.engine.validation.RuleValidator; import org.eclipse.edc.policy.model.Action; import org.eclipse.edc.policy.model.AtomicConstraint; import org.eclipse.edc.policy.model.Duty; @@ -60,7 +61,7 @@ class PolicyEngineImplTest { @BeforeEach void setUp() { - policyEngine = new PolicyEngineImpl(new ScopeFilter(bindingRegistry)); + policyEngine = new PolicyEngineImpl(new ScopeFilter(bindingRegistry), new RuleValidator(bindingRegistry)); } @Test diff --git a/core/common/lib/policy-engine-lib/src/test/java/org/eclipse/edc/policy/engine/PolicyEngineImplValidationTest.java b/core/common/lib/policy-engine-lib/src/test/java/org/eclipse/edc/policy/engine/PolicyEngineImplValidationTest.java new file mode 100644 index 00000000000..1194ae5d23b --- /dev/null +++ b/core/common/lib/policy-engine-lib/src/test/java/org/eclipse/edc/policy/engine/PolicyEngineImplValidationTest.java @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.policy.engine; + +import org.eclipse.edc.policy.engine.spi.AtomicConstraintFunction; +import org.eclipse.edc.policy.engine.spi.DynamicAtomicConstraintFunction; +import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry; +import org.eclipse.edc.policy.engine.validation.RuleValidator; +import org.eclipse.edc.policy.model.Action; +import org.eclipse.edc.policy.model.AndConstraint; +import org.eclipse.edc.policy.model.AtomicConstraint; +import org.eclipse.edc.policy.model.Duty; +import org.eclipse.edc.policy.model.LiteralExpression; +import org.eclipse.edc.policy.model.OrConstraint; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.policy.model.Prohibition; +import org.eclipse.edc.policy.model.Rule; +import org.eclipse.edc.policy.model.XoneConstraint; +import org.eclipse.edc.spi.result.Result; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import java.util.Set; +import java.util.stream.Stream; + +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.eclipse.edc.policy.engine.spi.PolicyEngine.ALL_SCOPES; +import static org.eclipse.edc.policy.model.Operator.EQ; +import static org.junit.jupiter.params.provider.Arguments.of; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class PolicyEngineImplValidationTest { + + private final RuleBindingRegistry bindingRegistry = new RuleBindingRegistryImpl(); + private PolicyEngine policyEngine; + + + @BeforeEach + void setUp() { + policyEngine = new PolicyEngineImpl(new ScopeFilter(bindingRegistry), new RuleValidator(bindingRegistry)); + } + + @Test + void validateEmptyPolicy() { + var emptyPolicy = Policy.Builder.newInstance().build(); + + var result = policyEngine.validate(emptyPolicy); + + assertThat(result).isSucceeded(); + } + + @Test + void validate_whenKeyNotBoundInTheRegistryAndToFunctions() { + + var left = new LiteralExpression("foo"); + var right = new LiteralExpression("bar"); + var constraint = AtomicConstraint.Builder.newInstance().leftExpression(left).operator(EQ).rightExpression(right).build(); + var permission = Permission.Builder.newInstance().constraint(constraint).build(); + var policy = Policy.Builder.newInstance().permission(permission).build(); + policyEngine.registerFunction(ALL_SCOPES, Duty.class, "foo", (op, rv, r, ctx) -> true); + policyEngine.registerFunction(ALL_SCOPES, Prohibition.class, "foo", (op, rv, r, ctx) -> true); + + var result = policyEngine.validate(policy); + + // The foo key is not bound nor to function nor in the RuleBindingRegistry + assertThat(result).isFailed().messages().hasSize(2) + .anyMatch(s -> s.startsWith("leftOperand 'foo' is not bound to any scopes")) + .anyMatch(s -> s.startsWith("left operand 'foo' is not bound to any functions")); + + } + + @ParameterizedTest + @ArgumentsSource(PolicyProvider.class) + void validate_whenKeyIsNotBoundInTheRegistry(Policy policy, Class ruleClass, String key) { + + policyEngine.registerFunction(ALL_SCOPES, ruleClass, key, (op, rv, duty, ctx) -> true); + + var result = policyEngine.validate(policy); + + // The input key is not bound in the RuleBindingRegistry + assertThat(result).isFailed().messages().hasSize(1) + .anyMatch(s -> s.startsWith("leftOperand '%s' is not bound to any scopes".formatted(key))); + + } + + @ParameterizedTest + @ArgumentsSource(PolicyProvider.class) + void validate(Policy policy, Class ruleClass, String key) { + + + bindingRegistry.bind(key, ALL_SCOPES); + policyEngine.registerFunction(ALL_SCOPES, ruleClass, key, (op, rv, duty, ctx) -> true); + + var result = policyEngine.validate(policy); + + assertThat(result).isSucceeded(); + + } + + + @ParameterizedTest + @ArgumentsSource(PolicyProvider.class) + void validate_withDynamicFunction(Policy policy, Class ruleClass, String key) { + + DynamicAtomicConstraintFunction function = mock(); + + when(function.canHandle(key)).thenReturn(true); + + when(function.validate(any(), any(), any(), any())).thenReturn(Result.success()); + + bindingRegistry.dynamicBind(s -> Set.of(ALL_SCOPES)); + policyEngine.registerFunction(ALL_SCOPES, ruleClass, function); + + var result = policyEngine.validate(policy); + + assertThat(result).isSucceeded(); + + } + + @ParameterizedTest + @ArgumentsSource(PolicyProvider.class) + void validate_shouldFail_whenSkippingDynamicFunction(Policy policy, Class ruleClass, String key) { + + DynamicAtomicConstraintFunction function = mock(); + + when(function.canHandle(key)).thenReturn(false); + + bindingRegistry.dynamicBind(s -> Set.of(ALL_SCOPES)); + policyEngine.registerFunction(ALL_SCOPES, ruleClass, function); + + var result = policyEngine.validate(policy); + + // The input key is not bound any functions , the dynamic one cannot handle the input key + assertThat(result).isFailed().messages().hasSize(1) + .anyMatch(s -> s.startsWith("left operand '%s' is not bound to any functions".formatted(key))); + + } + + @ParameterizedTest + @ArgumentsSource(PolicyProvider.class) + void validate_shouldFails_withDynamicFunction(Policy policy, Class ruleClass, String key) { + + DynamicAtomicConstraintFunction function = mock(); + + when(function.canHandle(key)).thenReturn(true); + + when(function.validate(any(), any(), any(), any())).thenReturn(Result.failure("Dynamic function validation failure")); + + bindingRegistry.dynamicBind(s -> Set.of(ALL_SCOPES)); + policyEngine.registerFunction(ALL_SCOPES, ruleClass, function); + + var result = policyEngine.validate(policy); + + assertThat(result).isFailed().detail().contains("Dynamic function validation failure"); + + } + + + @ParameterizedTest + @ArgumentsSource(PolicyProvider.class) + void validate_shouldFail_whenFunctionValidationFails(Policy policy, Class ruleClass, String key) { + + AtomicConstraintFunction function = mock(); + + when(function.validate(any(), any(), any())).thenReturn(Result.failure("Function validation failure")); + + bindingRegistry.bind(key, ALL_SCOPES); + policyEngine.registerFunction(ALL_SCOPES, ruleClass, key, function); + + var result = policyEngine.validate(policy); + + assertThat(result).isFailed().detail().contains("Function validation failure"); + + } + + @Test + void validate_shouldFail_whenActionIsNotBound() { + + var leftOperand = "foo"; + var left = new LiteralExpression(leftOperand); + var right = new LiteralExpression("bar"); + var constraint = AtomicConstraint.Builder.newInstance().leftExpression(left).operator(EQ).rightExpression(right).build(); + var permission = Permission.Builder.newInstance().constraint(constraint).action(Action.Builder.newInstance().type("use").build()).build(); + + var policy = Policy.Builder.newInstance().permission(permission).build(); + AtomicConstraintFunction function = mock(); + + when(function.validate(any(), any(), any())).thenReturn(Result.success()); + + bindingRegistry.bind("foo", ALL_SCOPES); + policyEngine.registerFunction(ALL_SCOPES, Permission.class, "foo", function); + + var result = policyEngine.validate(policy); + + // The use action is not bound in the RuleBindingRegistry + assertThat(result).isFailed().detail().contains("action 'use' is not bound to any scopes"); + + } + + @ParameterizedTest + @ArgumentsSource(PolicyWithMultiplicityConstraintProvider.class) + void validate_withMultiplicityConstraints(Policy policy, Class ruleClass, String[] keys) { + + + for (var key : keys) { + bindingRegistry.bind(key, ALL_SCOPES); + policyEngine.registerFunction(ALL_SCOPES, ruleClass, key, (op, rv, duty, ctx) -> true); + } + + + var result = policyEngine.validate(policy); + + assertThat(result).isSucceeded(); + + } + + private static class PolicyProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + + var leftOperand = "foo"; + var left = new LiteralExpression(leftOperand); + var right = new LiteralExpression("bar"); + var constraint = AtomicConstraint.Builder.newInstance().leftExpression(left).operator(EQ).rightExpression(right).build(); + var prohibition = Prohibition.Builder.newInstance().constraint(constraint).build(); + var permission = Permission.Builder.newInstance().constraint(constraint).build(); + var duty = Duty.Builder.newInstance().constraint(constraint).build(); + + return Stream.of( + of(Policy.Builder.newInstance().permission(permission).build(), Permission.class, leftOperand), + of(Policy.Builder.newInstance().duty(duty).build(), Duty.class, leftOperand), + of(Policy.Builder.newInstance().prohibition(prohibition).build(), Prohibition.class, leftOperand) + ); + } + } + + private static class PolicyWithMultiplicityConstraintProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + var keys = new String[]{ "foo", "baz" }; + var firstConstraint = atomicConstraint("foo", "bar"); + var secondConstraint = atomicConstraint("baz", "bar"); + + + var orConstraints = OrConstraint.Builder.newInstance().constraint(firstConstraint).constraint(secondConstraint).build(); + var andConstraints = AndConstraint.Builder.newInstance().constraint(firstConstraint).constraint(secondConstraint).build(); + var xoneConstraint = XoneConstraint.Builder.newInstance().constraint(firstConstraint).constraint(secondConstraint).build(); + + var prohibition = Prohibition.Builder.newInstance().constraint(orConstraints).build(); + var permission = Permission.Builder.newInstance().constraint(andConstraints).build(); + var duty = Duty.Builder.newInstance().constraint(xoneConstraint).build(); + + return Stream.of( + of(Policy.Builder.newInstance().permission(permission).build(), Permission.class, keys), + of(Policy.Builder.newInstance().duty(duty).build(), Duty.class, keys), + of(Policy.Builder.newInstance().prohibition(prohibition).build(), Prohibition.class, keys) + ); + } + + private AtomicConstraint atomicConstraint(String key, String value) { + var left = new LiteralExpression(key); + var right = new LiteralExpression(value); + return AtomicConstraint.Builder.newInstance() + .leftExpression(left) + .operator(EQ) + .rightExpression(right) + .build(); + } + } +} diff --git a/core/control-plane/lib/control-plane-policies-lib/src/test/java/org/eclipse/edc/connector/controlplane/policy/contract/ContractExpiryCheckFunctionEvaluationTest.java b/core/control-plane/lib/control-plane-policies-lib/src/test/java/org/eclipse/edc/connector/controlplane/policy/contract/ContractExpiryCheckFunctionEvaluationTest.java index 2c85a5d003c..05c01021c67 100644 --- a/core/control-plane/lib/control-plane-policies-lib/src/test/java/org/eclipse/edc/connector/controlplane/policy/contract/ContractExpiryCheckFunctionEvaluationTest.java +++ b/core/control-plane/lib/control-plane-policies-lib/src/test/java/org/eclipse/edc/connector/controlplane/policy/contract/ContractExpiryCheckFunctionEvaluationTest.java @@ -23,6 +23,7 @@ import org.eclipse.edc.policy.engine.spi.PolicyContextImpl; import org.eclipse.edc.policy.engine.spi.PolicyEngine; import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry; +import org.eclipse.edc.policy.engine.validation.RuleValidator; import org.eclipse.edc.policy.model.Action; import org.eclipse.edc.policy.model.AndConstraint; import org.eclipse.edc.policy.model.AtomicConstraint; @@ -68,7 +69,7 @@ void setup() { // bind/register rule to evaluate contract expiry bindingRegistry.bind("use", TRANSFER_SCOPE); bindingRegistry.bind(CONTRACT_EXPIRY_EVALUATION_KEY, TRANSFER_SCOPE); - policyEngine = new PolicyEngineImpl(new ScopeFilter(bindingRegistry)); + policyEngine = new PolicyEngineImpl(new ScopeFilter(bindingRegistry), new RuleValidator(bindingRegistry)); policyEngine.registerFunction(TRANSFER_SCOPE, Permission.class, CONTRACT_EXPIRY_EVALUATION_KEY, function); } diff --git a/spi/common/policy-engine-spi/src/main/java/org/eclipse/edc/policy/engine/spi/AtomicConstraintFunction.java b/spi/common/policy-engine-spi/src/main/java/org/eclipse/edc/policy/engine/spi/AtomicConstraintFunction.java index d52d47dd5a0..b3ec17b7cb3 100644 --- a/spi/common/policy-engine-spi/src/main/java/org/eclipse/edc/policy/engine/spi/AtomicConstraintFunction.java +++ b/spi/common/policy-engine-spi/src/main/java/org/eclipse/edc/policy/engine/spi/AtomicConstraintFunction.java @@ -16,6 +16,7 @@ import org.eclipse.edc.policy.model.Operator; import org.eclipse.edc.policy.model.Rule; +import org.eclipse.edc.spi.result.Result; /** * Invoked during policy evaluation when the left operand of an atomic constraint evaluates to a key associated with this function. The function is responsible for performing @@ -27,11 +28,23 @@ public interface AtomicConstraintFunction { /** * Performs the evaluation. * - * @param operator the operation + * @param operator the operation * @param rightValue the right-side expression for the constraint; the concrete type may be a string, primitive or object such as a JSON-LD encoded collection. - * @param rule the rule associated with the constraint - * @param context the policy context + * @param rule the rule associated with the constraint + * @param context the policy context */ boolean evaluate(Operator operator, Object rightValue, R rule, PolicyContext context); + + /** + * Performs a validation of an atomic constraint + * + * @param operator the operation + * @param rightValue the right-side expression for the constraint; the concrete type may be a string, primitive or object such as a JSON-LD encoded collection + * @param rule the rule associated with the constraint + * @return the result of the validation + */ + default Result validate(Operator operator, Object rightValue, R rule) { + return Result.success(); + } } diff --git a/spi/common/policy-engine-spi/src/main/java/org/eclipse/edc/policy/engine/spi/DynamicAtomicConstraintFunction.java b/spi/common/policy-engine-spi/src/main/java/org/eclipse/edc/policy/engine/spi/DynamicAtomicConstraintFunction.java index 44733149226..4cfc29dbfcb 100644 --- a/spi/common/policy-engine-spi/src/main/java/org/eclipse/edc/policy/engine/spi/DynamicAtomicConstraintFunction.java +++ b/spi/common/policy-engine-spi/src/main/java/org/eclipse/edc/policy/engine/spi/DynamicAtomicConstraintFunction.java @@ -16,6 +16,7 @@ import org.eclipse.edc.policy.model.Operator; import org.eclipse.edc.policy.model.Rule; +import org.eclipse.edc.spi.result.Result; /** * Invoked during policy evaluation as when the left operand of an atomic constraint evaluates to a key that is not bound to a {@link AtomicConstraintFunction}. @@ -42,4 +43,18 @@ public interface DynamicAtomicConstraintFunction { */ boolean canHandle(Object leftValue); + + /** + * Performs a validation of an atomic constraint + * + * @param leftValue the left-side expression for the constraint + * @param operator the operation + * @param rightValue the right-side expression for the constraint; the concrete type may be a string, primitive or object such as a JSON-LD encoded collection + * @param rule the rule associated with the constraint + * @return the result of the validation + */ + default Result validate(Object leftValue, Operator operator, Object rightValue, R rule) { + return Result.success(); + } + } diff --git a/spi/common/policy-engine-spi/src/main/java/org/eclipse/edc/policy/engine/spi/PolicyEngine.java b/spi/common/policy-engine-spi/src/main/java/org/eclipse/edc/policy/engine/spi/PolicyEngine.java index af010580054..d1fd26e034d 100644 --- a/spi/common/policy-engine-spi/src/main/java/org/eclipse/edc/policy/engine/spi/PolicyEngine.java +++ b/spi/common/policy-engine-spi/src/main/java/org/eclipse/edc/policy/engine/spi/PolicyEngine.java @@ -58,6 +58,11 @@ public interface PolicyEngine { */ Result evaluate(String scope, Policy policy, PolicyContext context); + /** + * Validates the given policy. + */ + Result validate(Policy policy); + /** * Registers a function that is invoked when a policy contains an atomic constraint whose left operator expression evaluates to the given key for the specified scope. * diff --git a/spi/common/policy-engine-spi/src/main/java/org/eclipse/edc/policy/engine/spi/RuleBindingRegistry.java b/spi/common/policy-engine-spi/src/main/java/org/eclipse/edc/policy/engine/spi/RuleBindingRegistry.java index 51a70d67175..68c63baa225 100644 --- a/spi/common/policy-engine-spi/src/main/java/org/eclipse/edc/policy/engine/spi/RuleBindingRegistry.java +++ b/spi/common/policy-engine-spi/src/main/java/org/eclipse/edc/policy/engine/spi/RuleBindingRegistry.java @@ -42,4 +42,10 @@ public interface RuleBindingRegistry { * Returns true of the rule type is bound to the scope; otherwise false. */ boolean isInScope(String ruleType, String scope); + + + /** + * Returns the bindings for a rule type; + */ + Set bindings(String ruleType); }