Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: update CX policy extensions #485

2 changes: 1 addition & 1 deletion .github/workflows/deployment-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ jobs:
helm install tx-prod charts/tractusx-connector \
-f edc-tests/deployment/src/main/resources/helm/tractusx-connector-test.yaml \
--dependency-update \
--wait-for-jobs --timeout=120s
--wait-for-jobs --timeout=120s

# wait for the pod to become ready
kubectl rollout status deployment tx-prod-controlplane
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/verify.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ on:
branches:
- main
- releases
- previews/*
tags:
- '[0-9]+.[0-9]+.[0-9]+'
release:
Expand Down
6 changes: 6 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,10 @@ nexusPublishing {
maxRetries.set(120)
delayBetween.set(Duration.ofSeconds(10))
}
}

configurations.all {
// Check for updates every 5 mins
// TODO: REMOVE THIS BEFORE MERGING TO main!!!!
resolutionStrategy.cacheChangingModulesFor(5, TimeUnit.MINUTES)
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ protected boolean evaluate(
}

final ParticipantAgent participantAgent = policyContext.getParticipantAgent();

if (participantAgent == null) {
return false;
}
final Map<String, Object> claims = participantAgent.getClaims();

if (!claims.containsKey(REFERRING_CONNECTOR_CLAIM)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ void getEdr_shouldReturnDataAddress_whenFound() {
var transferProcessId = "id";
var edr = EndpointDataReference.Builder.newInstance().endpoint("test").id(transferProcessId).build();
var response = Json.createObjectBuilder()
.add(DataAddress.TYPE, EndpointDataReference.EDR_SIMPLE_TYPE)
.add(DataAddress.EDC_DATA_ADDRESS_TYPE_PROPERTY, EndpointDataReference.EDR_SIMPLE_TYPE)
.add(EndpointDataReference.ENDPOINT, edr.getEndpoint())
.add(EndpointDataReference.ID, edr.getId())
.build();
Expand Down Expand Up @@ -164,7 +164,7 @@ void queryEdrs_shouldReturnCachedEntries_whenAssetIdIsProvided() {
.add(EDR_ENTRY_TRANSFER_PROCESS_ID, entry.getTransferProcessId())
.add(EDR_ENTRY_AGREEMENT_ID, entry.getAgreementId())
.build();

when(adapterTransferProcessService.findByAssetAndAgreement(assetId, null)).thenReturn(ServiceResult.success(List.of(entry)));
when(transformerRegistry.transform(any(EndpointDataReferenceEntry.class), eq(JsonObject.class))).thenReturn(Result.success(response));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ public TransferProcessLocalCallback(EndpointDataReferenceCache edrCache, Transfe

@Override
public <T extends Event> Result<Void> invoke(CallbackEventRemoteMessage<T> message) {
if (message.getEventEnvelope().getPayload() instanceof TransferProcessStarted) {
var transferProcessStarted = (TransferProcessStarted) message.getEventEnvelope().getPayload();
if (message.getEventEnvelope().getPayload() instanceof TransferProcessStarted transferProcessStarted) {
if (transferProcessStarted.getDataAddress() != null) {
return EndpointDataAddressConstants.to(transferProcessStarted.getDataAddress())
.compose(this::storeEdr)
Expand All @@ -57,8 +56,7 @@ public <T extends Event> Result<Void> invoke(CallbackEventRemoteMessage<T> messa
private Result<Void> storeEdr(EndpointDataReference edr) {
return transactionContext.execute(() -> {
// TODO upstream api for getting the TP with the DataRequest#id
var transferProcessId = transferProcessStore.processIdForDataRequestId(edr.getId());
var transferProcess = transferProcessStore.findById(transferProcessId);
var transferProcess = transferProcessStore.findForCorrelationId(edr.getId());
if (transferProcess != null) {
var cacheEntry = EndpointDataReferenceEntry.Builder.newInstance()
.transferProcessId(transferProcess.getId())
Expand All @@ -69,7 +67,7 @@ private Result<Void> storeEdr(EndpointDataReference edr) {
edrCache.save(cacheEntry, edr);
return Result.success();
} else {
return Result.failure(format("Failed to find a transfer process with ID %s", transferProcessId));
return Result.failure(format("Failed to find a transfer process with correlation ID %s", edr.getId()));
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,6 @@ void invoke_shouldStoreTheEdrInCache_whenDataAddressIsPresent() {

var edr = getEdr();

when(transferProcessStore.processIdForDataRequestId(edr.getId())).thenReturn(transferProcessId);

var dataRequest = DataRequest.Builder.newInstance().id(edr.getId())
.destinationType("HttpProxy")
.assetId(assetId)
Expand All @@ -92,6 +90,8 @@ void invoke_shouldStoreTheEdrInCache_whenDataAddressIsPresent() {
.dataRequest(dataRequest)
.build();

when(transferProcessStore.findForCorrelationId(edr.getId())).thenReturn(transferProcess);

when(transferProcessStore.findById(transferProcessId)).thenReturn(transferProcess);


Expand Down Expand Up @@ -130,7 +130,7 @@ void invoke_shouldNotFail_whenTransferProcessNotFound() {

var edr = getEdr();

when(transferProcessStore.processIdForDataRequestId(edr.getId())).thenReturn(transferProcessId);
when(transferProcessStore.findForCorrelationId(edr.getId())).thenReturn(null);

when(transferProcessStore.findById(transferProcessId)).thenReturn(null);

Expand Down
24 changes: 24 additions & 0 deletions edc-extensions/cx-policy/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2023 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
*
*/

plugins {
`java-library`
}

dependencies {
implementation(libs.edc.spi.policyengine)
implementation(libs.jakartaJson)
testImplementation(libs.jacksonJsonP)
testImplementation(libs.titaniumJsonLd)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2023 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.tractusx.edc.policy.cx;

import org.eclipse.edc.policy.engine.spi.PolicyEngine;
import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;

import static org.eclipse.tractusx.edc.policy.cx.summary.SummaryConstraintFunctionsProvider.registerBindings;
import static org.eclipse.tractusx.edc.policy.cx.summary.SummaryConstraintFunctionsProvider.registerFunctions;

/**
* Provides implementations of standard CX usage policies.
*/
public class CxPolicyExtension implements ServiceExtension {
private static final String NAME = "CX Policy";

@Inject
private PolicyEngine policyEngine;

@Inject
private RuleBindingRegistry bindingRegistry;

@Override
public String name() {
return NAME;
}

@Override
public void initialize(ServiceExtensionContext context) {
registerFunctions(policyEngine);
registerBindings(bindingRegistry);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright (c) 2023 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.tractusx.edc.policy.cx.common;

import jakarta.json.JsonObject;
import jakarta.json.JsonValue;
import org.eclipse.edc.policy.engine.spi.AtomicConstraintFunction;
import org.eclipse.edc.policy.engine.spi.PolicyContext;
import org.eclipse.edc.policy.model.Operator;
import org.eclipse.edc.policy.model.Permission;
import org.jetbrains.annotations.Nullable;

import java.util.stream.Collectors;

import static jakarta.json.JsonValue.ValueType.ARRAY;
import static jakarta.json.JsonValue.ValueType.OBJECT;
import static java.lang.String.format;
import static java.util.Arrays.stream;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;

/**
* Base processing for constraint functions that verify a permission against a Catena-X verifiable presentation.
*/
public abstract class AbstractVpConstraintFunction implements AtomicConstraintFunction<Permission> {
protected static final String CREDENTIAL_SUBJECT = PolicyNamespaces.W3C_VC_PREFIX + "#credentialSubject";

protected static final String VALUE = "@value";

protected final String errorPrefix;

protected final String credentialType;

private static final String ERROR_PREFIX_TEMPLATE = "Invalid %s VC format: ";

/**
* Ctor.
*
* @param credentialType the credential type that will be verified against.
*/
public AbstractVpConstraintFunction(String credentialType) {
requireNonNull(credentialType);
this.credentialType = credentialType;
this.errorPrefix = format(ERROR_PREFIX_TEMPLATE, credentialType);
}

/**
* Validates the operator is in the set of expected operators.
*/
protected boolean validateOperator(Operator operator, PolicyContext context, Operator... expectedOperators) {
var set = stream(expectedOperators).collect(Collectors.toSet());
if (!set.contains(operator)) {
var valid = set.stream().map(Enum::toString).collect(joining(","));
context.reportProblem(format("Unsupported operator for %s credential constraint, only %s allowed: %s", credentialType, valid, operator));
return false;
}
return true;
}

/**
* Validates the VP by checking that it is a {@link JsonObject}.
*/
protected boolean validatePresentation(@Nullable Object vp, PolicyContext context) {
if (vp == null) {
context.reportProblem(format("%s VP not found", credentialType));
return false;
}

if (!(vp instanceof JsonValue jsonValue)) {
context.reportProblem(format("%s VP is not a JSON type: %s", credentialType, vp.getClass().getName()));
return false;
}

if (!(OBJECT == jsonValue.getValueType())) {
context.reportProblem(format("%s VP must be type %s but was: %s", credentialType, OBJECT, jsonValue.getValueType()));
return false;
}

return true;
}

/**
* Returns the credential subject portion of a VC or null if there was an error. Error information will be reported to the context.
*/
@Nullable
protected JsonObject extractCredentialSubject(JsonObject credential, PolicyContext context) {
var subjectArray = credential.get(CREDENTIAL_SUBJECT);
if (subjectArray == null || subjectArray.getValueType() != ARRAY) {
context.reportProblem(errorPrefix + " no credentialSubject found");
return null;
}
if (subjectArray.asJsonArray().size() != 1) {
context.reportProblem(errorPrefix + " empty credentialSubject");
return null;
}

var subjectValue = subjectArray.asJsonArray().get(0);
if (subjectValue == null || subjectValue.getValueType() != OBJECT) {
context.reportProblem(errorPrefix + " invalid credentialSubject format");
return null;
}

return subjectValue.asJsonObject();
}

/**
* Returns true if the actual operand value is a string literal case-insensitive equal to the expected value.
*/
protected boolean validateRightOperand(String expectedValue, Object actualValue, PolicyContext context) {
if (!(actualValue instanceof String)) {
context.reportProblem(format("Invalid right operand format specified for %s credential", credentialType));
return false;
}

if (!expectedValue.equalsIgnoreCase(actualValue.toString().trim())) {
context.reportProblem(format("Invalid right operand specified for %s credential: %s", credentialType, actualValue));
return false;
}

return true;
}

}