From 73a4ae29c882c6ad4bc7d6b0cc2e8a543b5da4c4 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Wed, 8 Nov 2023 06:47:34 +0100 Subject: [PATCH 1/9] add possibility to supply PublicKey as config value --- .../identityhub/DefaultServicesExtension.java | 7 ---- .../core/CoreServicesExtension.java | 10 +++++ .../core/CredentialQueryResolverImpl.java | 37 +++++++++++++++++++ .../resolver/PublicKeyWrapperExtension.java | 8 ++++ 4 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java index 6e2f7829b..bbccb33fc 100644 --- a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java @@ -16,7 +16,6 @@ import org.eclipse.edc.identityhub.defaults.InMemoryCredentialStore; 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; @@ -31,12 +30,6 @@ 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'!"); diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java index 36db996cb..e34d16d6d 100644 --- a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java @@ -17,6 +17,8 @@ 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.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; @@ -61,6 +63,9 @@ public class CoreServicesExtension implements ServiceExtension { @Inject private JsonLd jsonLd; + @Inject + private CredentialStore credentialStore; + @Override public void initialize(ServiceExtensionContext context) { // Setup API @@ -88,6 +93,11 @@ public JwtVerifier getJwtVerifier() { return jwtVerifier; } + @Provider + public CredentialQueryResolver createCredentialQueryResolver(ServiceExtensionContext context) { + return new CredentialQueryResolverImpl(credentialStore); + } + private String getOwnDid(ServiceExtensionContext context) { return context.getConfig().getString(OWN_DID_PROPERTY); } diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java new file mode 100644 index 000000000..a52dfa661 --- /dev/null +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java @@ -0,0 +1,37 @@ +/* + * 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.model.PresentationQuery; +import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver; +import org.eclipse.edc.identityhub.spi.store.CredentialStore; +import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; +import org.eclipse.edc.spi.result.Result; + +import java.util.List; + +public class CredentialQueryResolverImpl implements CredentialQueryResolver { + + private final CredentialStore credentialStore; + + public CredentialQueryResolverImpl(CredentialStore credentialStore) { + this.credentialStore = credentialStore; + } + + @Override + public Result> query(PresentationQuery query, List issuerScopes) { + return null; + } +} diff --git a/extensions/cryptography/public-key-provider/src/main/java/org/eclipse/edc/identityhub/publickey/resolver/PublicKeyWrapperExtension.java b/extensions/cryptography/public-key-provider/src/main/java/org/eclipse/edc/identityhub/publickey/resolver/PublicKeyWrapperExtension.java index d9a1562ce..ff7ba4876 100644 --- a/extensions/cryptography/public-key-provider/src/main/java/org/eclipse/edc/identityhub/publickey/resolver/PublicKeyWrapperExtension.java +++ b/extensions/cryptography/public-key-provider/src/main/java/org/eclipse/edc/identityhub/publickey/resolver/PublicKeyWrapperExtension.java @@ -51,6 +51,9 @@ public class PublicKeyWrapperExtension implements ServiceExtension { @Setting(value = "Path to a file that holds the public key, e.g. a PEM file. Do not use in production!") public static final String PUBLIC_KEY_PATH_PROPERTY = "edc.ih.iam.publickey.path"; + @Setting(value = "Public key in PEM format") + public static final String PUBLIC_KEY_PEM = "edc.ih.iam.publickey.pem"; + @Inject private Vault vault; @@ -67,6 +70,11 @@ public PublicKeyWrapper createPublicKey(ServiceExtensionContext context) { return getPublicKeyFromFile(path); } + var pem = context.getSetting(PUBLIC_KEY_PEM, null); + if (pem != null) { + return parseRawPublicKey(pem); + } + throw new EdcException("No public key was configured! Please either configure '%s' or '%s'.".formatted(PUBLIC_KEY_PATH_PROPERTY, PUBLIC_KEY_VAULT_ALIAS_PROPERTY)); } From 0e3bd787681e15f6a7b94d34f375981a06da5b06 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Wed, 8 Nov 2023 09:44:03 +0100 Subject: [PATCH 2/9] added QueryResolverImpl + Test --- .../api/v1/PresentationApiController.java | 2 +- .../api/v1/PresentationApiControllerTest.java | 5 +- .../core/CredentialQueryResolverImpl.java | 153 +++++++++++++- .../core/CredentialQueryResolverImplTest.java | 194 ++++++++++++++++++ ...t.java => ResolutionApiComponentTest.java} | 11 +- .../resolution/CredentialQueryResolver.java | 3 +- 6 files changed, 358 insertions(+), 10 deletions(-) create mode 100644 core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java rename e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/{ResolutionApiEndToEndTest.java => ResolutionApiComponentTest.java} (97%) diff --git a/core/identity-hub-api/src/main/java/org/eclipse/edc/identityservice/api/v1/PresentationApiController.java b/core/identity-hub-api/src/main/java/org/eclipse/edc/identityservice/api/v1/PresentationApiController.java index 23d3093e7..700a91ecd 100644 --- a/core/identity-hub-api/src/main/java/org/eclipse/edc/identityservice/api/v1/PresentationApiController.java +++ b/core/identity-hub-api/src/main/java/org/eclipse/edc/identityservice/api/v1/PresentationApiController.java @@ -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) diff --git a/core/identity-hub-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java b/core/identity-hub-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java index 58149b66e..a68e809d4 100644 --- a/core/identity-hub-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java +++ b/core/identity-hub-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java @@ -41,6 +41,7 @@ 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; @@ -148,7 +149,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(Result.success(Stream.empty())); when(generator.createPresentation(anyList(), any())).thenReturn(Result.failure("test-failure")); @@ -163,7 +164,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(Result.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)); diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java index a52dfa661..0d119af42 100644 --- a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java @@ -17,21 +17,172 @@ import org.eclipse.edc.identityhub.spi.model.PresentationQuery; import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver; import org.eclipse.edc.identityhub.spi.store.CredentialStore; +import org.eclipse.edc.identityhub.spi.store.model.VerifiableCredentialResource; import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; +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 org.jetbrains.annotations.Nullable; +import java.util.ArrayList; import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import static org.eclipse.edc.spi.result.Result.failure; +import static org.eclipse.edc.spi.result.Result.success; + public class CredentialQueryResolverImpl implements CredentialQueryResolver { + private static final String SCOPE_SEPARATOR = ":"; private final CredentialStore credentialStore; + private final List allowedOperations = List.of("read", "*", "all"); public CredentialQueryResolverImpl(CredentialStore credentialStore) { this.credentialStore = credentialStore; } @Override - public Result> query(PresentationQuery query, List issuerScopes) { + public Result> query(PresentationQuery query, List issuerScopes) { + if (query.getPresentationDefinition() != null) { + throw new UnsupportedOperationException("Querying with a DIF Presentation Exchange definition is not yet supported."); + } + if (query.getScopes().isEmpty()) { + return failure("Invalid query: must contain at least one scope."); + } + + // check that all prover scopes are valid + var proverScopeFailures = checkScope(query.getScopes()); + if (proverScopeFailures != null) return proverScopeFailures; + + // check that all issuer scopes are valid + var issuerScopeFailures = checkScope(issuerScopes); + if (issuerScopeFailures != null) return issuerScopeFailures; + + // query storage for requested credentials + var queryspec = convertToQuerySpec(query.getScopes()); + var res = credentialStore.query(queryspec); + if (res.failed()) { + return failure(res.getFailureMessages()); + } + + // the credentials requested by the other party + var wantedCredentials = res.getContent().toList(); + + // check that prover scope is not wider than issuer scope + var issuerQuery = convertToQuerySpec(issuerScopes); + 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 creds + var allowedCredentials = wantedCredentials.stream().filter(predicate).toList(); + + var isValidQuery = validateResults(new ArrayList<>(wantedCredentials), new ArrayList<>(allowedCredentials)); + + return isValidQuery ? + success(wantedCredentials.stream().map(VerifiableCredentialResource::getVerifiableCredential)) + : failure("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 credentialsPredicate(String type) { + return resource -> { + var cred = resource.getVerifiableCredential(); + return cred != null && cred.credential() != null && cred.credential().getTypes().contains(type); + }; + } + + @Nullable + private Result> checkScope(List query) { + var proverScopeFailures = query.stream() + .map(this::isValidScope) + .filter(AbstractResult::failed) + .flatMap(r -> r.getFailureMessages().stream()) + .toList(); + if (!proverScopeFailures.isEmpty()) { + return failure(proverScopeFailures); + } return null; } + + /** + * Checks whether the list of requested credentials is valid. Validity is determined by whether the list of requested credentials + * contains elements that are not in the list of allowed credentials. The list of allowed credentials may contain more elements, but not less. + * Every element, that is in the list of requested credentials must be found in the list of allowed credentials. + * + * @param requestedCredentials The list of requested credentials. + * @param allowedCredentials The list of allowed credentials. + * @return true if the list of requested credentials contains only elements that can be found in the list of allowed credentials, false otherwise. + */ + private boolean validateResults(List requestedCredentials, List allowedCredentials) { + if (requestedCredentials == allowedCredentials) { + return true; + } + if (requestedCredentials.size() != allowedCredentials.size()) { + return false; + } + + requestedCredentials.removeAll(allowedCredentials); + return requestedCredentials.isEmpty(); + } + + private QuerySpec convertToQuerySpec(List scopes) { + var criteria = scopes.stream() + .map(this::convertScopeToCriterion) + .toList(); + + return QuerySpec.Builder.newInstance() + .filter(criteria) + .build(); + } + + /** + * Converts a scope string to a {@link Criterion} object. For example, + *
+     *     org.eclipse.edc.vc.type:DemoCredential:read
+     * 
+ * would be converted to + *
+     *     verifiableCredential.credential.types contains DemoCredential
+     * 
+ *

+ * take note that the operation ("read") must be checked somewhere else, and is ignored here. + * + * @param scope The scope string to convert. + * @return The converted {@link Criterion} object. + */ + //todo: make this pluggable and more versatile + private Criterion convertScopeToCriterion(String scope) { + var tokens = isValidScope(scope); + if (tokens.failed()) { + throw new IllegalArgumentException("Scope string cannot be converted: %s".formatted(tokens.getFailureDetail())); + } + var credentialType = tokens.getContent()[1]; + return new Criterion("verifiableCredential.credential.types", "like", credentialType); + } + + private Result isValidScope(String scope) { + if (scope == null) return failure("Scope was null"); + + var tokens = scope.split(SCOPE_SEPARATOR); + if (tokens.length != 3) { + return failure("Scope string has invalid format."); + } + if (!allowedOperations.contains(tokens[2])) { + return failure("Invalid scope operation: " + tokens[2]); + } + + return success(tokens); + } } diff --git a/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java new file mode 100644 index 000000000..fd0e91d24 --- /dev/null +++ b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java @@ -0,0 +1,194 @@ +/* + * 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.model.PresentationQuery; +import org.eclipse.edc.identityhub.spi.model.presentationdefinition.PresentationDefinition; +import org.eclipse.edc.identityhub.spi.store.CredentialStore; +import org.eclipse.edc.identityhub.spi.store.model.VerifiableCredentialResource; +import org.eclipse.edc.identitytrust.model.CredentialFormat; +import org.eclipse.edc.identitytrust.model.CredentialSubject; +import org.eclipse.edc.identitytrust.model.Issuer; +import org.eclipse.edc.identitytrust.model.VerifiableCredential; +import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; +import org.eclipse.edc.spi.result.StoreResult; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.eclipse.edc.spi.result.StoreResult.success; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class CredentialQueryResolverImplTest { + + private final CredentialStore storeMock = mock(); + private final CredentialQueryResolverImpl resolver = new CredentialQueryResolverImpl(storeMock); + + @Test + void query_noResult() { + when(storeMock.query(any())).thenReturn(success(Stream.empty())); + var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), + List.of("org.eclipse.edc.vc.type:AnotherCredential:read")); + assertThat(res.succeeded()).isTrue(); + assertThat(res.getContent()).isEmpty(); + } + + @Test + void query_noProverScope_shouldReturnEmpty() { + when(storeMock.query(any())).thenReturn(success(Stream.empty())); + var res = resolver.query(createPresentationQuery(), List.of("foobar")); + assertThat(res.succeeded()).isFalse(); + assertThat(res.getFailureDetail()).contains("Invalid query: must contain at least one scope."); + } + + @Test + void query_proverScopeStringInvalid_shouldReturnFailure() { + when(storeMock.query(any())).thenReturn(success(Stream.empty())); + var res = resolver.query(createPresentationQuery("invalid"), + List.of("org.eclipse.edc.vc.type:AnotherCredential:read")); + assertThat(res.failed()).isTrue(); + assertThat(res.getFailureDetail()).isEqualTo("Scope string has invalid format."); + } + + @Test + void query_scopeStringHasWrongOperator_shouldReturnFailure() { + var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:write"), List.of("ignored")); + assertThat(res.failed()).isTrue(); + assertThat(res.getFailureDetail()).isEqualTo("Invalid scope operation: write"); + } + + @Test + void query_singleScopeString() { + var credential = createCredentialResource("TestCredential"); + when(storeMock.query(any())).thenReturn(success(Stream.of(credential))); + var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), + List.of("org.eclipse.edc.vc.type:TestCredential:read")); + assertThat(res.succeeded()).withFailMessage(res::getFailureDetail).isTrue(); + assertThat(res.getContent()).containsExactly(credential.getVerifiableCredential()); + } + + @Test + void query_multipleScopeStrings() { + var credential1 = createCredentialResource("TestCredential"); + var credential2 = createCredentialResource("AnotherCredential"); + when(storeMock.query(any())).thenReturn(success(Stream.of(credential1, credential2))); + + var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read", + "org.eclipse.edc.vc.type:AnotherCredential:read"), + List.of("org.eclipse.edc.vc.type:TestCredential:read", "org.eclipse.edc.vc.type:AnotherCredential:read")); + assertThat(res.succeeded()).withFailMessage(res::getFailureDetail).isTrue(); + assertThat(res.getContent()).containsExactlyInAnyOrder(credential1.getVerifiableCredential(), credential2.getVerifiableCredential()); + } + + @Test + void query_presentationDefinition_unsupported() { + var q = PresentationQuery.Builder.newinstance().presentationDefinition(PresentationDefinition.Builder.newInstance().id("test-pd").build()).build(); + assertThatThrownBy(() -> resolver.query(q, List.of("org.eclipse.edc.vc.type:SomeCredential:read"))) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessage("Querying with a DIF Presentation Exchange definition is not yet supported."); + } + + @Test + void query_requestsTooManyCredentials_shouldReturnFailure() { + var credential1 = createCredentialResource("TestCredential"); + var credential2 = createCredentialResource("AnotherCredential"); + when(storeMock.query(any())).thenReturn(success(Stream.of(credential1, credential2))); + + var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read", + "org.eclipse.edc.vc.type:AnotherCredential:read"), + List.of("org.eclipse.edc.vc.type:TestCredential:read")); + + assertThat(res.failed()).isTrue(); + assertThat(res.getFailureDetail()).isEqualTo("Invalid query: requested Credentials outside of scope."); + } + + @Test + void query_moreCredentialsAllowed_shouldReturnOnlyRequested() { + var credential1 = createCredentialResource("TestCredential"); + when(storeMock.query(any())).thenReturn(success(Stream.of(credential1))); + + var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), + List.of("org.eclipse.edc.vc.type:TestCredential:read", "org.eclipse.edc.vc.type:AnotherCredential:read")); + + assertThat(res.succeeded()).isTrue(); + assertThat(res.getContent()).containsOnly(credential1.getVerifiableCredential()); + } + + @Test + void query_exactMatchAllowedAndRequestedCredentials() { + var credential1 = createCredentialResource("TestCredential"); + when(storeMock.query(any())).thenReturn(success(Stream.of(credential1))); + + var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), + List.of("org.eclipse.edc.vc.type:TestCredential:read")); + + assertThat(res.succeeded()).isTrue(); + assertThat(res.getContent()).containsOnly(credential1.getVerifiableCredential()); + } + + @Test + void query_requestedCredentialNotAllowed() { + var credential1 = createCredentialResource("TestCredential"); + when(storeMock.query(any())).thenReturn(success(Stream.of(credential1))); + + var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), + List.of("org.eclipse.edc.vc.type:AnotherCredential:read")); + + assertThat(res.failed()).isTrue(); + assertThat(res.getFailureDetail()).isEqualTo("Invalid query: requested Credentials outside of scope."); + } + + @Test + void query_storeReturnsFailure() { + var credential1 = createCredentialResource("TestCredential"); + when(storeMock.query(any())).thenReturn(StoreResult.notFound("test-failure")); + + var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), + List.of("org.eclipse.edc.vc.type:AnotherCredential:read")); + + assertThat(res.failed()).isTrue(); + assertThat(res.getFailureDetail()).isEqualTo("test-failure"); + } + + private PresentationQuery createPresentationQuery(@Nullable String... scope) { + var scopes = new ArrayList<>(Arrays.asList(scope)); + return PresentationQuery.Builder.newinstance().scopes(scopes).build(); + } + + private VerifiableCredentialResource createCredentialResource(String... type) { + var cred = VerifiableCredential.Builder.newInstance() + .types(Arrays.asList(type)) + .issuer(new Issuer("test-issuer", Map.of())) + .issuanceDate(Instant.now()) + .credentialSubject(CredentialSubject.Builder.newInstance().id("test-cred-id").claim("test-claim", "test-value").build()) + .build(); + return VerifiableCredentialResource.Builder.newInstance() + .credential(new VerifiableCredentialContainer("foobar", CredentialFormat.JSON_LD, cred)) + .holderId("test-holder") + .issuerId("test-issuer") + .build(); + } +} \ No newline at end of file diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ResolutionApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ResolutionApiComponentTest.java similarity index 97% rename from e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ResolutionApiEndToEndTest.java rename to e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ResolutionApiComponentTest.java index 9d2d7df5e..b9e320f23 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ResolutionApiEndToEndTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ResolutionApiComponentTest.java @@ -23,13 +23,14 @@ import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier; import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubRuntimeConfiguration; import org.eclipse.edc.identityhub.tests.fixtures.TestData; -import org.eclipse.edc.junit.annotations.EndToEndTest; +import org.eclipse.edc.junit.annotations.ComponentTest; import org.eclipse.edc.junit.extensions.EdcRuntimeExtension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.ArgumentMatchers; import java.util.List; +import java.util.stream.Stream; import static io.restassured.http.ContentType.JSON; import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; @@ -44,8 +45,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@EndToEndTest -public class ResolutionApiEndToEndTest { +@ComponentTest +public class ResolutionApiComponentTest { public static final String VALID_QUERY_WITH_SCOPE = """ { "@context": [ @@ -174,7 +175,7 @@ void query_queryResolutionFails_shouldReturn403() { void query_presentationGenerationFails_shouldReturn500() { var token = generateSiToken(); when(ACCESS_TOKEN_VERIFIER.verify(eq(token))).thenReturn(success(List.of("test-scope1"))); - when(CREDENTIAL_QUERY_RESOLVER.query(any(), ArgumentMatchers.anyList())).thenReturn(success(List.of())); + when(CREDENTIAL_QUERY_RESOLVER.query(any(), ArgumentMatchers.anyList())).thenReturn(success(Stream.empty())); when(PRESENTATION_GENERATOR.createPresentation(anyList(), eq(null))).thenReturn(failure("generator test error")); IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() @@ -191,7 +192,7 @@ void query_presentationGenerationFails_shouldReturn500() { void query_success() { var token = generateSiToken(); when(ACCESS_TOKEN_VERIFIER.verify(eq(token))).thenReturn(success(List.of("test-scope1"))); - when(CREDENTIAL_QUERY_RESOLVER.query(any(), ArgumentMatchers.anyList())).thenReturn(success(List.of())); + when(CREDENTIAL_QUERY_RESOLVER.query(any(), ArgumentMatchers.anyList())).thenReturn(success(Stream.empty())); when(PRESENTATION_GENERATOR.createPresentation(anyList(), eq(null))).thenReturn(success(createPresentationResponse())); var resp = IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/CredentialQueryResolver.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/CredentialQueryResolver.java index 66fdd1493..d33705663 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/CredentialQueryResolver.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/CredentialQueryResolver.java @@ -19,6 +19,7 @@ import org.eclipse.edc.spi.result.Result; import java.util.List; +import java.util.stream.Stream; /** * Resolves a list of {@link VerifiableCredentialContainer} objects based on an incoming {@link PresentationQuery} and a list of scope strings. @@ -33,5 +34,5 @@ public interface CredentialQueryResolver { * @param query The representation of the query to be executed. * @param issuerScopes The list of issuer scopes to be considered during the query processing. */ - Result> query(PresentationQuery query, List issuerScopes); + Result> query(PresentationQuery query, List issuerScopes); } \ No newline at end of file From 5fe1e4672866aa7d7a9022a811f9fdd1feff40d5 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Wed, 8 Nov 2023 12:13:56 +0100 Subject: [PATCH 3/9] add storageresult class --- .../core/CredentialQueryResolverImpl.java | 27 +++---- .../core/CredentialQueryResolverImplTest.java | 7 ++ .../resolution/CredentialQueryResolver.java | 4 +- .../spi/resolution/QueryFailure.java | 39 ++++++++++ .../spi/resolution/QueryResult.java | 73 +++++++++++++++++++ 5 files changed, 132 insertions(+), 18 deletions(-) create mode 100644 spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/QueryFailure.java create mode 100644 spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/QueryResult.java diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java index 0d119af42..c2e6f4ed0 100644 --- a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java @@ -16,19 +16,17 @@ 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.identitytrust.model.VerifiableCredentialContainer; 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 org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; -import java.util.stream.Stream; import static org.eclipse.edc.spi.result.Result.failure; import static org.eclipse.edc.spi.result.Result.success; @@ -45,27 +43,27 @@ public CredentialQueryResolverImpl(CredentialStore credentialStore) { } @Override - public Result> query(PresentationQuery query, List issuerScopes) { + public QueryResult query(PresentationQuery query, List issuerScopes) { if (query.getPresentationDefinition() != null) { throw new UnsupportedOperationException("Querying with a DIF Presentation Exchange definition is not yet supported."); } if (query.getScopes().isEmpty()) { - return failure("Invalid query: must contain at least one scope."); + return QueryResult.noScopeFound("Invalid query: must contain at least one scope."); } // check that all prover scopes are valid - var proverScopeFailures = checkScope(query.getScopes()); - if (proverScopeFailures != null) return proverScopeFailures; + var proverScopeResult = checkScope(query.getScopes()); + if (proverScopeResult.failed()) return QueryResult.invalidScope(proverScopeResult.getFailureMessages()); // check that all issuer scopes are valid - var issuerScopeFailures = checkScope(issuerScopes); - if (issuerScopeFailures != null) return issuerScopeFailures; + var issuerScopeResult = checkScope(issuerScopes); + if (issuerScopeResult.failed()) return QueryResult.invalidScope(issuerScopeResult.getFailureMessages()); // query storage for requested credentials var queryspec = convertToQuerySpec(query.getScopes()); var res = credentialStore.query(queryspec); if (res.failed()) { - return failure(res.getFailureMessages()); + return QueryResult.storageFailure(res.getFailureMessages()); } // the credentials requested by the other party @@ -84,8 +82,8 @@ public Result> query(PresentationQuery que var isValidQuery = validateResults(new ArrayList<>(wantedCredentials), new ArrayList<>(allowedCredentials)); return isValidQuery ? - success(wantedCredentials.stream().map(VerifiableCredentialResource::getVerifiableCredential)) - : failure("Invalid query: requested Credentials outside of scope."); + QueryResult.success(wantedCredentials.stream().map(VerifiableCredentialResource::getVerifiableCredential)) + : QueryResult.unauthorized("Invalid query: requested Credentials outside of scope."); } /** @@ -103,8 +101,7 @@ private Predicate credentialsPredicate(String type }; } - @Nullable - private Result> checkScope(List query) { + private Result checkScope(List query) { var proverScopeFailures = query.stream() .map(this::isValidScope) .filter(AbstractResult::failed) @@ -113,7 +110,7 @@ private Result> checkScope(List qu if (!proverScopeFailures.isEmpty()) { return failure(proverScopeFailures); } - return null; + return success(); } /** diff --git a/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java index fd0e91d24..01e66d18e 100644 --- a/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java +++ b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java @@ -17,6 +17,7 @@ import org.eclipse.edc.identityhub.spi.model.PresentationQuery; import org.eclipse.edc.identityhub.spi.model.presentationdefinition.PresentationDefinition; +import org.eclipse.edc.identityhub.spi.resolution.QueryFailure; import org.eclipse.edc.identityhub.spi.store.CredentialStore; import org.eclipse.edc.identityhub.spi.store.model.VerifiableCredentialResource; import org.eclipse.edc.identitytrust.model.CredentialFormat; @@ -61,6 +62,7 @@ void query_noProverScope_shouldReturnEmpty() { when(storeMock.query(any())).thenReturn(success(Stream.empty())); var res = resolver.query(createPresentationQuery(), List.of("foobar")); assertThat(res.succeeded()).isFalse(); + assertThat(res.reason()).isEqualTo(QueryFailure.Reason.INVALID_SCOPE); assertThat(res.getFailureDetail()).contains("Invalid query: must contain at least one scope."); } @@ -70,6 +72,7 @@ void query_proverScopeStringInvalid_shouldReturnFailure() { var res = resolver.query(createPresentationQuery("invalid"), List.of("org.eclipse.edc.vc.type:AnotherCredential:read")); assertThat(res.failed()).isTrue(); + assertThat(res.reason()).isEqualTo(QueryFailure.Reason.INVALID_SCOPE); assertThat(res.getFailureDetail()).isEqualTo("Scope string has invalid format."); } @@ -77,6 +80,7 @@ void query_proverScopeStringInvalid_shouldReturnFailure() { void query_scopeStringHasWrongOperator_shouldReturnFailure() { var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:write"), List.of("ignored")); assertThat(res.failed()).isTrue(); + assertThat(res.reason()).isEqualTo(QueryFailure.Reason.INVALID_SCOPE); assertThat(res.getFailureDetail()).isEqualTo("Invalid scope operation: write"); } @@ -122,6 +126,7 @@ void query_requestsTooManyCredentials_shouldReturnFailure() { List.of("org.eclipse.edc.vc.type:TestCredential:read")); assertThat(res.failed()).isTrue(); + assertThat(res.reason()).isEqualTo(QueryFailure.Reason.UNAUTHORIZED_SCOPE); assertThat(res.getFailureDetail()).isEqualTo("Invalid query: requested Credentials outside of scope."); } @@ -158,6 +163,7 @@ void query_requestedCredentialNotAllowed() { List.of("org.eclipse.edc.vc.type:AnotherCredential:read")); assertThat(res.failed()).isTrue(); + assertThat(res.reason()).isEqualTo(QueryFailure.Reason.UNAUTHORIZED_SCOPE); assertThat(res.getFailureDetail()).isEqualTo("Invalid query: requested Credentials outside of scope."); } @@ -170,6 +176,7 @@ void query_storeReturnsFailure() { List.of("org.eclipse.edc.vc.type:AnotherCredential:read")); assertThat(res.failed()).isTrue(); + assertThat(res.reason()).isEqualTo(QueryFailure.Reason.STORAGE_FAILURE); assertThat(res.getFailureDetail()).isEqualTo("test-failure"); } diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/CredentialQueryResolver.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/CredentialQueryResolver.java index d33705663..559159742 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/CredentialQueryResolver.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/CredentialQueryResolver.java @@ -16,10 +16,8 @@ import org.eclipse.edc.identityhub.spi.model.PresentationQuery; import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; -import org.eclipse.edc.spi.result.Result; import java.util.List; -import java.util.stream.Stream; /** * Resolves a list of {@link VerifiableCredentialContainer} objects based on an incoming {@link PresentationQuery} and a list of scope strings. @@ -34,5 +32,5 @@ public interface CredentialQueryResolver { * @param query The representation of the query to be executed. * @param issuerScopes The list of issuer scopes to be considered during the query processing. */ - Result> query(PresentationQuery query, List issuerScopes); + QueryResult query(PresentationQuery query, List issuerScopes); } \ No newline at end of file diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/QueryFailure.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/QueryFailure.java new file mode 100644 index 000000000..93292e557 --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/QueryFailure.java @@ -0,0 +1,39 @@ +/* + * 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.spi.resolution; + +import org.eclipse.edc.spi.result.Failure; + +import java.util.List; + +public class QueryFailure extends Failure { + private final Reason reason; + + QueryFailure(List messages, Reason reason) { + super(messages); + this.reason = reason; + } + + public Reason getReason() { + return reason; + } + + public enum Reason { + INVALID_SCOPE, + STORAGE_FAILURE, + UNAUTHORIZED_SCOPE, + OTHER + } +} diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/QueryResult.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/QueryResult.java new file mode 100644 index 000000000..a79ed2c1d --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/QueryResult.java @@ -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.spi.resolution; + +import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; +import org.eclipse.edc.spi.result.AbstractResult; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static org.eclipse.edc.identityhub.spi.resolution.QueryFailure.Reason.INVALID_SCOPE; +import static org.eclipse.edc.identityhub.spi.resolution.QueryFailure.Reason.OTHER; +import static org.eclipse.edc.identityhub.spi.resolution.QueryFailure.Reason.STORAGE_FAILURE; +import static org.eclipse.edc.identityhub.spi.resolution.QueryFailure.Reason.UNAUTHORIZED_SCOPE; + + +public class QueryResult extends AbstractResult, QueryFailure, QueryResult> { + protected QueryResult(Stream content, QueryFailure failure) { + super(content, failure); + } + + public QueryFailure.Reason reason() { + return getFailure().getReason(); + } + + @Override + protected , C1> @NotNull R1 newInstance(@Nullable C1 content, @Nullable QueryFailure failure) { + if (content instanceof Stream) { + return (R1) new QueryResult((Stream) content, failure); + } + return (R1) new QueryResult(null, failure); + } + + public static QueryResult other(String... message) { + return new QueryResult(null, new QueryFailure(Arrays.asList(message), OTHER)); + } + + public static QueryResult noScopeFound(String message) { + return new QueryResult(null, new QueryFailure(List.of(message), INVALID_SCOPE)); + } + + public static QueryResult storageFailure(List failureMessages) { + return new QueryResult(null, new QueryFailure(failureMessages, STORAGE_FAILURE)); + } + + public static QueryResult invalidScope(List failureMessages) { + return new QueryResult(null, new QueryFailure(failureMessages, INVALID_SCOPE)); + } + + public static QueryResult unauthorized(String failureMessage) { + return new QueryResult(null, new QueryFailure(List.of(failureMessage), UNAUTHORIZED_SCOPE)); + } + + public static QueryResult success(Stream credentials) { + return new QueryResult(credentials, null); + } + +} From 5c681ddb02dbf018a255fcfbc4b0ad36c900d63d Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Wed, 8 Nov 2023 16:59:25 +0100 Subject: [PATCH 4/9] made scope-to-criterion conversion pluggable --- .../api/v1/PresentationApiControllerTest.java | 8 +- .../identityhub/DefaultServicesExtension.java | 10 +++ .../core/CoreServicesExtension.java | 6 +- .../core/CredentialQueryResolverImpl.java | 88 +++++-------------- .../EdcScopeToCriterionTransformer.java | 73 +++++++++++++++ .../core/CredentialQueryResolverImplTest.java | 7 +- .../EdcScopeToCriterionTransformerTest.java | 47 ++++++++++ .../tests/ResolutionApiComponentTest.java | 7 +- .../spi/ScopeToCriterionTransformer.java | 37 ++++++++ 9 files changed, 209 insertions(+), 74 deletions(-) create mode 100644 core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/defaults/EdcScopeToCriterionTransformer.java create mode 100644 core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/defaults/EdcScopeToCriterionTransformerTest.java create mode 100644 spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/ScopeToCriterionTransformer.java diff --git a/core/identity-hub-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java b/core/identity-hub-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java index a68e809d4..0b35c9471 100644 --- a/core/identity-hub-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java +++ b/core/identity-hub-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java @@ -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; @@ -49,6 +50,7 @@ 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; @@ -135,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) @@ -149,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(Stream.empty())); + when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(success(Stream.empty())); when(generator.createPresentation(anyList(), any())).thenReturn(Result.failure("test-failure")); @@ -164,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(Stream.empty())); + 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)); diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java index bbccb33fc..d4cb4c5ed 100644 --- a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java @@ -14,7 +14,9 @@ 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.store.CredentialStore; import org.eclipse.edc.runtime.metamodel.annotation.Extension; @@ -35,4 +37,12 @@ public PresentationGenerator createPresentationGenerator(ServiceExtensionContext 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(); + } + } diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java index e34d16d6d..2afe833ef 100644 --- a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java @@ -17,6 +17,7 @@ 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; @@ -66,6 +67,9 @@ public class CoreServicesExtension implements ServiceExtension { @Inject private CredentialStore credentialStore; + @Inject + private ScopeToCriterionTransformer transformer; + @Override public void initialize(ServiceExtensionContext context) { // Setup API @@ -95,7 +99,7 @@ public JwtVerifier getJwtVerifier() { @Provider public CredentialQueryResolver createCredentialQueryResolver(ServiceExtensionContext context) { - return new CredentialQueryResolverImpl(credentialStore); + return new CredentialQueryResolverImpl(credentialStore, transformer); } private String getOwnDid(ServiceExtensionContext context) { diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java index c2e6f4ed0..b69581c49 100644 --- a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java @@ -14,6 +14,7 @@ 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; @@ -34,12 +35,14 @@ public class CredentialQueryResolverImpl implements CredentialQueryResolver { - private static final String SCOPE_SEPARATOR = ":"; + private final CredentialStore credentialStore; - private final List allowedOperations = List.of("read", "*", "all"); - public CredentialQueryResolverImpl(CredentialStore credentialStore) { + private final ScopeToCriterionTransformer scopeTransformer; + + public CredentialQueryResolverImpl(CredentialStore credentialStore, ScopeToCriterionTransformer scopeTransformer) { this.credentialStore = credentialStore; + this.scopeTransformer = scopeTransformer; } @Override @@ -52,37 +55,37 @@ public QueryResult query(PresentationQuery query, List issuerScopes) { } // check that all prover scopes are valid - var proverScopeResult = checkScope(query.getScopes()); + var proverScopeResult = parseScopes(query.getScopes()); if (proverScopeResult.failed()) return QueryResult.invalidScope(proverScopeResult.getFailureMessages()); // check that all issuer scopes are valid - var issuerScopeResult = checkScope(issuerScopes); + var issuerScopeResult = parseScopes(issuerScopes); if (issuerScopeResult.failed()) return QueryResult.invalidScope(issuerScopeResult.getFailureMessages()); // query storage for requested credentials - var queryspec = convertToQuerySpec(query.getScopes()); + var queryspec = convertToQuerySpec(proverScopeResult.getContent()); var res = credentialStore.query(queryspec); if (res.failed()) { return QueryResult.storageFailure(res.getFailureMessages()); } // the credentials requested by the other party - var wantedCredentials = res.getContent().toList(); + var requestedCredentials = res.getContent().toList(); // check that prover scope is not wider than issuer scope - var issuerQuery = convertToQuerySpec(issuerScopes); + 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 creds - var allowedCredentials = wantedCredentials.stream().filter(predicate).toList(); + var allowedCredentials = requestedCredentials.stream().filter(predicate).toList(); - var isValidQuery = validateResults(new ArrayList<>(wantedCredentials), new ArrayList<>(allowedCredentials)); + var isValidQuery = validateResults(new ArrayList<>(requestedCredentials), new ArrayList<>(allowedCredentials)); return isValidQuery ? - QueryResult.success(wantedCredentials.stream().map(VerifiableCredentialResource::getVerifiableCredential)) + QueryResult.success(requestedCredentials.stream().map(VerifiableCredentialResource::getVerifiableCredential)) : QueryResult.unauthorized("Invalid query: requested Credentials outside of scope."); } @@ -101,16 +104,16 @@ private Predicate credentialsPredicate(String type }; } - private Result checkScope(List query) { - var proverScopeFailures = query.stream() - .map(this::isValidScope) - .filter(AbstractResult::failed) - .flatMap(r -> r.getFailureMessages().stream()) + private Result> parseScopes(List query) { + var transformResult = query.stream() + .map(scopeTransformer::transform) .toList(); - if (!proverScopeFailures.isEmpty()) { - return failure(proverScopeFailures); + + if (transformResult.stream().anyMatch(AbstractResult::failed)) { + return failure(transformResult.stream().flatMap(r -> r.getFailureMessages().stream()).toList()); } - return success(); + + return success(transformResult.stream().map(AbstractResult::getContent).toList()); } /** @@ -126,7 +129,7 @@ private boolean validateResults(List requestedCred if (requestedCredentials == allowedCredentials) { return true; } - if (requestedCredentials.size() != allowedCredentials.size()) { + if (requestedCredentials.size() > allowedCredentials.size()) { return false; } @@ -134,52 +137,9 @@ private boolean validateResults(List requestedCred return requestedCredentials.isEmpty(); } - private QuerySpec convertToQuerySpec(List scopes) { - var criteria = scopes.stream() - .map(this::convertScopeToCriterion) - .toList(); - + private QuerySpec convertToQuerySpec(List criteria) { return QuerySpec.Builder.newInstance() .filter(criteria) .build(); } - - /** - * Converts a scope string to a {@link Criterion} object. For example, - *

-     *     org.eclipse.edc.vc.type:DemoCredential:read
-     * 
- * would be converted to - *
-     *     verifiableCredential.credential.types contains DemoCredential
-     * 
- *

- * take note that the operation ("read") must be checked somewhere else, and is ignored here. - * - * @param scope The scope string to convert. - * @return The converted {@link Criterion} object. - */ - //todo: make this pluggable and more versatile - private Criterion convertScopeToCriterion(String scope) { - var tokens = isValidScope(scope); - if (tokens.failed()) { - throw new IllegalArgumentException("Scope string cannot be converted: %s".formatted(tokens.getFailureDetail())); - } - var credentialType = tokens.getContent()[1]; - return new Criterion("verifiableCredential.credential.types", "like", credentialType); - } - - private Result isValidScope(String scope) { - if (scope == null) return failure("Scope was null"); - - var tokens = scope.split(SCOPE_SEPARATOR); - if (tokens.length != 3) { - return failure("Scope string has invalid format."); - } - if (!allowedOperations.contains(tokens[2])) { - return failure("Invalid scope operation: " + tokens[2]); - } - - return success(tokens); - } } diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/defaults/EdcScopeToCriterionTransformer.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/defaults/EdcScopeToCriterionTransformer.java new file mode 100644 index 000000000..d3543c94a --- /dev/null +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/defaults/EdcScopeToCriterionTransformer.java @@ -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: + *

+ *  org.eclipse.edc.vc.type:SomeCredential:[read|all|*]
+ * 
+ * This scope string will get translated into a {@link Criterion} like: + *
+ *     verifiableCredential.credential.types like SomeCredential
+ * 
+ * + * This MUST be adapted to the needs and requirements of the dataspace! + * Do NOT use this in production code! + */ +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 allowedOperations = List.of("read", "*", "all"); + + @Override + public Result 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 parseScope(String scope) { + if (scope == null) return failure("Scope was null"); + + 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); + } +} diff --git a/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java index 01e66d18e..47d290efa 100644 --- a/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java +++ b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java @@ -15,6 +15,7 @@ package org.eclipse.edc.identityhub.core; +import org.eclipse.edc.identityhub.defaults.EdcScopeToCriterionTransformer; import org.eclipse.edc.identityhub.spi.model.PresentationQuery; import org.eclipse.edc.identityhub.spi.model.presentationdefinition.PresentationDefinition; import org.eclipse.edc.identityhub.spi.resolution.QueryFailure; @@ -46,7 +47,7 @@ class CredentialQueryResolverImplTest { private final CredentialStore storeMock = mock(); - private final CredentialQueryResolverImpl resolver = new CredentialQueryResolverImpl(storeMock); + private final CredentialQueryResolverImpl resolver = new CredentialQueryResolverImpl(storeMock, new EdcScopeToCriterionTransformer()); @Test void query_noResult() { @@ -73,7 +74,7 @@ void query_proverScopeStringInvalid_shouldReturnFailure() { List.of("org.eclipse.edc.vc.type:AnotherCredential:read")); assertThat(res.failed()).isTrue(); assertThat(res.reason()).isEqualTo(QueryFailure.Reason.INVALID_SCOPE); - assertThat(res.getFailureDetail()).isEqualTo("Scope string has invalid format."); + assertThat(res.getFailureDetail()).contains("Scope string has invalid format."); } @Test @@ -81,7 +82,7 @@ void query_scopeStringHasWrongOperator_shouldReturnFailure() { var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:write"), List.of("ignored")); assertThat(res.failed()).isTrue(); assertThat(res.reason()).isEqualTo(QueryFailure.Reason.INVALID_SCOPE); - assertThat(res.getFailureDetail()).isEqualTo("Invalid scope operation: write"); + assertThat(res.getFailureDetail()).contains("Invalid scope operation: write"); } @Test diff --git a/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/defaults/EdcScopeToCriterionTransformerTest.java b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/defaults/EdcScopeToCriterionTransformerTest.java new file mode 100644 index 000000000..6c7fe4878 --- /dev/null +++ b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/defaults/EdcScopeToCriterionTransformerTest.java @@ -0,0 +1,47 @@ +/* + * 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.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; + +class EdcScopeToCriterionTransformerTest { + private final EdcScopeToCriterionTransformer transformer = new EdcScopeToCriterionTransformer(); + + @ParameterizedTest + @ValueSource(strings = { + "org.eclipse.edc.vc.type:TestCredential:read", + "org.eclipse.edc.vc.type:TestCredential:*", + "org.eclipse.edc.vc.type:TestCredential:all", + "org.eclipse.edc.vc.type:foo:all", + }) + void transform_validScope(String scope) { + assertThat(transformer.transform(scope)).isSucceeded(); + } + + @ParameterizedTest + @ValueSource(strings = { + "invalidAlias:TestCredential:read", + "org.eclipse.edc.vc.type:TestCredential:write", + "org.eclipse.edc.vc.type:TestCredential:foo", + "org.eclipse.edc::foo", + "org.eclipse.edc:foo", + }) + void transform_invalidScope(String scope) { + assertThat(transformer.transform(scope)).isFailed(); + } +} \ No newline at end of file diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ResolutionApiComponentTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ResolutionApiComponentTest.java index b9e320f23..3296fac04 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ResolutionApiComponentTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ResolutionApiComponentTest.java @@ -20,6 +20,7 @@ import org.eclipse.edc.identityhub.spi.model.PresentationResponse; import org.eclipse.edc.identityhub.spi.model.PresentationSubmission; 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.identityhub.tests.fixtures.IdentityHubRuntimeConfiguration; import org.eclipse.edc.identityhub.tests.fixtures.TestData; @@ -157,7 +158,7 @@ void query_tokenVerificationFails_shouldReturn401() { void query_queryResolutionFails_shouldReturn403() { var token = generateSiToken(); when(ACCESS_TOKEN_VERIFIER.verify(eq(token))).thenReturn(success(List.of("test-scope1"))); - when(CREDENTIAL_QUERY_RESOLVER.query(any(), ArgumentMatchers.anyList())).thenReturn(failure("scope mismatch!")); + when(CREDENTIAL_QUERY_RESOLVER.query(any(), ArgumentMatchers.anyList())).thenReturn(QueryResult.unauthorized("scope mismatch!")); IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() .contentType(JSON) @@ -175,7 +176,7 @@ void query_queryResolutionFails_shouldReturn403() { void query_presentationGenerationFails_shouldReturn500() { var token = generateSiToken(); when(ACCESS_TOKEN_VERIFIER.verify(eq(token))).thenReturn(success(List.of("test-scope1"))); - when(CREDENTIAL_QUERY_RESOLVER.query(any(), ArgumentMatchers.anyList())).thenReturn(success(Stream.empty())); + when(CREDENTIAL_QUERY_RESOLVER.query(any(), ArgumentMatchers.anyList())).thenReturn(QueryResult.success(Stream.empty())); when(PRESENTATION_GENERATOR.createPresentation(anyList(), eq(null))).thenReturn(failure("generator test error")); IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() @@ -192,7 +193,7 @@ void query_presentationGenerationFails_shouldReturn500() { void query_success() { var token = generateSiToken(); when(ACCESS_TOKEN_VERIFIER.verify(eq(token))).thenReturn(success(List.of("test-scope1"))); - when(CREDENTIAL_QUERY_RESOLVER.query(any(), ArgumentMatchers.anyList())).thenReturn(success(Stream.empty())); + when(CREDENTIAL_QUERY_RESOLVER.query(any(), ArgumentMatchers.anyList())).thenReturn(QueryResult.success(Stream.empty())); when(PRESENTATION_GENERATOR.createPresentation(anyList(), eq(null))).thenReturn(success(createPresentationResponse())); var resp = IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/ScopeToCriterionTransformer.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/ScopeToCriterionTransformer.java new file mode 100644 index 000000000..63933417f --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/ScopeToCriterionTransformer.java @@ -0,0 +1,37 @@ +/* + * 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.spi; + +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.result.Result; + +/** + * Converts a scope string to a {@link Criterion} object. Implementations must be able to parse the shape of the + * scope string and convert it into a {@link Criterion}. + *

+ * The shape of the scope string is specific to the dataspace. + */ +@FunctionalInterface +public interface ScopeToCriterionTransformer { + /** + * Converts a scope string to a {@link Criterion} object. If the scope string is invalid, a failure result is returned. + * This can happen, for example if the shape of the string is not correct, or if a wrong operator is used in a specific + * context. + * + * @param scope The scope string to convert. + * @return A {@link Result} with the converted {@link Criterion}. + */ + Result transform(String scope); +} From de458b72c632703cf93ad616101ffac04ea0af1c Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Wed, 8 Nov 2023 17:09:59 +0100 Subject: [PATCH 5/9] cleanup --- .../edc/identityhub/core/CoreServicesExtension.java | 2 +- .../core/CredentialQueryResolverImpl.java | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java index 2afe833ef..900534dc4 100644 --- a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java @@ -98,7 +98,7 @@ public JwtVerifier getJwtVerifier() { } @Provider - public CredentialQueryResolver createCredentialQueryResolver(ServiceExtensionContext context) { + public CredentialQueryResolver createCredentialQueryResolver() { return new CredentialQueryResolverImpl(credentialStore, transformer); } diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java index b69581c49..e6867bd02 100644 --- a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java @@ -35,9 +35,7 @@ public class CredentialQueryResolverImpl implements CredentialQueryResolver { - private final CredentialStore credentialStore; - private final ScopeToCriterionTransformer scopeTransformer; public CredentialQueryResolverImpl(CredentialStore credentialStore, ScopeToCriterionTransformer scopeTransformer) { @@ -104,8 +102,15 @@ private Predicate credentialsPredicate(String type }; } - private Result> parseScopes(List query) { - var transformResult = query.stream() + /** + * 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> parseScopes(List scopes) { + var transformResult = scopes.stream() .map(scopeTransformer::transform) .toList(); From 4c26579c6e46ba30cba0b74dff3b23988714231c Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Wed, 8 Nov 2023 17:14:11 +0100 Subject: [PATCH 6/9] javadoc --- .../spi/resolution/QueryResult.java | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/QueryResult.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/QueryResult.java index a79ed2c1d..4cbbeea40 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/QueryResult.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/QueryResult.java @@ -19,16 +19,16 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Arrays; import java.util.List; import java.util.stream.Stream; import static org.eclipse.edc.identityhub.spi.resolution.QueryFailure.Reason.INVALID_SCOPE; -import static org.eclipse.edc.identityhub.spi.resolution.QueryFailure.Reason.OTHER; import static org.eclipse.edc.identityhub.spi.resolution.QueryFailure.Reason.STORAGE_FAILURE; import static org.eclipse.edc.identityhub.spi.resolution.QueryFailure.Reason.UNAUTHORIZED_SCOPE; - +/** + * Represents a query executed by the {@link CredentialQueryResolver} + */ public class QueryResult extends AbstractResult, QueryFailure, QueryResult> { protected QueryResult(Stream content, QueryFailure failure) { super(content, failure); @@ -46,26 +46,37 @@ public QueryFailure.Reason reason() { return (R1) new QueryResult(null, failure); } - public static QueryResult other(String... message) { - return new QueryResult(null, new QueryFailure(Arrays.asList(message), OTHER)); - } - + /** + * The query failed because no scope string was found + */ public static QueryResult noScopeFound(String message) { return new QueryResult(null, new QueryFailure(List.of(message), INVALID_SCOPE)); } + /** + * The query failed because the credential storage reported an error + */ public static QueryResult storageFailure(List failureMessages) { return new QueryResult(null, new QueryFailure(failureMessages, STORAGE_FAILURE)); } + /** + * The query failed, because the scope string was not valid (format, allowed values, etc.) + */ public static QueryResult invalidScope(List failureMessages) { return new QueryResult(null, new QueryFailure(failureMessages, INVALID_SCOPE)); } + /** + * The query failed because the query is unauthorized, e.g. by insufficiently broad scopes + */ public static QueryResult unauthorized(String failureMessage) { return new QueryResult(null, new QueryFailure(List.of(failureMessage), UNAUTHORIZED_SCOPE)); } + /** + * Query successful. List of credentials is in the content. + */ public static QueryResult success(Stream credentials) { return new QueryResult(credentials, null); } From b22b6db5030acdaf4942f2a48d07e61b5fb584ac Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Wed, 8 Nov 2023 17:14:48 +0100 Subject: [PATCH 7/9] DEPENDENCIES --- DEPENDENCIES | 7 ------- 1 file changed, 7 deletions(-) diff --git a/DEPENDENCIES b/DEPENDENCIES index 0ba760b25..8067c688b 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -314,19 +314,12 @@ maven/mavencentral/org.jetbrains/annotations/13.0, Apache-2.0, approved, clearly 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 From 027309acba9ca8b641005926baf2d28a4fc08447 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger <43503240+paullatzelsperger@users.noreply.github.com> Date: Thu, 9 Nov 2023 06:36:55 +0100 Subject: [PATCH 8/9] Update core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java Co-authored-by: Jim Marino --- .../org/eclipse/edc/identityhub/DefaultServicesExtension.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java index d4cb4c5ed..959c5bbdd 100644 --- a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java @@ -41,7 +41,7 @@ public PresentationGenerator createPresentationGenerator(ServiceExtensionContext @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!"); + "with a specialized implementation for your dataspace"); return new EdcScopeToCriterionTransformer(); } From e6544b07512b9c9c763419171305f66c1f585c6f Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Thu, 9 Nov 2023 07:48:38 +0100 Subject: [PATCH 9/9] pr remars --- .../core/CredentialQueryResolverImpl.java | 44 +++++-------------- .../core/CredentialQueryResolverImplTest.java | 14 ++++++ 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java index e6867bd02..a82fcd705 100644 --- a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java @@ -25,7 +25,6 @@ import org.eclipse.edc.spi.result.AbstractResult; import org.eclipse.edc.spi.result.Result; -import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; @@ -54,21 +53,25 @@ public QueryResult query(PresentationQuery query, List issuerScopes) { // check that all prover scopes are valid var proverScopeResult = parseScopes(query.getScopes()); - if (proverScopeResult.failed()) return QueryResult.invalidScope(proverScopeResult.getFailureMessages()); + 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()); + if (issuerScopeResult.failed()) { + return QueryResult.invalidScope(issuerScopeResult.getFailureMessages()); + } // query storage for requested credentials var queryspec = convertToQuerySpec(proverScopeResult.getContent()); - var res = credentialStore.query(queryspec); - if (res.failed()) { - return QueryResult.storageFailure(res.getFailureMessages()); + var credentialResult = credentialStore.query(queryspec); + if (credentialResult.failed()) { + return QueryResult.storageFailure(credentialResult.getFailureMessages()); } // the credentials requested by the other party - var requestedCredentials = res.getContent().toList(); + var requestedCredentials = credentialResult.getContent().toList(); // check that prover scope is not wider than issuer scope var issuerQuery = convertToQuerySpec(issuerScopeResult.getContent()); @@ -77,10 +80,8 @@ public QueryResult query(PresentationQuery query, List issuerScopes) { .reduce(Predicate::or) .orElse(x -> false); - // now narrow down the requested credentials to only contain allowed creds - var allowedCredentials = requestedCredentials.stream().filter(predicate).toList(); - - var isValidQuery = validateResults(new ArrayList<>(requestedCredentials), new ArrayList<>(allowedCredentials)); + // 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)) @@ -121,27 +122,6 @@ private Result> parseScopes(List scopes) { return success(transformResult.stream().map(AbstractResult::getContent).toList()); } - /** - * Checks whether the list of requested credentials is valid. Validity is determined by whether the list of requested credentials - * contains elements that are not in the list of allowed credentials. The list of allowed credentials may contain more elements, but not less. - * Every element, that is in the list of requested credentials must be found in the list of allowed credentials. - * - * @param requestedCredentials The list of requested credentials. - * @param allowedCredentials The list of allowed credentials. - * @return true if the list of requested credentials contains only elements that can be found in the list of allowed credentials, false otherwise. - */ - private boolean validateResults(List requestedCredentials, List allowedCredentials) { - if (requestedCredentials == allowedCredentials) { - return true; - } - if (requestedCredentials.size() > allowedCredentials.size()) { - return false; - } - - requestedCredentials.removeAll(allowedCredentials); - return requestedCredentials.isEmpty(); - } - private QuerySpec convertToQuerySpec(List criteria) { return QuerySpec.Builder.newInstance() .filter(criteria) diff --git a/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java index 47d290efa..009373758 100644 --- a/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java +++ b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java @@ -168,6 +168,20 @@ void query_requestedCredentialNotAllowed() { assertThat(res.getFailureDetail()).isEqualTo("Invalid query: requested Credentials outside of scope."); } + @Test + void query_sameSizeDifferentScope() { + var credential1 = createCredentialResource("TestCredential"); + var credential2 = createCredentialResource("AnotherCredential"); + when(storeMock.query(any())).thenReturn(success(Stream.of(credential1))); + + var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read", "org.eclipse.edc.vc.type:AnotherCredential:read"), + List.of("org.eclipse.edc.vc.type:FooCredential:read", "org.eclipse.edc.vc.type:BarCredential:read")); + + assertThat(res.failed()).isTrue(); + assertThat(res.reason()).isEqualTo(QueryFailure.Reason.UNAUTHORIZED_SCOPE); + assertThat(res.getFailureDetail()).isEqualTo("Invalid query: requested Credentials outside of scope."); + } + @Test void query_storeReturnsFailure() { var credential1 = createCredentialResource("TestCredential");