From 9f0a2f88c66c07a960fdf343f22e53837b4be15c Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger <43503240+paullatzelsperger@users.noreply.github.com> Date: Thu, 9 Nov 2023 08:56:16 +0100 Subject: [PATCH] feat: implement CredentialQueryResolver (#168) * add possibility to supply PublicKey as config value * added QueryResolverImpl + Test * add storageresult class * made scope-to-criterion conversion pluggable * cleanup * javadoc * DEPENDENCIES * Update core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java Co-authored-by: Jim Marino * pr remars --------- Co-authored-by: Jim Marino --- DEPENDENCIES | 7 - .../api/v1/PresentationApiController.java | 2 +- .../api/v1/PresentationApiControllerTest.java | 9 +- .../identityhub/DefaultServicesExtension.java | 17 +- .../core/CoreServicesExtension.java | 14 ++ .../core/CredentialQueryResolverImpl.java | 130 +++++++++++ .../EdcScopeToCriterionTransformer.java | 73 ++++++ .../core/CredentialQueryResolverImplTest.java | 216 ++++++++++++++++++ .../EdcScopeToCriterionTransformerTest.java | 47 ++++ ...t.java => ResolutionApiComponentTest.java} | 14 +- .../resolver/PublicKeyWrapperExtension.java | 8 + .../spi/ScopeToCriterionTransformer.java | 37 +++ .../resolution/CredentialQueryResolver.java | 3 +- .../spi/resolution/QueryFailure.java | 39 ++++ .../spi/resolution/QueryResult.java | 84 +++++++ 15 files changed, 674 insertions(+), 26 deletions(-) create mode 100644 core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java 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/core/CredentialQueryResolverImplTest.java create mode 100644 core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/defaults/EdcScopeToCriterionTransformerTest.java rename e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/{ResolutionApiEndToEndTest.java => ResolutionApiComponentTest.java} (95%) create mode 100644 spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/ScopeToCriterionTransformer.java 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/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 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..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; @@ -41,6 +42,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; @@ -48,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; @@ -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) @@ -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")); @@ -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)); 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..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 @@ -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; @@ -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(); + } + } 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..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 @@ -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; @@ -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 @@ -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); } 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..a82fcd705 --- /dev/null +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java @@ -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 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 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> parseScopes(List 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 criteria) { + return QuerySpec.Builder.newInstance() + .filter(criteria) + .build(); + } +} 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 new file mode 100644 index 000000000..009373758 --- /dev/null +++ b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java @@ -0,0 +1,216 @@ +/* + * 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.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; +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, new EdcScopeToCriterionTransformer()); + + @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.reason()).isEqualTo(QueryFailure.Reason.INVALID_SCOPE); + 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.reason()).isEqualTo(QueryFailure.Reason.INVALID_SCOPE); + assertThat(res.getFailureDetail()).contains("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.reason()).isEqualTo(QueryFailure.Reason.INVALID_SCOPE); + assertThat(res.getFailureDetail()).contains("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.reason()).isEqualTo(QueryFailure.Reason.UNAUTHORIZED_SCOPE); + 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.reason()).isEqualTo(QueryFailure.Reason.UNAUTHORIZED_SCOPE); + 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"); + 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.reason()).isEqualTo(QueryFailure.Reason.STORAGE_FAILURE); + 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/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/ResolutionApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ResolutionApiComponentTest.java similarity index 95% 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..3296fac04 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 @@ -20,16 +20,18 @@ 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; -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 +46,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": [ @@ -156,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) @@ -174,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(List.of())); + 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() @@ -191,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(List.of())); + 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/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)); } 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); +} 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..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,7 +16,6 @@ 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; @@ -33,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..4cbbeea40 --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/QueryResult.java @@ -0,0 +1,84 @@ +/* + * 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.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.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); + } + + 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); + } + + /** + * 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); + } + +}