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: implement CredentialQueryResolver #168

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
7 changes: 0 additions & 7 deletions DEPENDENCIES
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
maven/mavencentral/com.apicatalog/carbon-did/0.0.2, Apache-2.0, approved, #9239

Check warning on line 1 in DEPENDENCIES

View workflow job for this annotation

GitHub Actions / check / Dash-Verify-Licenses

Restricted Dependencies found

Some dependencies are marked 'restricted' - please review them
maven/mavencentral/com.apicatalog/iron-ed25519-cryptosuite-2020/0.8.1, Apache-2.0, approved, #11157
maven/mavencentral/com.apicatalog/iron-verifiable-credentials/0.8.1, Apache-2.0, approved, #9234
maven/mavencentral/com.apicatalog/titanium-json-ld/1.0.0, Apache-2.0, approved, clearlydefined
Expand Down Expand Up @@ -314,19 +314,12 @@
maven/mavencentral/org.jetbrains/annotations/17.0.0, Apache-2.0, approved, clearlydefined
maven/mavencentral/org.jetbrains/annotations/24.0.1, Apache-2.0, approved, #7417
maven/mavencentral/org.junit-pioneer/junit-pioneer/2.1.0, EPL-2.0, approved, #10550
maven/mavencentral/org.junit.jupiter/junit-jupiter-api/5.10.0, EPL-2.0, approved, #9714
maven/mavencentral/org.junit.jupiter/junit-jupiter-api/5.10.1, EPL-2.0, approved, #9714
maven/mavencentral/org.junit.jupiter/junit-jupiter-engine/5.10.0, EPL-2.0, approved, #9711
maven/mavencentral/org.junit.jupiter/junit-jupiter-engine/5.10.1, EPL-2.0, approved, #9711
maven/mavencentral/org.junit.jupiter/junit-jupiter-params/5.10.0, EPL-2.0, approved, #9708
maven/mavencentral/org.junit.jupiter/junit-jupiter-params/5.10.1, EPL-2.0, approved, #9708
maven/mavencentral/org.junit.platform/junit-platform-commons/1.10.0, EPL-2.0, approved, #9715
maven/mavencentral/org.junit.platform/junit-platform-commons/1.10.1, EPL-2.0, approved, #9715
maven/mavencentral/org.junit.platform/junit-platform-engine/1.10.0, EPL-2.0, approved, #9709
maven/mavencentral/org.junit.platform/junit-platform-engine/1.10.1, EPL-2.0, approved, #9709
maven/mavencentral/org.junit.platform/junit-platform-launcher/1.10.0, EPL-2.0, approved, #9704
maven/mavencentral/org.junit.platform/junit-platform-launcher/1.10.1, EPL-2.0, approved, #9704
maven/mavencentral/org.junit/junit-bom/5.10.0, EPL-2.0, approved, #9844
maven/mavencentral/org.junit/junit-bom/5.10.1, EPL-2.0, approved, #9844
maven/mavencentral/org.junit/junit-bom/5.9.2, EPL-2.0, approved, #4711
maven/mavencentral/org.jvnet.mimepull/mimepull/1.9.15, CDDL-1.1 OR GPL-2.0-only WITH Classpath-exception-2.0, approved, CQ21484
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public Response queryPresentation(JsonObject query, @HeaderParam(AUTHORIZATION)
var credentials = queryResolver.query(presentationQuery, issuerScopes).orElseThrow(f -> new NotAuthorizedException(f.getFailureDetail()));

// package the credentials in a VP and sign
var presentationResponse = presentationGenerator.createPresentation(credentials, presentationQuery.getPresentationDefinition())
var presentationResponse = presentationGenerator.createPresentation(credentials.toList(), presentationQuery.getPresentationDefinition())
.orElseThrow(failure -> new EdcException("Error creating VerifiablePresentation: %s".formatted(failure.getFailureDetail())));
return Response.ok()
.entity(presentationResponse)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.eclipse.edc.identityhub.spi.model.PresentationSubmission;
import org.eclipse.edc.identityhub.spi.model.presentationdefinition.PresentationDefinition;
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.resolution.QueryResult;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
import org.eclipse.edc.junit.annotations.ApiTest;
import org.eclipse.edc.spi.EdcException;
Expand All @@ -41,13 +42,15 @@
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import java.util.stream.Stream;

import static jakarta.json.Json.createObjectBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.eclipse.edc.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.buildSignedJwt;
import static org.eclipse.edc.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.generateEcKey;
import static org.eclipse.edc.identityhub.spi.model.PresentationQuery.PRESENTATION_QUERY_TYPE_PROPERTY;
import static org.eclipse.edc.identityhub.spi.resolution.QueryResult.success;
import static org.eclipse.edc.validator.spi.ValidationResult.failure;
import static org.eclipse.edc.validator.spi.ValidationResult.success;
import static org.eclipse.edc.validator.spi.Violation.violation;
Expand Down Expand Up @@ -134,7 +137,7 @@ void query_queryResolutionFails_shouldReturn403() {
var presentationQueryBuilder = createPresentationQueryBuilder().build();
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.success(presentationQueryBuilder));
when(accessTokenVerifier.verify(anyString())).thenReturn(Result.success(List.of("test-scope1")));
when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(Result.failure("test-failure"));
when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(QueryResult.unauthorized("test-failure"));

assertThatThrownBy(() -> controller().queryPresentation(createObjectBuilder().build(), generateJwt()))
.isInstanceOf(NotAuthorizedException.class)
Expand All @@ -148,7 +151,7 @@ void query_presentationGenerationFails_shouldReturn500() {
var presentationQueryBuilder = createPresentationQueryBuilder().build();
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.success(presentationQueryBuilder));
when(accessTokenVerifier.verify(anyString())).thenReturn(Result.success(List.of("test-scope1")));
when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(Result.success(List.of()));
when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(success(Stream.empty()));

when(generator.createPresentation(anyList(), any())).thenReturn(Result.failure("test-failure"));

Expand All @@ -163,7 +166,7 @@ void query_success() {
var presentationQueryBuilder = createPresentationQueryBuilder().build();
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.success(presentationQueryBuilder));
when(accessTokenVerifier.verify(anyString())).thenReturn(Result.success(List.of("test-scope1")));
when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(Result.success(List.of()));
when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(success(Stream.empty()));

var pres = new PresentationResponse(generateJwt(), new PresentationSubmission("id", "def-id", List.of(new InputDescriptorMapping("id", "ldp_vp", "$.verifiableCredentials[0]"))));
when(generator.createPresentation(anyList(), any())).thenReturn(Result.success(pres));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@

package org.eclipse.edc.identityhub;

import org.eclipse.edc.identityhub.defaults.EdcScopeToCriterionTransformer;
import org.eclipse.edc.identityhub.defaults.InMemoryCredentialStore;
import org.eclipse.edc.identityhub.spi.ScopeToCriterionTransformer;
import org.eclipse.edc.identityhub.spi.generator.PresentationGenerator;
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.store.CredentialStore;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Provider;
Expand All @@ -31,15 +32,17 @@ public CredentialStore createInMemStore() {

}

@Provider(isDefault = true)
public CredentialQueryResolver createCredentialResolver(ServiceExtensionContext context) {
context.getMonitor().warning(" #### Creating a default NOOP CredentialQueryResolver, that will always return 'null'!");
return (query, issuerScopes) -> null;
}

@Provider(isDefault = true)
public PresentationGenerator createPresentationGenerator(ServiceExtensionContext context) {
context.getMonitor().warning(" #### Creating a default NOOP PresentationGenerator, that will always return 'null'!");
return (credentials, presentationDefinition) -> null;
}

@Provider(isDefault = true)
public ScopeToCriterionTransformer createScopeTransformer(ServiceExtensionContext context) {
context.getMonitor().warning("Using the default EdcScopeToCriterionTransformer. This is not intended for production use and should be replaced " +
"with a specialized implementation for your dataspace");
return new EdcScopeToCriterionTransformer();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
import org.eclipse.edc.iam.did.spi.key.PublicKeyWrapper;
import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry;
import org.eclipse.edc.iam.identitytrust.validation.SelfIssuedIdTokenValidator;
import org.eclipse.edc.identityhub.spi.ScopeToCriterionTransformer;
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.store.CredentialStore;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
import org.eclipse.edc.identityhub.token.verification.AccessTokenVerifierImpl;
import org.eclipse.edc.identitytrust.validation.JwtValidator;
Expand Down Expand Up @@ -61,6 +64,12 @@ public class CoreServicesExtension implements ServiceExtension {
@Inject
private JsonLd jsonLd;

@Inject
private CredentialStore credentialStore;

@Inject
private ScopeToCriterionTransformer transformer;

@Override
public void initialize(ServiceExtensionContext context) {
// Setup API
Expand Down Expand Up @@ -88,6 +97,11 @@ public JwtVerifier getJwtVerifier() {
return jwtVerifier;
}

@Provider
public CredentialQueryResolver createCredentialQueryResolver() {
return new CredentialQueryResolverImpl(credentialStore, transformer);
}

private String getOwnDid(ServiceExtensionContext context) {
return context.getConfig().getString(OWN_DID_PROPERTY);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* 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.edc.identityhub.core;

import org.eclipse.edc.identityhub.spi.ScopeToCriterionTransformer;
import org.eclipse.edc.identityhub.spi.model.PresentationQuery;
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.resolution.QueryResult;
import org.eclipse.edc.identityhub.spi.store.CredentialStore;
import org.eclipse.edc.identityhub.spi.store.model.VerifiableCredentialResource;
import org.eclipse.edc.spi.query.Criterion;
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.spi.result.AbstractResult;
import org.eclipse.edc.spi.result.Result;

import java.util.List;
import java.util.function.Predicate;

import static org.eclipse.edc.spi.result.Result.failure;
import static org.eclipse.edc.spi.result.Result.success;


public class CredentialQueryResolverImpl implements CredentialQueryResolver {

private final CredentialStore credentialStore;
private final ScopeToCriterionTransformer scopeTransformer;

public CredentialQueryResolverImpl(CredentialStore credentialStore, ScopeToCriterionTransformer scopeTransformer) {
this.credentialStore = credentialStore;
this.scopeTransformer = scopeTransformer;
}

@Override
public QueryResult query(PresentationQuery query, List<String> issuerScopes) {
if (query.getPresentationDefinition() != null) {
throw new UnsupportedOperationException("Querying with a DIF Presentation Exchange definition is not yet supported.");
}
if (query.getScopes().isEmpty()) {
return QueryResult.noScopeFound("Invalid query: must contain at least one scope.");
}

// check that all prover scopes are valid
var proverScopeResult = parseScopes(query.getScopes());
if (proverScopeResult.failed()) {
return QueryResult.invalidScope(proverScopeResult.getFailureMessages());
}

// check that all issuer scopes are valid
var issuerScopeResult = parseScopes(issuerScopes);
if (issuerScopeResult.failed()) {
return QueryResult.invalidScope(issuerScopeResult.getFailureMessages());
}

// query storage for requested credentials
var queryspec = convertToQuerySpec(proverScopeResult.getContent());
var credentialResult = credentialStore.query(queryspec);
if (credentialResult.failed()) {
return QueryResult.storageFailure(credentialResult.getFailureMessages());
}

// the credentials requested by the other party
var requestedCredentials = credentialResult.getContent().toList();

// check that prover scope is not wider than issuer scope
var issuerQuery = convertToQuerySpec(issuerScopeResult.getContent());
var predicate = issuerQuery.getFilterExpression().stream()
.map(c -> credentialsPredicate(c.getOperandRight().toString()))
.reduce(Predicate::or)
.orElse(x -> false);

// now narrow down the requested credentials to only contain allowed credentials
var isValidQuery = requestedCredentials.stream().filter(predicate).count() == requestedCredentials.size();

return isValidQuery ?
QueryResult.success(requestedCredentials.stream().map(VerifiableCredentialResource::getVerifiableCredential))
: QueryResult.unauthorized("Invalid query: requested Credentials outside of scope.");
}

/**
* Returns a predicate that filters {@link VerifiableCredentialResource} objects based on the provided type by
* inspecting the {@code types} property of the {@link org.eclipse.edc.identitytrust.model.VerifiableCredential} that is
* encapsulated in the resource.
*
* @param type The type to filter by.
* @return A predicate that filters {@link VerifiableCredentialResource} objects based on the provided type.
*/
private Predicate<VerifiableCredentialResource> credentialsPredicate(String type) {
return resource -> {
var cred = resource.getVerifiableCredential();
return cred != null && cred.credential() != null && cred.credential().getTypes().contains(type);
};
}

/**
* Parses a list of scope strings, converts them to {@link Criterion} objects, and returns a {@link Result} containing
* the list of converted criteria. If any scope string fails to be converted, a failure result is returned.
*
* @param scopes The list of scope strings to parse and convert.
* @return A {@link Result} containing the list of converted {@link Criterion} objects.
*/
private Result<List<Criterion>> parseScopes(List<String> scopes) {
var transformResult = scopes.stream()
.map(scopeTransformer::transform)
.toList();

if (transformResult.stream().anyMatch(AbstractResult::failed)) {
return failure(transformResult.stream().flatMap(r -> r.getFailureMessages().stream()).toList());
}

return success(transformResult.stream().map(AbstractResult::getContent).toList());
}

private QuerySpec convertToQuerySpec(List<Criterion> criteria) {
return QuerySpec.Builder.newInstance()
.filter(criteria)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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.edc.identityhub.defaults;

import org.eclipse.edc.identityhub.spi.ScopeToCriterionTransformer;
import org.eclipse.edc.spi.query.Criterion;
import org.eclipse.edc.spi.result.Result;

import java.util.List;

import static org.eclipse.edc.spi.result.Result.failure;
import static org.eclipse.edc.spi.result.Result.success;

/**
* Implementation of the {@link ScopeToCriterionTransformer} interface that converts a scope string to a {@link Criterion} object.
* This is a default/example implementation, that assumes scope strings adhere to the following format:
* <pre>
* org.eclipse.edc.vc.type:SomeCredential:[read|all|*]
* </pre>
* This scope string will get translated into a {@link Criterion} like:
* <pre>
* verifiableCredential.credential.types like SomeCredential
* </pre>
*
* <em>This MUST be adapted to the needs and requirements of the dataspace!</em>
* <em>Do NOT use this in production code!</em>
*/
public class EdcScopeToCriterionTransformer implements ScopeToCriterionTransformer {
public static final String TYPE_OPERAND = "verifiableCredential.credential.types";
public static final String ALIAS_LITERAL = "org.eclipse.edc.vc.type";
public static final String LIKE_OPERATOR = "like";
private static final String SCOPE_SEPARATOR = ":";
private final List<String> allowedOperations = List.of("read", "*", "all");

@Override
public Result<Criterion> transform(String scope) {
var tokens = parseScope(scope);
if (tokens.failed()) {
return failure("Scope string cannot be converted: %s".formatted(tokens.getFailureDetail()));
}
var credentialType = tokens.getContent()[1];
return success(new Criterion(TYPE_OPERAND, LIKE_OPERATOR, credentialType));
}

private Result<String[]> parseScope(String scope) {
if (scope == null) return failure("Scope was null");
paullatzelsperger marked this conversation as resolved.
Show resolved Hide resolved

var tokens = scope.split(SCOPE_SEPARATOR);
if (tokens.length != 3) {
return failure("Scope string has invalid format.");
}
if (!ALIAS_LITERAL.equalsIgnoreCase(tokens[0])) {
return failure("Scope alias MUST be %s but was %s".formatted(ALIAS_LITERAL, tokens[0]));
}
if (!allowedOperations.contains(tokens[2])) {
return failure("Invalid scope operation: " + tokens[2]);
}

return success(tokens);
}
}
Loading
Loading