Skip to content

Commit

Permalink
feat: adds REST API for policy validation (#4448)
Browse files Browse the repository at this point in the history
* feat: adds REST API for policy validation

* chore: dependencies file
  • Loading branch information
wolf4ood authored Sep 2, 2024
1 parent 1858db2 commit bcb2e42
Show file tree
Hide file tree
Showing 18 changed files with 354 additions and 19 deletions.
2 changes: 1 addition & 1 deletion DEPENDENCIES
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ maven/mavencentral/com.lmax/disruptor/3.4.4, Apache-2.0, approved, clearlydefine
maven/mavencentral/com.networknt/json-schema-validator/1.0.76, Apache-2.0, approved, CQ22638
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.28, Apache-2.0, approved, clearlydefined
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.40, Apache-2.0, approved, #15156
maven/mavencentral/com.puppycrawl.tools/checkstyle/10.18.0, LGPL-2.1-or-later, restricted, clearlydefined
maven/mavencentral/com.puppycrawl.tools/checkstyle/10.18.1, LGPL-2.1-or-later, restricted, clearlydefined
maven/mavencentral/com.samskivert/jmustache/1.15, BSD-2-Clause AND BSD-3-Clause, approved, clearlydefined
maven/mavencentral/com.squareup.okhttp3/okhttp-dnsoverhttps/4.12.0, Apache-2.0, approved, #11159
maven/mavencentral/com.squareup.okhttp3/okhttp/4.12.0, Apache-2.0, approved, #15227
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ private <R extends Rule> List<AtomicConstraintFunction<Rule>> getFunctions(Strin
if (functions.isEmpty()) {
functions = dynamicConstraintFunctions
.stream()
.filter(f -> ruleKind.isAssignableFrom(f.type))
.filter(f -> f.function.canHandle(key))
.map(entry -> wrapDynamicFunction(key, entry.function))
.toList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,33 @@ void validate(Policy policy, Class<Rule> ruleClass, String key) {

}

@Test
void validate_shouldFail_withDynamicFunction() {

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();

DynamicAtomicConstraintFunction<Duty> function = mock();

when(function.canHandle(leftOperand)).thenReturn(true);

when(function.validate(any(), any(), any(), any())).thenReturn(Result.success());

bindingRegistry.dynamicBind(s -> Set.of(ALL_SCOPES));
policyEngine.registerFunction(ALL_SCOPES, Duty.class, function);

var result = policyEngine.validate(policy);

assertThat(result).isFailed()
.messages()
.anyMatch(s -> s.startsWith("left operand '%s' is not bound to any functions".formatted(leftOperand)));

}

@ParameterizedTest
@ArgumentsSource(PolicyProvider.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ transactionContext, contractValidationService, consumerOfferResolver, protocolTo
public PolicyDefinitionService policyDefinitionService() {
var policyDefinitionObservable = new PolicyDefinitionObservableImpl();
policyDefinitionObservable.registerListener(new PolicyDefinitionEventListener(clock, eventRouter));
return new PolicyDefinitionServiceImpl(transactionContext, policyDefinitionStore, contractDefinitionStore, policyDefinitionObservable);
return new PolicyDefinitionServiceImpl(transactionContext, policyDefinitionStore, contractDefinitionStore, policyDefinitionObservable, policyEngine);
}

@Provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@
import org.eclipse.edc.connector.controlplane.policy.spi.store.PolicyDefinitionStore;
import org.eclipse.edc.connector.controlplane.services.query.QueryValidator;
import org.eclipse.edc.connector.controlplane.services.spi.policydefinition.PolicyDefinitionService;
import org.eclipse.edc.policy.engine.spi.PolicyEngine;
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.Expression;
import org.eclipse.edc.policy.model.LiteralExpression;
import org.eclipse.edc.policy.model.MultiplicityConstraint;
import org.eclipse.edc.policy.model.OrConstraint;
import org.eclipse.edc.policy.model.Policy;
import org.eclipse.edc.policy.model.XoneConstraint;
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.result.ServiceResult;
import org.eclipse.edc.transaction.spi.TransactionContext;
import org.jetbrains.annotations.NotNull;
Expand All @@ -46,13 +49,16 @@ public class PolicyDefinitionServiceImpl implements PolicyDefinitionService {
private final ContractDefinitionStore contractDefinitionStore;
private final PolicyDefinitionObservable observable;
private final QueryValidator queryValidator;
private final PolicyEngine policyEngine;


public PolicyDefinitionServiceImpl(TransactionContext transactionContext, PolicyDefinitionStore policyStore,
ContractDefinitionStore contractDefinitionStore, PolicyDefinitionObservable observable) {
ContractDefinitionStore contractDefinitionStore, PolicyDefinitionObservable observable, PolicyEngine policyEngine) {
this.transactionContext = transactionContext;
this.policyStore = policyStore;
this.contractDefinitionStore = contractDefinitionStore;
this.observable = observable;
this.policyEngine = policyEngine;
queryValidator = new QueryValidator(PolicyDefinition.class, getSubtypeMap());
}

Expand Down Expand Up @@ -117,6 +123,11 @@ public ServiceResult<PolicyDefinition> update(PolicyDefinition policyDefinition)
});
}

@Override
public Result<Void> validate(Policy policy) {
return policyEngine.validate(policy);
}

private List<PolicyDefinition> queryPolicyDefinitions(QuerySpec query) {
return transactionContext.execute(() -> {
try (var stream = policyStore.findAll(query)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
import org.eclipse.edc.connector.controlplane.policy.spi.PolicyDefinition;
import org.eclipse.edc.connector.controlplane.policy.spi.observe.PolicyDefinitionObservable;
import org.eclipse.edc.connector.controlplane.policy.spi.store.PolicyDefinitionStore;
import org.eclipse.edc.policy.engine.spi.PolicyEngine;
import org.eclipse.edc.policy.model.Policy;
import org.eclipse.edc.spi.query.Criterion;
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.result.StoreResult;
import org.eclipse.edc.transaction.spi.NoopTransactionContext;
import org.eclipse.edc.transaction.spi.TransactionContext;
Expand Down Expand Up @@ -56,7 +58,8 @@ class PolicyDefinitionServiceImplTest {
private final ContractDefinitionStore contractDefinitionStore = mock(ContractDefinitionStore.class);
private final TransactionContext dummyTransactionContext = new NoopTransactionContext();
private final PolicyDefinitionObservable observable = mock(PolicyDefinitionObservable.class);
private final PolicyDefinitionServiceImpl policyServiceImpl = new PolicyDefinitionServiceImpl(dummyTransactionContext, policyStore, contractDefinitionStore, observable);
private final PolicyEngine policyEngine = mock();
private final PolicyDefinitionServiceImpl policyServiceImpl = new PolicyDefinitionServiceImpl(dummyTransactionContext, policyStore, contractDefinitionStore, observable, policyEngine);


@Test
Expand Down Expand Up @@ -240,6 +243,37 @@ void updatePolicy_shouldReturnNotFound_whenNotExists() {
verify(observable, never()).invokeForEach(any());
}

@Test
void validatePolicy() {
var policyId = "policyId";
var policy = createPolicy(policyId);

when(policyEngine.validate(policy.getPolicy())).thenReturn(Result.success());

assertThat(policyServiceImpl.validate(policy.getPolicy())).isSucceeded();
}

@Test
void validatePolicy_shouldFail_whenPolicyEngineFails() {
var policyId = "policyId";
var policy = createPolicy(policyId);

when(policyEngine.validate(policy.getPolicy())).thenReturn(Result.failure("validation failure"));

assertThat(policyServiceImpl.validate(policy.getPolicy())).isFailed()
.detail()
.isEqualTo("validation failure");
}

@NotNull
private Predicate<PolicyDefinition> hasId(String policyId) {
return it -> policyId.equals(it.getId());
}

private PolicyDefinition createPolicy(String policyId) {
return PolicyDefinition.Builder.newInstance().policy(Policy.Builder.newInstance().build()).id(policyId).build();
}

private static class InvalidFilters implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
Expand Down Expand Up @@ -268,13 +302,4 @@ public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
);
}
}

@NotNull
private Predicate<PolicyDefinition> hasId(String policyId) {
return it -> policyId.equals(it.getId());
}

private PolicyDefinition createPolicy(String policyId) {
return PolicyDefinition.Builder.newInstance().policy(Policy.Builder.newInstance().build()).id(policyId).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
{
"version": "3.1.0-alpha",
"urlPath": "/v3.1alpha",
"lastUpdated": "2024-07-10T09:17:00Z",
"lastUpdated": "2024-08-30T10:17:00Z",
"maturity": "alpha"
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
public abstract class BasePolicyDefinitionApiController {

protected final Monitor monitor;
private final TypeTransformerRegistry transformerRegistry;
private final PolicyDefinitionService service;
protected final PolicyDefinitionService service;
protected final TypeTransformerRegistry transformerRegistry;
private final JsonObjectValidatorRegistry validatorRegistry;

public BasePolicyDefinitionApiController(Monitor monitor, TypeTransformerRegistry transformerRegistry,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@

import jakarta.json.Json;
import org.eclipse.edc.connector.controlplane.api.management.policy.transform.JsonObjectFromPolicyDefinitionTransformer;
import org.eclipse.edc.connector.controlplane.api.management.policy.transform.JsonObjectFromPolicyValidationResultTransformer;
import org.eclipse.edc.connector.controlplane.api.management.policy.transform.JsonObjectToPolicyDefinitionTransformer;
import org.eclipse.edc.connector.controlplane.api.management.policy.v2.PolicyDefinitionApiV2Controller;
import org.eclipse.edc.connector.controlplane.api.management.policy.v3.PolicyDefinitionApiV3Controller;
import org.eclipse.edc.connector.controlplane.api.management.policy.v31alpha.PolicyDefinitionApiV31AlphaController;
import org.eclipse.edc.connector.controlplane.api.management.policy.validation.PolicyDefinitionValidator;
import org.eclipse.edc.connector.controlplane.services.spi.policydefinition.PolicyDefinitionService;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
Expand Down Expand Up @@ -69,11 +71,13 @@ public void initialize(ServiceExtensionContext context) {
var mapper = typeManager.getMapper(JSON_LD);
managementApiTransformerRegistry.register(new JsonObjectToPolicyDefinitionTransformer());
managementApiTransformerRegistry.register(new JsonObjectFromPolicyDefinitionTransformer(jsonBuilderFactory, mapper));
managementApiTransformerRegistry.register(new JsonObjectFromPolicyValidationResultTransformer(jsonBuilderFactory));

validatorRegistry.register(EDC_POLICY_DEFINITION_TYPE, PolicyDefinitionValidator.instance());

var monitor = context.getMonitor();
webService.registerResource(ApiContext.MANAGEMENT, new PolicyDefinitionApiV2Controller(monitor, managementApiTransformerRegistry, service, validatorRegistry));
webService.registerResource(ApiContext.MANAGEMENT, new PolicyDefinitionApiV3Controller(monitor, managementApiTransformerRegistry, service, validatorRegistry));
webService.registerResource(ApiContext.MANAGEMENT, new PolicyDefinitionApiV31AlphaController(monitor, managementApiTransformerRegistry, service, validatorRegistry));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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.connector.controlplane.api.management.policy.model;

import java.util.List;

import static org.eclipse.edc.spi.constants.CoreConstants.EDC_NAMESPACE;

public record PolicyValidationResult(boolean isValid, List<String> errors) {
public static final String EDC_POLICY_VALIDATION_RESULT_TYPE = EDC_NAMESPACE + "PolicyValidationResult";
public static final String EDC_POLICY_VALIDATION_RESULT_IS_VALID = EDC_NAMESPACE + "isValid";
public static final String EDC_POLICY_VALIDATION_RESULT_ERRORS = EDC_NAMESPACE + "errors";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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.connector.controlplane.api.management.policy.transform;

import jakarta.json.JsonBuilderFactory;
import jakarta.json.JsonObject;
import org.eclipse.edc.connector.controlplane.api.management.policy.model.PolicyValidationResult;
import org.eclipse.edc.jsonld.spi.transformer.AbstractJsonLdTransformer;
import org.eclipse.edc.transform.spi.TransformerContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import static org.eclipse.edc.connector.controlplane.api.management.policy.model.PolicyValidationResult.EDC_POLICY_VALIDATION_RESULT_TYPE;
import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE;

public class JsonObjectFromPolicyValidationResultTransformer extends AbstractJsonLdTransformer<PolicyValidationResult, JsonObject> {

private final JsonBuilderFactory jsonFactory;

public JsonObjectFromPolicyValidationResultTransformer(JsonBuilderFactory jsonFactory) {
super(PolicyValidationResult.class, JsonObject.class);
this.jsonFactory = jsonFactory;
}

@Override
public @Nullable JsonObject transform(@NotNull PolicyValidationResult input, @NotNull TransformerContext context) {
var objectBuilder = jsonFactory.createObjectBuilder();
objectBuilder.add(TYPE, EDC_POLICY_VALIDATION_RESULT_TYPE);
objectBuilder.add(PolicyValidationResult.EDC_POLICY_VALIDATION_RESULT_IS_VALID, input.isValid());
objectBuilder.add(PolicyValidationResult.EDC_POLICY_VALIDATION_RESULT_ERRORS, jsonFactory.createArrayBuilder(input.errors()));
return objectBuilder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import org.eclipse.edc.api.management.schema.ManagementApiSchema;
import org.eclipse.edc.api.model.ApiCoreSchema;

import java.util.List;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static org.eclipse.edc.connector.controlplane.policy.spi.PolicyDefinition.EDC_POLICY_DEFINITION_TYPE;
import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.CONTEXT;
Expand Down Expand Up @@ -99,6 +101,15 @@ public interface PolicyDefinitionApiV31Alpha {
)
void updatePolicyDefinitionV3(String id, JsonObject policyDefinition);

@Operation(description = "Validates an existing Policy, If the Policy is not found, an error is reported",
responses = {
@ApiResponse(responseCode = "200", description = "Returns the validation result", content = @Content(schema = @Schema(implementation = PolicyValidationResultSchema.class))),
@ApiResponse(responseCode = "404", description = "policy definition could not be updated, because it does not exists",
content = @Content(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))
}
)
JsonObject validatePolicyDefinitionV3(String id);

@Schema(name = "PolicyDefinitionInput", example = PolicyDefinitionInputSchema.POLICY_DEFINITION_INPUT_EXAMPLE)
record PolicyDefinitionInputSchema(
@Schema(name = CONTEXT, requiredMode = REQUIRED)
Expand Down Expand Up @@ -168,4 +179,22 @@ record PolicyDefinitionOutputSchema(
""";
}

@Schema(name = "PolicyValidationResultSchema", example = PolicyValidationResultSchema.POLICY_VALIDATION_RESULT_OUTPUT_EXAMPLE)
record PolicyValidationResultSchema(
Boolean isValid,
List<String> errors) {

public static final String POLICY_VALIDATION_RESULT_OUTPUT_EXAMPLE = """
{
"@context": { "@vocab": "https://w3id.org/edc/v0.0.1/ns/" },
"@type": "PolicyValidationResult",
"isValid": false,
"errors": [
"error1",
"error2"
]
}
""";
}

}
Loading

0 comments on commit bcb2e42

Please sign in to comment.