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

feat: Initial implementation of Catena-X policies #477

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the expected value should be a vararg, because there could be several allowed ones, same as we have for the validateOperator

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the expected value should be a vararg, because there could be several allowed ones, same as we have for the validateOperator

Yeah, that method is going to actually have to be modified when we switch versions from the left to right operand as described here. I'll do that in another PR.

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;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import jakarta.json.JsonStructure;
import jakarta.json.JsonValue;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;

import static java.util.Collections.emptySet;
import static java.util.stream.Collectors.toSet;

/**
* Provides functions for working with Json-Ld types.
*/
public class JsonLdTypeFunctions {
private static final String TYPE = "@type";
private static final Stream<JsonObject> EMPTY_STREAM = Stream.of();

/**
* Returns a stream of objects that are of the given Json-Ld type starting at the root.
*
* @param typeValue the type to include
* @param root the root object to traverse
* @return the stream of types
*/
public static Stream<JsonObject> extractObjectsOfType(String typeValue, JsonStructure root) {
if (root instanceof JsonObject rootObject) {
return matchTypeValue(typeValue, rootObject.get(TYPE)) ? Stream.of(rootObject) :
extractObjectsOfType(typeValue, rootObject.values().stream());
} else if (root instanceof JsonArray rootArray) {
return extractObjectsOfType(typeValue, rootArray.stream());
}
return EMPTY_STREAM;
}

/**
* Returns a stream of objects that are of the given Json-Ld type in the stream.
*
* @param typeValue the type to include
* @param stream the stream of roots to traverse
* @return the stream of types
*/
public static Stream<JsonObject> extractObjectsOfType(String typeValue, Stream<JsonValue> stream) {
return stream.filter(v -> v instanceof JsonStructure)
.flatMap(v -> extractObjectsOfType(typeValue, (JsonStructure) v)).filter(Objects::nonNull);
}

/**
* Partitions a stream of objects by their type, returning a type-to-collection mapping.
*/
public static Map<String, List<JsonObject>> partitionByType(Stream<JsonObject> stream) {
var partitions = new HashMap<String, List<JsonObject>>();
stream.forEach(object -> getTypes(object).forEach(type -> partitions.computeIfAbsent(type, k -> new ArrayList<>()).add(object)));
return partitions;
}

/**
* Returns the types associated with the object
*/
private static Set<String> getTypes(JsonObject object) {
var result = object.get(TYPE);
if (result instanceof JsonArray resultArray) {
return resultArray.stream().filter(e -> e instanceof JsonString).map(s -> ((JsonString) s).getString()).collect(toSet());
} else if (result instanceof JsonString resultString) {
return Set.of(resultString.getString());
}
return emptySet();
}

/**
* Returns true if the type value matches the Json value.
*/
private static boolean matchTypeValue(String typeValue, JsonValue jsonValue) {
if (jsonValue instanceof JsonString stringValue) {
return typeValue.equals(stringValue.getString());
} else if (jsonValue instanceof JsonArray arrayValue) {
return arrayValue.stream().anyMatch(v -> v instanceof JsonString && typeValue.equals(((JsonString) v).getString()));
}
return false;
}

private JsonLdTypeFunctions() {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.JsonArray;
import jakarta.json.JsonNumber;
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import jakarta.json.JsonValue;
import org.jetbrains.annotations.Nullable;

import static jakarta.json.JsonValue.ValueType.FALSE;
import static jakarta.json.JsonValue.ValueType.TRUE;
import static java.lang.String.valueOf;

/**
* Functions for working with Json-ld values.
*/
public class JsonLdValueFunctions {
private static final String VALUE = "@value";

/**
* Extracts the value of a root node and converts it to a string representation. Note this method accepts null nodes as a convenience.
*/
@Nullable
public static String extractStringValue(@Nullable JsonValue root) {
if (root == null) {
return null;
} else if (root instanceof JsonArray rootArray) {
if (rootArray.isEmpty()) {
return null;
}
var jsonValue = rootArray.get(0);
return (jsonValue instanceof JsonObject elementObject) ? convertType(elementObject.get(VALUE)) : null;
} else if (root instanceof JsonObject rootObject) {
return convertType(rootObject.get(VALUE));
} else {
return convertType(root);
}
}

/**
* Converts the value to a string representation.
*/
@Nullable
private static String convertType(JsonValue value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eventually we'll likely want to upstream these utilities

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I thinking we need separate set of Json-Ld utils.

if (value instanceof JsonString valueString) {
return valueString.getString();
} else if (value instanceof JsonNumber valueNumber) {
return valueNumber.isIntegral() ? valueOf(valueNumber.longValue()) : valueOf(valueNumber.doubleValue());
} else if (TRUE == value.getValueType()) {
return "TRUE";
} else if (FALSE == value.getValueType()) {
return "FALSE";
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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;

/**
* Defines policy namespaces.
*/
public interface PolicyNamespaces {

String W3C_VC_PREFIX = "https://www.w3.org/2018/credentials";
String W3C_VC_NS = W3C_VC_PREFIX + "/v1";
String W3_VP_PROPERTY = W3C_VC_PREFIX + "/vp";

String TX_NS = "https://w3id.org/2023/catenax/credentials/";
String TX_SUMMARY_NS = TX_NS + "summary";
String TX_SUMMARY_NS_V1 = TX_SUMMARY_NS + "/v1";
String TX_USE_CASE_NS = TX_NS + "usecase";
String TX_USE_CASE_NS_V1 = TX_USE_CASE_NS + "/v1";

String TX_SUMMARY_CREDENTIAL = "SummaryCredential";

}
Loading