From 7c5e2e26d58cfea7de0503442da8e6a8704ee75d Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Tue, 24 Oct 2023 17:39:40 +0200 Subject: [PATCH] added controller tests --- core/identity-hub-api/build.gradle.kts | 4 + .../api/PresentationApiExtension.java | 14 +- .../api/v1/PresentationApi.java | 3 +- .../api/v1/PresentationApiController.java | 60 +++++- .../api/v1/PresentationApiControllerTest.java | 196 ++++++++++++++++++ gradle/libs.versions.toml | 10 +- spi/identity-hub-spi/build.gradle.kts | 1 + .../spi/generator/PresentationGenerator.java | 31 +++ .../spi/model/InputDescriptorMapping.java | 18 ++ .../spi/model/PresentationResponse.java | 19 ++ .../spi/model/PresentationSubmission.java | 24 +++ .../resolution/CredentialQueryResolver.java | 37 ++++ .../spi/verification/AccessTokenVerifier.java | 39 ++++ .../PresentationSubmissionSerDesTest.java | 69 ++++++ 14 files changed, 515 insertions(+), 10 deletions(-) create mode 100644 core/identity-hub-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java create mode 100644 spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationGenerator.java create mode 100644 spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/InputDescriptorMapping.java create mode 100644 spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/PresentationResponse.java create mode 100644 spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/PresentationSubmission.java create mode 100644 spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/CredentialQueryResolver.java create mode 100644 spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/verification/AccessTokenVerifier.java create mode 100644 spi/identity-hub-spi/src/test/java/org/eclipse/edc/identityhub/spi/model/PresentationSubmissionSerDesTest.java diff --git a/core/identity-hub-api/build.gradle.kts b/core/identity-hub-api/build.gradle.kts index 9cbdb1068..d04e8d3ba 100644 --- a/core/identity-hub-api/build.gradle.kts +++ b/core/identity-hub-api/build.gradle.kts @@ -20,10 +20,14 @@ plugins { dependencies { api(libs.edc.spi.jsonld) + api(libs.edc.spi.core) api(project(":spi:identity-hub-spi")) implementation(libs.edc.spi.validator) implementation(libs.edc.spi.web) implementation(libs.jakarta.rsApi) testImplementation(libs.edc.junit) testImplementation(libs.edc.ext.jsonld) + testImplementation(testFixtures(libs.edc.core.jersey)) + testImplementation(testFixtures(project(":spi:identity-hub-spi"))) + testImplementation(libs.nimbus.jwt) } diff --git a/core/identity-hub-api/src/main/java/org/eclipse/edc/identityservice/api/PresentationApiExtension.java b/core/identity-hub-api/src/main/java/org/eclipse/edc/identityservice/api/PresentationApiExtension.java index 9cbcf02f9..985af439c 100644 --- a/core/identity-hub-api/src/main/java/org/eclipse/edc/identityservice/api/PresentationApiExtension.java +++ b/core/identity-hub-api/src/main/java/org/eclipse/edc/identityservice/api/PresentationApiExtension.java @@ -14,7 +14,10 @@ package org.eclipse.edc.identityservice.api; +import org.eclipse.edc.identityhub.spi.generator.PresentationGenerator; import org.eclipse.edc.identityhub.spi.model.PresentationQuery; +import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver; +import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier; import org.eclipse.edc.identityservice.api.v1.PresentationApiController; import org.eclipse.edc.identityservice.api.validation.PresentationQueryValidator; import org.eclipse.edc.runtime.metamodel.annotation.Inject; @@ -35,10 +38,19 @@ public class PresentationApiExtension implements ServiceExtension { @Inject private WebService webService; + @Inject + private AccessTokenVerifier accessTokenVerifier; + + @Inject + private CredentialQueryResolver credentialResolver; + + @Inject + private PresentationGenerator presentationGenerator; + @Override public void initialize(ServiceExtensionContext context) { validatorRegistry.register(PresentationQuery.PRESENTATION_QUERY_TYPE_PROPERTY, new PresentationQueryValidator()); - var controller = new PresentationApiController(validatorRegistry, typeTransformer); + var controller = new PresentationApiController(validatorRegistry, typeTransformer, credentialResolver, accessTokenVerifier, presentationGenerator, context.getMonitor()); webService.registerResource("presentation", controller); } diff --git a/core/identity-hub-api/src/main/java/org/eclipse/edc/identityservice/api/v1/PresentationApi.java b/core/identity-hub-api/src/main/java/org/eclipse/edc/identityservice/api/v1/PresentationApi.java index ebc946250..25ced44fe 100644 --- a/core/identity-hub-api/src/main/java/org/eclipse/edc/identityservice/api/v1/PresentationApi.java +++ b/core/identity-hub-api/src/main/java/org/eclipse/edc/identityservice/api/v1/PresentationApi.java @@ -28,6 +28,7 @@ import io.swagger.v3.oas.annotations.security.SecurityScheme; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.json.JsonObject; +import jakarta.ws.rs.core.Response; import org.eclipse.edc.identityservice.api.v1.ApiSchema.ApiErrorDetailSchema; @OpenAPIDefinition( @@ -58,5 +59,5 @@ public interface PresentationApi { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetailSchema.class)), mediaType = "application/json")) } ) - JsonObject queryPresentation(JsonObject query); + Response queryPresentation(String authHeader, JsonObject query); } 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 4b1a5c524..7ba18fcec 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 @@ -15,13 +15,25 @@ package org.eclipse.edc.identityservice.api.v1; import jakarta.json.JsonObject; +import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import org.eclipse.edc.identityhub.spi.generator.PresentationGenerator; import org.eclipse.edc.identityhub.spi.model.PresentationQuery; +import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver; +import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; +import org.eclipse.edc.web.spi.ApiErrorDetail; +import org.eclipse.edc.web.spi.exception.AuthenticationFailedException; +import org.eclipse.edc.web.spi.exception.InvalidRequestException; +import org.eclipse.edc.web.spi.exception.NotAuthorizedException; import org.eclipse.edc.web.spi.exception.ValidationFailureException; +import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; import static org.eclipse.edc.identityhub.spi.model.PresentationQuery.PRESENTATION_QUERY_TYPE_PROPERTY; @@ -30,20 +42,60 @@ public class PresentationApiController implements PresentationApi { private final JsonObjectValidatorRegistry validatorRegistry; private final TypeTransformerRegistry transformerRegistry; + private final CredentialQueryResolver queryResolver; + private final AccessTokenVerifier accessTokenVerifier; + private final PresentationGenerator presentationGenerator; + private final Monitor monitor; - public PresentationApiController(JsonObjectValidatorRegistry validatorRegistry, TypeTransformerRegistry transformerRegistry) { + public PresentationApiController(JsonObjectValidatorRegistry validatorRegistry, TypeTransformerRegistry transformerRegistry, CredentialQueryResolver queryResolver, AccessTokenVerifier accessTokenVerifier, + PresentationGenerator presentationGenerator, Monitor monitor) { this.validatorRegistry = validatorRegistry; this.transformerRegistry = transformerRegistry; + this.queryResolver = queryResolver; + this.accessTokenVerifier = accessTokenVerifier; + this.presentationGenerator = presentationGenerator; + this.monitor = monitor; } + @POST @Path("query") @Override - public JsonObject queryPresentation(JsonObject query) { + public Response queryPresentation(@HeaderParam(AUTHORIZATION) String token, JsonObject query) { + if (token == null) { + throw new AuthenticationFailedException("Authorization header missing"); + } validatorRegistry.validate(PRESENTATION_QUERY_TYPE_PROPERTY, query).orElseThrow(ValidationFailureException::new); - var transformResult = transformerRegistry.transform(query, PresentationQuery.class); + var presentationQuery = transformerRegistry.transform(query, PresentationQuery.class).orElseThrow(InvalidRequestException::new); + + if (presentationQuery.getPresentationDefinition() != null) { + monitor.warning("DIF Presentation Queries are not supported yet. This will get implemented in future iterations."); + return notImplemented(); + } + + // verify and validate the requestor's SI token + var issuerScopes = accessTokenVerifier.verify(token).orElseThrow(f -> new AuthenticationFailedException("ID token verification failed: %s".formatted(f.getFailureDetail()))); + + // query the database + var credentials = queryResolver.query(presentationQuery, issuerScopes).orElseThrow(f -> new NotAuthorizedException(f.getFailureDetail())); - return null; + // package the credentials in a VP and sign + var presentationResponse = presentationGenerator.createPresentation(credentials, presentationQuery.getPresentationDefinition()) + .orElseThrow(failure -> new EdcException("Error creating VerifiablePresentation: %s".formatted(failure.getFailureDetail()))); + return Response.ok() + .entity(presentationResponse) + .build(); } + + private Response notImplemented() { + var error = ApiErrorDetail.Builder.newInstance() + .message("Not implemented.") + .type("Not implemented.") + .build(); + return Response.status(503) + .entity(error) + .build(); + } + } 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 new file mode 100644 index 000000000..97bda4371 --- /dev/null +++ b/core/identity-hub-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java @@ -0,0 +1,196 @@ +/* + * 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.identityservice.api.v1; + +import com.nimbusds.jwt.JWTClaimsSet; +import jakarta.json.JsonObject; +import org.eclipse.edc.identityhub.spi.generator.PresentationGenerator; +import org.eclipse.edc.identityhub.spi.model.InputDescriptorMapping; +import org.eclipse.edc.identityhub.spi.model.PresentationQuery; +import org.eclipse.edc.identityhub.spi.model.PresentationResponse; +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.verification.AccessTokenVerifier; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; +import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; +import org.eclipse.edc.web.spi.ApiErrorDetail; +import org.eclipse.edc.web.spi.exception.AuthenticationFailedException; +import org.eclipse.edc.web.spi.exception.InvalidRequestException; +import org.eclipse.edc.web.spi.exception.NotAuthorizedException; +import org.eclipse.edc.web.spi.exception.ValidationFailureException; +import org.junit.jupiter.api.Test; + +import java.sql.Date; +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +import static jakarta.json.Json.createObjectBuilder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.eclipse.edc.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.buildSignedJwt; +import static org.eclipse.edc.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.generateEcKey; +import static org.eclipse.edc.identityhub.spi.model.PresentationQuery.PRESENTATION_QUERY_TYPE_PROPERTY; +import static org.eclipse.edc.validator.spi.ValidationResult.failure; +import static org.eclipse.edc.validator.spi.ValidationResult.success; +import static org.eclipse.edc.validator.spi.Violation.violation; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@SuppressWarnings("resource") +class PresentationApiControllerTest extends RestControllerTestBase { + + private final JsonObjectValidatorRegistry validatorRegistryMock = mock(); + private final TypeTransformerRegistry typeTransformerRegistry = mock(); + private final CredentialQueryResolver queryResolver = mock(); + private final AccessTokenVerifier accessTokenVerifier = mock(); + private final PresentationGenerator generator = mock(); + + + @Test + void query_tokenNotPresent_shouldReturn401() { + assertThatThrownBy(() -> controller().queryPresentation(null, createObjectBuilder().build())) + .isInstanceOf(AuthenticationFailedException.class) + .hasMessage("Authorization header missing"); + } + + @Test + void query_validationError_shouldReturn400() { + when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(failure(violation("foo", "bar"))); + + assertThatThrownBy(() -> controller().queryPresentation(generateJwt(), createObjectBuilder().build())) + .isInstanceOf(ValidationFailureException.class) + .hasMessage("foo"); + } + + @Test + void query_transformationError_shouldReturn400() { + when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(success()); + when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.failure("cannot transform")); + + assertThatThrownBy(() -> controller().queryPresentation(generateJwt(), createObjectBuilder().build())) + .isInstanceOf(InvalidRequestException.class) + .hasMessage("cannot transform"); + verifyNoInteractions(accessTokenVerifier, queryResolver, generator); + } + + @Test + void query_withPresentationDefinition_shouldReturn501() { + when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(success()); + var presentationQueryBuilder = createPresentationQueryBuilder() + .presentationDefinition(PresentationDefinition.Builder.newInstance().id(UUID.randomUUID().toString()).build()); + when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.success(presentationQueryBuilder.build())); + + var response = controller().queryPresentation(generateJwt(), createObjectBuilder().build()); + assertThat(response.getStatus()).isEqualTo(503); + assertThat(response.getEntity()).extracting(o -> (ApiErrorDetail) o).satisfies(ed -> { + assertThat(ed.getMessage()).isEqualTo("Not implemented."); + assertThat(ed.getType()).isEqualTo("Not implemented."); + }); + verifyNoInteractions(accessTokenVerifier, queryResolver, generator); + } + + + @Test + void query_tokenVerificationFails_shouldReturn401() { + when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(success()); + var presentationQueryBuilder = createPresentationQueryBuilder().build(); + when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.success(presentationQueryBuilder)); + when(accessTokenVerifier.verify(anyString())).thenReturn(Result.failure("test-failure")); + + assertThatThrownBy(() -> controller().queryPresentation(generateJwt(), createObjectBuilder().build())) + .isExactlyInstanceOf(AuthenticationFailedException.class) + .hasMessage("ID token verification failed: test-failure"); + verifyNoInteractions(queryResolver, generator); + } + + @Test + void query_queryResolutionFails_shouldReturn403() { + when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(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.failure("test-failure")); + + assertThatThrownBy(() -> controller().queryPresentation(generateJwt(), createObjectBuilder().build())) + .isInstanceOf(NotAuthorizedException.class) + .hasMessage("test-failure"); + verifyNoInteractions(generator); + } + + @Test + void query_presentationGenerationFails_shouldReturn500() { + when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(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(generator.createPresentation(anyList(), any())).thenReturn(Result.failure("test-failure")); + + assertThatThrownBy(() -> controller().queryPresentation(generateJwt(), createObjectBuilder().build())) + .isExactlyInstanceOf(EdcException.class) + .hasMessage("Error creating VerifiablePresentation: test-failure"); + } + + @Test + void query_success() { + when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(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())); + + 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)); + + var response = controller().queryPresentation(generateJwt(), createObjectBuilder().build()); + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getEntity()).isEqualTo(pres); + + } + + @Override + protected PresentationApiController controller() { + return new PresentationApiController(validatorRegistryMock, typeTransformerRegistry, queryResolver, accessTokenVerifier, generator, mock()); + } + + private String generateJwt() { + var ecKey = generateEcKey(); + var jwt = buildSignedJwt(new JWTClaimsSet.Builder().audience("test-audience") + .expirationTime(Date.from(Instant.now().plusSeconds(3600))) + .issuer("test-issuer") + .subject("test-subject") + .jwtID(UUID.randomUUID().toString()).build(), ecKey); + + return jwt.serialize(); + } + + private PresentationQuery.Builder createPresentationQueryBuilder() { + return PresentationQuery.Builder.newinstance() + .scopes(List.of("test-scope1", "test-scope2")); + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9e796b71e..179979a9d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,15 +27,17 @@ edc-spi-transform = { module = "org.eclipse.edc:transform-spi", version.ref = "e edc-spi-transaction-datasource = { module = "org.eclipse.edc:transaction-datasource-spi", version.ref = "edc" } edc-spi-identity-did = { module = "org.eclipse.edc:identity-did-spi", version.ref = "edc" } edc-spi-aggregate-service = { module = "org.eclipse.edc:aggregate-service-spi", version.ref = "edc" } -edc-spi-jsonld = { module="org.eclipse.edc:json-ld-spi", version.ref= "edc"} -edc-spi-validator = { module="org.eclipse.edc:validator-spi", version.ref= "edc"} -edc-spi-web = { module="org.eclipse.edc:web-spi", version.ref= "edc"} +edc-spi-jsonld = { module = "org.eclipse.edc:json-ld-spi", version.ref = "edc" } +edc-spi-validator = { module = "org.eclipse.edc:validator-spi", version.ref = "edc" } +edc-spi-web = { module = "org.eclipse.edc:web-spi", version.ref = "edc" } +edc-spi-identitytrust = { module = "org.eclipse.edc:identity-trust-spi", version.ref = "edc" } edc-core-connector = { module = "org.eclipse.edc:connector-core", version.ref = "edc" } edc-core-controlPlane = { module = "org.eclipse.edc:control-plane-core", version.ref = "edc" } edc-core-micrometer = { module = "org.eclipse.edc:micrometer-core", version.ref = "edc" } edc-core-api = { module = "org.eclipse.edc:api-core", version.ref = "edc" } edc-core-stateMachine = { module = "org.eclipse.edc:state-machine", version.ref = "edc" } edc-core-sql = { module = "org.eclipse.edc:sql-core", version.ref = "edc" } +edc-core-jersey = { module = "org.eclipse.edc:jersey-core", version.ref = "edc" } edc-core-junit = { module = "org.eclipse.edc:junit", version.ref = "edc" } edc-core-transform = { module = "org.eclipse.edc:transform-core", version.ref = "edc" } edc-ext-identity-did-crypto = { module = "org.eclipse.edc:identity-did-crypto", version.ref = "edc" } @@ -58,7 +60,7 @@ swagger-jaxrs = { module = "io.swagger.core.v3:swagger-jaxrs2-jakarta", version. jakarta-rsApi = { module = "jakarta.ws.rs:jakarta.ws.rs-api", version.ref = "rsApi" } [bundles] -connector = [ "edc-boot", "edc-core-connector", "edc-ext-http", "edc-ext-observability", "edc-ext-jsonld"] +connector = ["edc-boot", "edc-core-connector", "edc-ext-http", "edc-ext-observability", "edc-ext-jsonld"] [plugins] shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" } diff --git a/spi/identity-hub-spi/build.gradle.kts b/spi/identity-hub-spi/build.gradle.kts index caa66f68b..a9663e4ae 100644 --- a/spi/identity-hub-spi/build.gradle.kts +++ b/spi/identity-hub-spi/build.gradle.kts @@ -22,6 +22,7 @@ val swagger: String by project dependencies { + api(libs.edc.spi.identitytrust) implementation(libs.jackson.databind) implementation(libs.nimbus.jwt) implementation(libs.edc.spi.identity.did) diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationGenerator.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationGenerator.java new file mode 100644 index 000000000..b2cfc64d4 --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationGenerator.java @@ -0,0 +1,31 @@ +/* + * 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.generator; + +import org.eclipse.edc.identityhub.spi.model.PresentationResponse; +import org.eclipse.edc.identityhub.spi.model.presentationdefinition.PresentationDefinition; +import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; +import org.eclipse.edc.spi.result.Result; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * Represents a Presentation Generator that creates a presentation based on a list of verifiable credentials + * and an optional presentation definition. + */ +public interface PresentationGenerator { + Result createPresentation(List credentials, @Nullable PresentationDefinition presentationDefinition); +} diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/InputDescriptorMapping.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/InputDescriptorMapping.java new file mode 100644 index 000000000..0558d51bc --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/InputDescriptorMapping.java @@ -0,0 +1,18 @@ +/* + * 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.model; + +public record InputDescriptorMapping(String id, String format, String path) { +} diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/PresentationResponse.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/PresentationResponse.java new file mode 100644 index 000000000..e22fc5e1b --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/PresentationResponse.java @@ -0,0 +1,19 @@ +/* + * 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.model; + + +public record PresentationResponse(String vpToken, PresentationSubmission presentationSubmission) { +} diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/PresentationSubmission.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/PresentationSubmission.java new file mode 100644 index 000000000..64e5c6aa0 --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/PresentationSubmission.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.spi.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public record PresentationSubmission(@JsonProperty("id") String id, + @JsonProperty("definition_id") String definitionId, + @JsonProperty("descriptor_map") List descriptorMap) { +} 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 new file mode 100644 index 000000000..66fdd1493 --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/CredentialQueryResolver.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.resolution; + +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; + +/** + * Resolves a list of {@link VerifiableCredentialContainer} objects based on an incoming {@link PresentationQuery} and a list of scope strings. + */ +public interface CredentialQueryResolver { + + /** + * Query method for fetching credentials. If this method returns a successful result, it will contain a list of {@link VerifiableCredentialContainer}. + * If a failure is returned, that means that the given query does not match the given issuer scopes, which would be equivalent to an unauthorized access (c.f. HTTP 403 error). + * The Result could also contain information about any errors or issues the occurred during the query execution. + * + * @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); +} \ No newline at end of file diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/verification/AccessTokenVerifier.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/verification/AccessTokenVerifier.java new file mode 100644 index 000000000..539e63feb --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/verification/AccessTokenVerifier.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.verification; + +import org.eclipse.edc.spi.result.Result; + +import java.util.List; + +/** + * The AccessTokenVerifier interface represents a verifier for Self-Issued JWT tokens. It takes a base64-encoded ID token. + */ +public interface AccessTokenVerifier { + /** + * Performs the verification on a self-issued ID token, asserting the following aspects: + *
    + *
  • iss == aud
  • + *
  • aud == the Verifiers own DID. In practice, this will be the DID of the participant agent (i.e. the connector)
  • + *
  • the token contains an {@code access_token} claim, and that it is also in JWT format
  • + *
  • access_token.sub == sub
  • + *
  • that the access_token contains >1 scope strings
  • + *
+ * + * @param token The token to be verified. Must be a JWT in base64 encoding. + * @return A {@code Result} containing a {@code List} of scope strings. + */ + Result> verify(String token); +} diff --git a/spi/identity-hub-spi/src/test/java/org/eclipse/edc/identityhub/spi/model/PresentationSubmissionSerDesTest.java b/spi/identity-hub-spi/src/test/java/org/eclipse/edc/identityhub/spi/model/PresentationSubmissionSerDesTest.java new file mode 100644 index 000000000..5aade13e4 --- /dev/null +++ b/spi/identity-hub-spi/src/test/java/org/eclipse/edc/identityhub/spi/model/PresentationSubmissionSerDesTest.java @@ -0,0 +1,69 @@ +/* + * 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.model; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class PresentationSubmissionSerDesTest { + private final ObjectMapper mapper = new ObjectMapper(); + + @Test + void verifyDeserialization() throws JsonProcessingException { + var json = """ + { + "id": "a30e3b91-fb77-4d22-95fa-871689c322e2", + "definition_id": "32f54163-7166-48f1-93d8-ff217bdb0653", + "descriptor_map": [ + { + "id": "banking_input_2", + "format": "jwt_vc", + "path": "$.verifiableCredential[0]" + }, + { + "id": "employment_input", + "format": "ldp_vc", + "path": "$.verifiableCredential[1]" + }, + { + "id": "citizenship_input_1", + "format": "ldp_vc", + "path": "$.verifiableCredential[2]" + } + ] + } + """; + var pd = mapper.readValue(json, PresentationSubmission.class); + assertThat(pd).isNotNull(); + + assertThat(pd.id()).isEqualTo("a30e3b91-fb77-4d22-95fa-871689c322e2"); + assertThat(pd.definitionId()).isEqualTo("32f54163-7166-48f1-93d8-ff217bdb0653"); + assertThat(pd.descriptorMap()).hasSize(3); + } + + @Test + void verifySerialization() throws JsonProcessingException { + var pd = new PresentationSubmission("test-id", "test-def-id", List.of(new InputDescriptorMapping("test-input", "ldp_vc", "$.verifiableCredentials[0]"))); + var json = mapper.writeValueAsString(pd); + + var deser = mapper.readValue(json, PresentationSubmission.class); + assertThat(deser).usingRecursiveComparison().isEqualTo(pd); + } +} \ No newline at end of file