Skip to content

Commit

Permalink
feat: adds policy validation in policy engine
Browse files Browse the repository at this point in the history
  • Loading branch information
wolf4ood committed Aug 21, 2024
1 parent 70d1b54 commit 504b11f
Show file tree
Hide file tree
Showing 14 changed files with 641 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";

private static final String DEFAULT_EDC_HOSTNAME = "localhost";
@Inject
private EventExecutorServiceContainer eventExecutorServiceContainer;

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -55,9 +58,11 @@ public class PolicyEngineImpl implements PolicyEngine {
private final Map<String, List<BiFunction<Policy, PolicyContext, Boolean>>> preValidators = new HashMap<>();
private final Map<String, List<BiFunction<Policy, PolicyContext, Boolean>>> 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
Expand Down Expand Up @@ -129,6 +134,20 @@ public Result<Void> evaluate(String scope, Policy policy, PolicyContext context)
}
}

@Override
public Result<Void> 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 <R extends Rule> void registerFunction(String scope, Class<R> type, String key, AtomicConstraintFunction<R> function) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,7 @@ public void dynamicBind(Function<String, Set<String>> 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;
}
Expand All @@ -62,4 +57,14 @@ public boolean isInScope(String ruleType, String scope) {
return false;
}

@Override
public Set<String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
/*
* 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.List;
import java.util.Map;
import java.util.Objects;
import java.util.Stack;
import java.util.TreeMap;
import java.util.stream.Collectors;

public class PolicyValidator implements Policy.Visitor<Result<Void>>, Rule.Visitor<Result<Void>>, Constraint.Visitor<Result<Void>> {

private final Stack<Rule> ruleContext = new Stack<>();

// Key -> Functions
private final Map<String, List<ConstraintFunctionEntry<Rule>>> constraintFunctions = new TreeMap<>();
private final List<DynamicAtomicConstraintFunctionEntry<Rule>> dynamicConstraintFunctions = new ArrayList<>();
private RuleValidator ruleValidator;

public Result<Void> validate(Policy policy) {
return policy.accept(this);
}

@Override
public Result<Void> visitAndConstraint(AndConstraint constraint) {
return validateMultiplicityConstraint(constraint);
}

@Override
public Result<Void> visitOrConstraint(OrConstraint constraint) {
return validateMultiplicityConstraint(constraint);
}

@Override
public Result<Void> visitXoneConstraint(XoneConstraint constraint) {
return validateMultiplicityConstraint(constraint);
}

@Override
public Result<Void> 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<Void> visitPolicy(Policy policy) {
var result = Result.success();
result = policy.getPermissions().stream()
.map(permission -> permission.accept(this))
.reduce(result, Result::merge);

result = policy.getProhibitions().stream()
.map(prohibition -> prohibition.accept(this))
.reduce(result, Result::merge);

result = policy.getObligations().stream()
.map(duty -> duty.accept(this))
.reduce(result, Result::merge);

return result;
}

@Override
public Result<Void> 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<Void> visitProhibition(Prohibition prohibition) {
return validateRule(prohibition);
}

@Override
public Result<Void> visitDuty(Duty duty) {
return validateRule(duty);
}

private Result<Void> validateMultiplicityConstraint(MultiplicityConstraint multiplicityConstraint) {
return multiplicityConstraint.getConstraints().stream()
.map(c -> c.accept(this))
.reduce(Result.success(), Result::merge);
}

private Result<Void> 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<Void> 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<Void> 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<Void> 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 <R extends Rule> List<AtomicConstraintFunction<Rule>> getFunctions(String key, Class<R> ruleKind) {
var functions = constraintFunctions.getOrDefault(key, new ArrayList<>())
.stream()
.filter(entry -> ruleKind.isAssignableFrom(entry.type()))
.map(entry -> entry.function)
.collect(Collectors.toList());

if (functions.isEmpty()) {
functions = dynamicConstraintFunctions
.stream()
.filter(f -> f.function.canHandle(key))
.map(entry -> wrapDynamicFunction(key, entry.function))
.toList();
}

return functions;
}

private <R extends Rule> AtomicConstraintFunction<R> wrapDynamicFunction(String key, DynamicAtomicConstraintFunction<R> 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 <R extends Rule> Builder evaluationFunction(String key, Class<R> ruleKind, AtomicConstraintFunction<R> function) {
validator.constraintFunctions.computeIfAbsent(key, k -> new ArrayList<>())
.add(new ConstraintFunctionEntry(ruleKind, function));
return this;
}

@SuppressWarnings({ "unchecked", "rawtypes" })
public <R extends Rule> Builder dynamicEvaluationFunction(Class<R> ruleKind, DynamicAtomicConstraintFunction<R> 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<R extends Rule>(
Class<R> type,
AtomicConstraintFunction<R> function) {
}

private record DynamicAtomicConstraintFunctionEntry<R extends Rule>(
Class<R> type,
DynamicAtomicConstraintFunction<R> function) {
}

private record AtomicConstraintFunctionWrapper<R extends Rule>(String leftOperand,
DynamicAtomicConstraintFunction<R> inner) implements AtomicConstraintFunction<R> {

@Override
public boolean evaluate(Operator operator, Object rightValue, R rule, PolicyContext context) {
throw new UnsupportedOperationException("Evaluation is not supported");
}

@Override
public Result<Void> validate(Operator operator, Object rightValue, R rule) {
return inner.validate(leftOperand, operator, rightValue, rule);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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;

public class RuleValidator {

private final RuleBindingRegistry registry;

public RuleValidator(RuleBindingRegistry registry) {
this.registry = registry;
}

boolean isBounded(String ruleType) {
return !registry.bindings(ruleType).isEmpty();
}
}
Loading

0 comments on commit 504b11f

Please sign in to comment.