From 8e1123225a2da260b5a676a3a4b2aa3d5babca78 Mon Sep 17 00:00:00 2001 From: Benjamin SCHOLTES Date: Mon, 26 Aug 2024 10:50:18 +0200 Subject: [PATCH] feat: Credential create/update API --- DEPENDENCIES | 2 +- .../VerifiableCredentialApiEndToEndTest.java | 173 ++++++ .../IdentityHubEndToEndTestContext.java | 39 ++ .../main/resources/identity-api-version.json | 2 +- .../v1/unstable/KeyPairResourceApi.java | 17 +- .../identity-api/validators/build.gradle.kts | 1 + ...VerifiableCredentialManifestValidator.java | 48 ++ ...fiableCredentialManifestValidatorTest.java | 98 ++++ .../build.gradle.kts | 2 + .../VerifiableCredentialApiExtension.java | 9 +- .../v1/unstable/VerifiableCredentialsApi.java | 52 +- .../VerifiableCredentialsApiController.java | 47 +- ...rifiableCredentialResourceTransformer.java | 51 ++ ...erifiableCredentialsApiControllerTest.java | 491 +++++++++++++----- ...ableCredentialResourceTransformerTest.java | 64 +++ .../model/IdentityResource.java | 2 +- .../model/VerifiableCredentialManifest.java | 124 +++++ .../VerifiableCredentialManifestTest.java | 53 ++ 18 files changed, 1108 insertions(+), 167 deletions(-) create mode 100644 e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/VerifiableCredentialApiEndToEndTest.java create mode 100644 extensions/api/identity-api/validators/src/main/java/org/eclipse/edc/identityhub/api/v1/validation/VerifiableCredentialManifestValidator.java create mode 100644 extensions/api/identity-api/validators/src/test/java/org/eclipse/edc/identityhub/api/v1/validation/VerifiableCredentialManifestValidatorTest.java create mode 100644 extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/transformer/VerifiableCredentialManifestToVerifiableCredentialResourceTransformer.java create mode 100644 extensions/api/identity-api/verifiable-credentials-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/transformer/VerifiableCredentialManifestToVerifiableCredentialResourceTransformerTest.java create mode 100644 spi/verifiable-credential-spi/src/main/java/org/eclipse/edc/identityhub/spi/verifiablecredentials/model/VerifiableCredentialManifest.java create mode 100644 spi/verifiable-credential-spi/src/test/java/org/eclipse/edc/identityhub/spi/verifiablecredentials/model/VerifiableCredentialManifestTest.java diff --git a/DEPENDENCIES b/DEPENDENCIES index da7042112..13a742fb1 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -370,7 +370,7 @@ maven/mavencentral/org.ow2.asm/asm-commons/9.7, BSD-3-Clause, approved, #14075 maven/mavencentral/org.ow2.asm/asm-tree/9.7, BSD-3-Clause, approved, #14073 maven/mavencentral/org.ow2.asm/asm/9.1, BSD-3-Clause, approved, CQ23029 maven/mavencentral/org.ow2.asm/asm/9.7, BSD-3-Clause, approved, #14076 -maven/mavencentral/org.postgresql/postgresql/42.7.3, BSD-2-Clause AND Apache-2.0, approved, #11681 +maven/mavencentral/org.postgresql/postgresql/42.7.4, BSD-2-Clause AND Apache-2.0, approved, #11681 maven/mavencentral/org.reflections/reflections/0.10.2, Apache-2.0 AND WTFPL, approved, clearlydefined maven/mavencentral/org.rnorth.duct-tape/duct-tape/1.0.8, MIT, approved, clearlydefined maven/mavencentral/org.slf4j/slf4j-api/1.7.22, MIT, approved, CQ11943 diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/VerifiableCredentialApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/VerifiableCredentialApiEndToEndTest.java new file mode 100644 index 000000000..36b16aebd --- /dev/null +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/VerifiableCredentialApiEndToEndTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2024 Amadeus IT Group. + * + * 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: + * Amadeus IT Group - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.tests; + +import io.restassured.http.Header; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredentialContainer; +import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService; +import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialManifest; +import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndExtension; +import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubEndToEndTestContext; +import org.eclipse.edc.junit.annotations.EndToEndTest; +import org.eclipse.edc.junit.annotations.PostgresqlIntegrationTest; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.query.QuerySpec; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.Arrays; +import java.util.Base64; +import java.util.UUID; + +import static io.restassured.http.ContentType.JSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.notNullValue; + +public class VerifiableCredentialApiEndToEndTest { + + abstract static class Tests { + + @AfterEach + void tearDown(ParticipantContextService store) { + // purge all users + store.query(QuerySpec.max()).getContent() + .forEach(pc -> store.deleteParticipantContext(pc.getParticipantId()).getContent()); + } + + @Test + void findById(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + var user = "user1"; + var token = context.createParticipant(user); + + var credential = context.createCredential(); + var resourceId = context.storeCredential(credential, user); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .get("/v1alpha/participants/%s/credentials/%s".formatted(toBase64(user), resourceId)) + .then() + .log().ifValidationFails() + .statusCode(200) + .body(notNullValue())); + } + + @Test + void create(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + var user = "user1"; + var token = context.createParticipant(user); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> { + var vc = context.createCredential(); + var resourceId = UUID.randomUUID().toString(); + var manifest = createManifest(user, vc).id(resourceId).build(); + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .body(manifest) + .post("/v1alpha/participants/%s/credentials".formatted(toBase64(user))) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(notNullValue()); + + var resource = context.getCredential(resourceId).orElseThrow(() -> new EdcException("Failed to credential with id %s".formatted(resourceId))); + assertThat(resource.getVerifiableCredential().credential()).usingRecursiveComparison().isEqualTo(vc); + }); + } + + @Test + void update(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + var user = "user1"; + var token = context.createParticipant(user); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> { + var credential1 = context.createCredential(); + var credential2 = context.createCredential(); + var resourceId1 = context.storeCredential(credential1, user); + var manifest = createManifest(user, credential2).id(resourceId1).build(); + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .body(manifest) + .put("/v1alpha/participants/%s/credentials".formatted(toBase64(user))) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(notNullValue()); + + var resource = context.getCredential(resourceId1).orElseThrow(() -> new EdcException("Failed to retrieve credential with id %s".formatted(resourceId1))); + assertThat(resource.getVerifiableCredential().credential()).usingRecursiveComparison().isEqualTo(credential2); + }); + } + + @Test + void delete(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + var user = "user1"; + var token = context.createParticipant(user); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> { + var credential = context.createCredential(); + var resourceId = context.storeCredential(credential, user); + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .delete("/v1alpha/participants/%s/credentials/%s".formatted(toBase64(user), resourceId)) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(notNullValue()); + + var resource = context.getCredential(resourceId); + assertThat(resource.isEmpty()).isTrue(); + }); + } + + private String toBase64(String s) { + return Base64.getUrlEncoder().encodeToString(s.getBytes()); + } + + private VerifiableCredentialManifest.Builder createManifest(String participantId, VerifiableCredential vc) { + return VerifiableCredentialManifest.Builder.newInstance() + .verifiableCredentialContainer(new VerifiableCredentialContainer("rawVc", CredentialFormat.JWT, vc)) + .participantId(participantId); + } + + } + + @Nested + @EndToEndTest + @ExtendWith(IdentityHubEndToEndExtension.InMemory.class) + class InMemory extends Tests { + } + + @Nested + @PostgresqlIntegrationTest + @ExtendWith(IdentityHubEndToEndExtension.Postgres.class) + class Postgres extends Tests { + } +} diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubEndToEndTestContext.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubEndToEndTestContext.java index def395b09..94a9bc5c7 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubEndToEndTestContext.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/IdentityHubEndToEndTestContext.java @@ -17,6 +17,11 @@ import com.nimbusds.jose.jwk.Curve; import org.eclipse.edc.iam.did.spi.document.DidDocument; import org.eclipse.edc.iam.did.spi.document.Service; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSubject; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.Issuer; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredentialContainer; import org.eclipse.edc.identithub.spi.did.DidDocumentService; import org.eclipse.edc.identityhub.participantcontext.ApiTokenGenerator; import org.eclipse.edc.identityhub.spi.authentication.ServicePrincipal; @@ -27,17 +32,22 @@ import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantManifest; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantResource; +import org.eclipse.edc.identityhub.spi.store.CredentialStore; import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore; import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore; +import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VcStatus; +import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialResource; import org.eclipse.edc.junit.extensions.EmbeddedRuntime; import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.security.Vault; +import java.time.Instant; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; /** @@ -63,6 +73,28 @@ public String createParticipant(String participantId) { return createParticipant(participantId, List.of()); } + public VerifiableCredential createCredential() { + return VerifiableCredential.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .type("test-type") + .issuanceDate(Instant.now()) + .issuer(new Issuer("did:web:issuer")) + .credentialSubject(CredentialSubject.Builder.newInstance().id("id").claim("foo", "bar").build()) + .build(); + } + + public String storeCredential(VerifiableCredential credential, String participantId) { + var resource = VerifiableCredentialResource.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .state(VcStatus.ISSUED) + .participantId(participantId) + .holderId("holderId") + .issuerId("issuerId") + .credential(new VerifiableCredentialContainer("rawVc", CredentialFormat.JWT, credential)) + .build(); + runtime.getService(CredentialStore.class).create(resource).orElseThrow(f -> new EdcException(f.getFailureDetail())); + return resource.getId(); + } public String createParticipant(String participantId, List roles) { var manifest = ParticipantManifest.Builder.newInstance() @@ -161,4 +193,11 @@ public ParticipantContext getParticipant(String participantId) { .orElseThrow(f -> new EdcException(f.getFailureDetail())); } + public Optional getCredential(String credentialId) { + return runtime.getService(CredentialStore.class) + .query(QuerySpec.Builder.newInstance().filter(new Criterion("id", "=", credentialId)).build()) + .orElseThrow(f -> new EdcException(f.getFailureDetail())) + .stream().findFirst(); + } + } diff --git a/extensions/api/identity-api/api-configuration/src/main/resources/identity-api-version.json b/extensions/api/identity-api/api-configuration/src/main/resources/identity-api-version.json index e1cd4bdad..aeb66133d 100644 --- a/extensions/api/identity-api/api-configuration/src/main/resources/identity-api-version.json +++ b/extensions/api/identity-api/api-configuration/src/main/resources/identity-api-version.json @@ -2,7 +2,7 @@ { "version": "1.0.0-alpha", "urlPath": "/v1alpha", - "lastUpdated": "2024-08-22T09:20:00Z", + "lastUpdated": "2024-08-27T11:00:00Z", "maturity": null } ] \ No newline at end of file diff --git a/extensions/api/identity-api/keypair-api/src/main/java/org/eclipse/edc/identityhub/api/keypair/v1/unstable/KeyPairResourceApi.java b/extensions/api/identity-api/keypair-api/src/main/java/org/eclipse/edc/identityhub/api/keypair/v1/unstable/KeyPairResourceApi.java index 2a3f18b10..663734cda 100644 --- a/extensions/api/identity-api/keypair-api/src/main/java/org/eclipse/edc/identityhub/api/keypair/v1/unstable/KeyPairResourceApi.java +++ b/extensions/api/identity-api/keypair-api/src/main/java/org/eclipse/edc/identityhub/api/keypair/v1/unstable/KeyPairResourceApi.java @@ -28,7 +28,6 @@ import jakarta.ws.rs.core.SecurityContext; import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource; import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyDescriptor; -import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; import org.eclipse.edc.web.spi.ApiErrorDetail; import java.util.Collection; @@ -44,7 +43,7 @@ public interface KeyPairResourceApi { }, responses = { @ApiResponse(responseCode = "200", description = "The KeyPairResource.", - content = @Content(schema = @Schema(implementation = ParticipantContext.class))), + content = @Content(schema = @Schema(implementation = KeyPairResource.class))), @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", @@ -59,7 +58,7 @@ public interface KeyPairResourceApi { operationId = "queryKeyPairByParticipantId", responses = { @ApiResponse(responseCode = "200", description = "The KeyPairResource.", - content = @Content(schema = @Schema(implementation = ParticipantContext.class))), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = KeyPairResource.class)))), @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", @@ -75,8 +74,7 @@ public interface KeyPairResourceApi { requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = KeyDescriptor.class), mediaType = "application/json")), parameters = @Parameter(name = "makeDefault", description = "Make the new key pair the default key pair"), responses = { - @ApiResponse(responseCode = "200", description = "The KeyPairResource was successfully created and linked to the participant.", - content = @Content(schema = @Schema(implementation = ParticipantContext.class))), + @ApiResponse(responseCode = "200", description = "The KeyPairResource was successfully created and linked to the participant."), @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", @@ -94,8 +92,7 @@ public interface KeyPairResourceApi { @Parameter(name = "participantId", description = "Base64-Url encode Participant Context ID", required = true, in = ParameterIn.PATH) }, responses = { - @ApiResponse(responseCode = "200", description = "The KeyPairResource.", - content = @Content(schema = @Schema(implementation = ParticipantContext.class))), + @ApiResponse(responseCode = "200", description = "The KeyPairResource."), @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed.", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", @@ -114,8 +111,7 @@ public interface KeyPairResourceApi { @Parameter(name = "participantId", description = "Base64-Url encode Participant Context ID", required = true, in = ParameterIn.PATH) }, responses = { - @ApiResponse(responseCode = "200", description = "The KeyPairResource was successfully rotated and linked to the participant.", - content = @Content(schema = @Schema(implementation = ParticipantContext.class))), + @ApiResponse(responseCode = "200", description = "The KeyPairResource was successfully rotated and linked to the participant."), @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", @@ -133,8 +129,7 @@ public interface KeyPairResourceApi { }, requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = KeyDescriptor.class), mediaType = "application/json")), responses = { - @ApiResponse(responseCode = "200", description = "The KeyPairResource was successfully rotated and linked to the participant.", - content = @Content(schema = @Schema(implementation = ParticipantContext.class))), + @ApiResponse(responseCode = "200", description = "The KeyPairResource was successfully rotated and linked to the participant."), @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", diff --git a/extensions/api/identity-api/validators/build.gradle.kts b/extensions/api/identity-api/validators/build.gradle.kts index aee3def03..42639e04b 100644 --- a/extensions/api/identity-api/validators/build.gradle.kts +++ b/extensions/api/identity-api/validators/build.gradle.kts @@ -7,6 +7,7 @@ dependencies { api(libs.edc.spi.core) api(project(":spi:identity-hub-spi")) api(project(":spi:did-spi")) + api(project(":spi:verifiable-credential-spi")) implementation(libs.edc.lib.util) testImplementation(libs.edc.junit) diff --git a/extensions/api/identity-api/validators/src/main/java/org/eclipse/edc/identityhub/api/v1/validation/VerifiableCredentialManifestValidator.java b/extensions/api/identity-api/validators/src/main/java/org/eclipse/edc/identityhub/api/v1/validation/VerifiableCredentialManifestValidator.java new file mode 100644 index 000000000..d56ddced9 --- /dev/null +++ b/extensions/api/identity-api/validators/src/main/java/org/eclipse/edc/identityhub/api/v1/validation/VerifiableCredentialManifestValidator.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 Amadeus IT Group + * + * 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: + * Amadeus IT Group - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.api.v1.validation; + +import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialManifest; +import org.eclipse.edc.validator.spi.ValidationResult; +import org.eclipse.edc.validator.spi.Validator; + +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; + +public class VerifiableCredentialManifestValidator implements Validator { + @Override + public ValidationResult validate(VerifiableCredentialManifest input) { + if (input == null) { + return failure(violation("Input was null", ".")); + } + + if (input.getParticipantId() == null) { + return failure(violation("Participant id was null", "participantId")); + } + + var container = input.getVerifiableCredentialContainer(); + if (container == null) { + return failure(violation("VerifiableCredentialContainer was null", "credential")); + } + + var credential = container.credential(); + if (credential == null) { + return failure(violation("VerifiableCredential was null", "credential.credential")); + } + + return success(); + } +} diff --git a/extensions/api/identity-api/validators/src/test/java/org/eclipse/edc/identityhub/api/v1/validation/VerifiableCredentialManifestValidatorTest.java b/extensions/api/identity-api/validators/src/test/java/org/eclipse/edc/identityhub/api/v1/validation/VerifiableCredentialManifestValidatorTest.java new file mode 100644 index 000000000..a3131b3af --- /dev/null +++ b/extensions/api/identity-api/validators/src/test/java/org/eclipse/edc/identityhub/api/v1/validation/VerifiableCredentialManifestValidatorTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2024 Amadeus IT Group + * + * 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: + * Amadeus IT Group - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.api.v1.validation; + +import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSubject; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.Issuer; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredentialContainer; +import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialManifest; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.UUID; + +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; + +class VerifiableCredentialManifestValidatorTest { + + private final VerifiableCredentialManifestValidator validator = new VerifiableCredentialManifestValidator(); + + @Test + void validManifest_shouldPassValidation() { + var manifest = VerifiableCredentialManifest.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .participantId(UUID.randomUUID().toString()) + .verifiableCredentialContainer(new VerifiableCredentialContainer("rawVc", CredentialFormat.JWT, VerifiableCredential.Builder.newInstance() + .type("type") + .credentialSubject(CredentialSubject.Builder.newInstance() + .id("id") + .claim("key", "value") + .build()) + .issuer(new Issuer("issuer")) + .issuanceDate(Instant.now()) + .build())) + .build(); + + var result = validator.validate(manifest); + + assertThat(result).isSucceeded(); + } + + @Test + void validate_missingVerifiableCredentialContainer_shouldFailValidation() { + var manifest = VerifiableCredentialManifest.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .participantId(UUID.randomUUID().toString()) + .build(); + + var result = validator.validate(manifest); + + assertThat(result).isFailed().withFailMessage("VerifiableCredentialContainer was null"); + } + + @Test + void validate_missingParticipantId_shouldFailValidation() { + var manifest = VerifiableCredentialManifest.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .build(); + + var result = validator.validate(manifest); + + assertThat(result).isFailed().withFailMessage("Participant id was null"); + } + + @Test + void validate_missingVerifiableCredential_shouldFailValidation() { + var manifest = VerifiableCredentialManifest.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .participantId(UUID.randomUUID().toString()) + .verifiableCredentialContainer(new VerifiableCredentialContainer("rawVc", CredentialFormat.JWT, null)) + .build(); + + var result = validator.validate(manifest); + + assertThat(result).isFailed().withFailMessage("VerifiableCredential was null"); + } + + @Test + void validate_nullManifest_shouldFailValidation() { + var result = validator.validate(null); + + assertThat(result).isFailed().withFailMessage("Input was null"); + } + +} \ No newline at end of file diff --git a/extensions/api/identity-api/verifiable-credentials-api/build.gradle.kts b/extensions/api/identity-api/verifiable-credentials-api/build.gradle.kts index b7b2376bb..389180b30 100644 --- a/extensions/api/identity-api/verifiable-credentials-api/build.gradle.kts +++ b/extensions/api/identity-api/verifiable-credentials-api/build.gradle.kts @@ -10,6 +10,8 @@ dependencies { api(project(":spi:identity-hub-store-spi")) api(project(":spi:verifiable-credential-spi")) implementation(project(":extensions:api:identity-api:api-configuration")) + implementation(project(":extensions:api:identity-api:validators")) + implementation(libs.edc.spi.transform) implementation(libs.edc.spi.web) implementation(libs.edc.lib.util) implementation(libs.jakarta.rsApi) diff --git a/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/VerifiableCredentialApiExtension.java b/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/VerifiableCredentialApiExtension.java index a74a7428a..1c3ef140b 100644 --- a/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/VerifiableCredentialApiExtension.java +++ b/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/VerifiableCredentialApiExtension.java @@ -14,8 +14,10 @@ package org.eclipse.edc.identityhub.api.verifiablecredentials; +import org.eclipse.edc.identityhub.api.v1.validation.VerifiableCredentialManifestValidator; import org.eclipse.edc.identityhub.api.verifiablecredentials.v1.unstable.GetAllCredentialsApiController; import org.eclipse.edc.identityhub.api.verifiablecredentials.v1.unstable.VerifiableCredentialsApiController; +import org.eclipse.edc.identityhub.api.verifiablecredentials.v1.unstable.transformer.VerifiableCredentialManifestToVerifiableCredentialResourceTransformer; import org.eclipse.edc.identityhub.spi.AuthorizationService; import org.eclipse.edc.identityhub.spi.IdentityHubApiContext; import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantResource; @@ -28,6 +30,7 @@ import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.eclipse.edc.web.spi.WebService; import static org.eclipse.edc.identityhub.api.verifiablecredentials.VerifiableCredentialApiExtension.NAME; @@ -36,6 +39,8 @@ public class VerifiableCredentialApiExtension implements ServiceExtension { public static final String NAME = "VerifiableCredentials API Extension"; + @Inject + TypeTransformerRegistry typeTransformerRegistry; @Inject private WebService webService; @Inject @@ -51,7 +56,9 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { authorizationService.addLookupFunction(VerifiableCredentialResource.class, this::queryById); - var controller = new VerifiableCredentialsApiController(credentialStore, authorizationService); + var registry = typeTransformerRegistry.forContext("identity-api"); + registry.register(new VerifiableCredentialManifestToVerifiableCredentialResourceTransformer()); + var controller = new VerifiableCredentialsApiController(credentialStore, authorizationService, new VerifiableCredentialManifestValidator(), registry); var getAllController = new GetAllCredentialsApiController(credentialStore); webService.registerResource(IdentityHubApiContext.IDENTITY, controller); webService.registerResource(IdentityHubApiContext.IDENTITY, getAllController); diff --git a/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApi.java b/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApi.java index f2a4475dc..b961fe10e 100644 --- a/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApi.java +++ b/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApi.java @@ -22,11 +22,11 @@ import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.ws.rs.core.SecurityContext; -import org.eclipse.edc.iam.did.spi.document.DidDocument; -import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; +import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialManifest; import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialResource; import org.eclipse.edc.web.spi.ApiErrorDetail; @@ -44,10 +44,10 @@ public interface VerifiableCredentialsApi { }, responses = { @ApiResponse(responseCode = "200", description = "The VerifiableCredential.", - content = @Content(schema = @Schema(implementation = ParticipantContext.class))), + content = @Content(schema = @Schema(implementation = VerifiableCredentialResource.class))), @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), - @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", + @ApiResponse(responseCode = "403", description = "The request could not be completed, because either the authentication was missing or was not valid.", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), @ApiResponse(responseCode = "404", description = "A VerifiableCredential with the given ID does not exist.", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) @@ -55,6 +55,42 @@ public interface VerifiableCredentialsApi { ) VerifiableCredentialResource getCredential(String id, SecurityContext securityContext); + @Operation(description = "Adds a new VerifiableCredential into the system.", + operationId = "addCredential", + parameters = { + @Parameter(name = "participantId", description = "Base64-Url encode Participant Context ID", required = true, in = ParameterIn.PATH) + }, + requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = VerifiableCredentialManifest.class))), + responses = { + @ApiResponse(responseCode = "204", description = "The VerifiableCredential was successfully created."), + @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "403", description = "The request could not be completed, because either the authentication was missing or was not valid.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "409", description = "Could not create VerifiableCredential, because a VerifiableCredential with that ID already exists", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) + } + ) + void addCredential(String participantId, VerifiableCredentialManifest manifest, SecurityContext securityContext); + + @Operation(description = "Update an existing VerifiableCredential.", + operationId = "updateCredential", + parameters = { + @Parameter(name = "participantId", description = "Base64-Url encode Participant Context ID", required = true, in = ParameterIn.PATH) + }, + requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = VerifiableCredentialManifest.class))), + responses = { + @ApiResponse(responseCode = "204", description = "The VerifiableCredential was updated successfully."), + @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "403", description = "The request could not be completed, because either the authentication was missing or was not valid.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "404", description = "VerifiableCredential could not be updated because it does not exist.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) + } + ) + void updateCredential(VerifiableCredentialManifest manifest, SecurityContext securityContext); + @Operation(description = "Query VerifiableCredentials by type.", operationId = "queryCredentialsByType", @@ -63,8 +99,8 @@ public interface VerifiableCredentialsApi { }, responses = { @ApiResponse(responseCode = "200", description = "The list of VerifiableCredentials.", - content = @Content(array = @ArraySchema(schema = @Schema(implementation = DidDocument.class)))), - @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = VerifiableCredentialResource.class)))), + @ApiResponse(responseCode = "403", description = "The request could not be completed, because either the authentication was missing or was not valid.", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), @ApiResponse(responseCode = "400", description = "The query was malformed or was not understood by the server.", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), @@ -78,10 +114,10 @@ public interface VerifiableCredentialsApi { @Parameter(name = "participantId", description = "Base64-Url encode Participant Context ID", required = true, in = ParameterIn.PATH), }, responses = { - @ApiResponse(responseCode = "200", description = "The VerifiableCredential was deleted successfully", content = { @Content(schema = @Schema(implementation = String.class)) }), + @ApiResponse(responseCode = "200", description = "The VerifiableCredential was deleted successfully", content = {@Content(schema = @Schema(implementation = String.class))}), @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), - @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", + @ApiResponse(responseCode = "403", description = "The request could not be completed, because either the authentication was missing or was not valid.", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), @ApiResponse(responseCode = "404", description = "A VerifiableCredential with the given ID does not exist.", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) diff --git a/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiController.java b/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiController.java index 9fb66fb2b..eb02a7194 100644 --- a/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiController.java +++ b/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiController.java @@ -9,6 +9,7 @@ * * Contributors: * Metaform Systems, Inc. - initial API and implementation + * Amadeus IT Group - adds endpoints to create and updates verifiable credentials * */ @@ -17,6 +18,8 @@ import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; @@ -24,19 +27,26 @@ import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.SecurityContext; import org.eclipse.edc.identityhub.api.Versions; +import org.eclipse.edc.identityhub.api.v1.validation.VerifiableCredentialManifestValidator; import org.eclipse.edc.identityhub.spi.AuthorizationService; +import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; import org.eclipse.edc.identityhub.spi.store.CredentialStore; +import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialManifest; import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialResource; import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.eclipse.edc.web.spi.exception.InvalidRequestException; import org.eclipse.edc.web.spi.exception.ObjectNotFoundException; +import org.eclipse.edc.web.spi.exception.ValidationFailureException; import java.util.Collection; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; import static org.eclipse.edc.identityhub.spi.AuthorizationResultHandler.exceptionMapper; +import static org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextId.onEncoded; +import static org.eclipse.edc.spi.result.ServiceResult.badRequest; @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) @@ -45,10 +55,14 @@ public class VerifiableCredentialsApiController implements VerifiableCredentials private final CredentialStore credentialStore; private final AuthorizationService authorizationService; + private final VerifiableCredentialManifestValidator validator; + private final TypeTransformerRegistry typeTransformerRegistry; - public VerifiableCredentialsApiController(CredentialStore credentialStore, AuthorizationService authorizationService) { + public VerifiableCredentialsApiController(CredentialStore credentialStore, AuthorizationService authorizationService, VerifiableCredentialManifestValidator validator, TypeTransformerRegistry typeTransformerRegistry) { this.credentialStore = credentialStore; this.authorizationService = authorizationService; + this.validator = validator; + this.typeTransformerRegistry = typeTransformerRegistry; } @GET @@ -63,6 +77,33 @@ public VerifiableCredentialResource getCredential(@PathParam("credentialId") Str return result.stream().findFirst().orElseThrow(() -> new ObjectNotFoundException(VerifiableCredentialResource.class, id)); } + @POST + @Override + public void addCredential(@PathParam("participantId") String participantId, VerifiableCredentialManifest manifest, @Context SecurityContext securityContext) { + validator.validate(manifest).orElseThrow(ValidationFailureException::new); + + var decoded = onEncoded(participantId).orElseThrow(InvalidRequestException::new); + authorizationService.isAuthorized(securityContext, decoded, ParticipantContext.class) + .compose(u -> typeTransformerRegistry.transform(manifest, VerifiableCredentialResource.class) + .map(ServiceResult::success) + .orElse(failure -> badRequest(failure.getFailureDetail()))) + .compose(vcr -> ServiceResult.from(credentialStore.create(vcr))) + .orElseThrow(exceptionMapper(VerifiableCredentialResource.class)); + } + + @PUT + @Override + public void updateCredential(VerifiableCredentialManifest manifest, @Context SecurityContext securityContext) { + validator.validate(manifest).orElseThrow(ValidationFailureException::new); + + authorizationService.isAuthorized(securityContext, manifest.getId(), VerifiableCredentialResource.class) + .compose(u -> typeTransformerRegistry.transform(manifest, VerifiableCredentialResource.class) + .map(ServiceResult::success) + .orElse(failure -> badRequest(failure.getFailureDetail()))) + .compose(vcr -> ServiceResult.from(credentialStore.update(vcr))) + .orElseThrow(exceptionMapper(VerifiableCredentialResource.class)); + } + @GET @Override public Collection queryCredentialsByType(@QueryParam("type") String type, @Context SecurityContext securityContext) { @@ -72,7 +113,8 @@ public Collection queryCredentialsByType(@QueryPar return credentialStore.query(query) .orElseThrow(InvalidRequestException::new) - .stream().filter(vcr -> authorizationService.isAuthorized(securityContext, vcr.getId(), VerifiableCredentialResource.class).succeeded()) + .stream() + .filter(vcr -> authorizationService.isAuthorized(securityContext, vcr.getId(), VerifiableCredentialResource.class).succeeded()) .toList(); } @@ -87,4 +129,5 @@ public void deleteCredential(@PathParam("credentialId") String id, @Context Secu throw exceptionMapper(VerifiableCredentialResource.class, id).apply(ServiceResult.fromFailure(res).getFailure()); } } + } diff --git a/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/transformer/VerifiableCredentialManifestToVerifiableCredentialResourceTransformer.java b/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/transformer/VerifiableCredentialManifestToVerifiableCredentialResourceTransformer.java new file mode 100644 index 000000000..88a233556 --- /dev/null +++ b/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/transformer/VerifiableCredentialManifestToVerifiableCredentialResourceTransformer.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 Amadeus IT Group. + * + * 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: + * Amadeus IT Group - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.api.verifiablecredentials.v1.unstable.transformer; + +import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VcStatus; +import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialManifest; +import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialResource; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.eclipse.edc.transform.spi.TypeTransformer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class VerifiableCredentialManifestToVerifiableCredentialResourceTransformer implements TypeTransformer { + + @Override + public Class getInputType() { + return VerifiableCredentialManifest.class; + } + + @Override + public Class getOutputType() { + return VerifiableCredentialResource.class; + } + + @Override + public @Nullable VerifiableCredentialResource transform(@NotNull VerifiableCredentialManifest manifest, @NotNull TransformerContext transformerContext) { + var container = manifest.getVerifiableCredentialContainer(); + return VerifiableCredentialResource.Builder.newInstance() + .id(manifest.getId()) + .participantId(manifest.getParticipantId()) + .issuerId(container.credential().getIssuer().id()) + .holderId(container.credential().getCredentialSubject().stream().findFirst().get().getId()) + .state(VcStatus.ISSUED) + .issuancePolicy(manifest.getIssuancePolicy()) + .reissuancePolicy(manifest.getReissuancePolicy()) + .credential(container) + .build(); + } +} diff --git a/extensions/api/identity-api/verifiable-credentials-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiControllerTest.java b/extensions/api/identity-api/verifiable-credentials-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiControllerTest.java index 929d96882..320d7cc7a 100644 --- a/extensions/api/identity-api/verifiable-credentials-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiControllerTest.java +++ b/extensions/api/identity-api/verifiable-credentials-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiControllerTest.java @@ -21,22 +21,37 @@ import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential; import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredentialContainer; import org.eclipse.edc.identityhub.api.Versions; +import org.eclipse.edc.identityhub.api.v1.validation.VerifiableCredentialManifestValidator; import org.eclipse.edc.identityhub.spi.AuthorizationService; +import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext; import org.eclipse.edc.identityhub.spi.store.CredentialStore; +import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialManifest; import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialResource; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.spi.result.ServiceResult; import org.eclipse.edc.spi.result.StoreResult; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.eclipse.edc.validator.spi.ValidationResult; +import org.eclipse.edc.validator.spi.Violation; import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.time.Instant; import java.util.Arrays; +import java.util.Base64; import java.util.List; import java.util.Map; +import java.util.UUID; import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.spi.result.Result.failure; +import static org.eclipse.edc.spi.result.ServiceResult.unauthorized; +import static org.eclipse.edc.spi.result.StoreResult.alreadyExists; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -45,177 +60,369 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +@ApiTest class VerifiableCredentialsApiControllerTest extends RestControllerTestBase { + private static final String PARTICIPANT_ID = "test-participant"; private static final String CREDENTIAL_ID = "test-credential-id"; private final CredentialStore credentialStore = mock(); private final AuthorizationService authorizationService = mock(); + private final VerifiableCredentialManifestValidator validator = mock(); + private final TypeTransformerRegistry typeTransformerRegistry = mock(); @BeforeEach void setUp() { when(authorizationService.isAuthorized(any(), anyString(), any())).thenReturn(ServiceResult.success()); } - @Test - void findById() { - var credential = createCredential("VerifiableCredential").build(); - when(credentialStore.query(any())).thenReturn(StoreResult.success(List.of(credential))); - - var result = baseRequest() - .get("/%s".formatted(CREDENTIAL_ID)) - .then() - .log().ifValidationFails() - .statusCode(200) - .extract().body().as(VerifiableCredentialResource.class); - - assertThat(result).usingRecursiveComparison().ignoringFields("clock").isEqualTo(credential); - verify(credentialStore).query(any()); - verifyNoMoreInteractions(credentialStore); - } - - - @Test - void findById_unauthorized403() { - when(authorizationService.isAuthorized(any(), anyString(), any())).thenReturn(ServiceResult.unauthorized("test-message")); - baseRequest() - .get("/%s".formatted(CREDENTIAL_ID)) - .then() - .log().ifValidationFails() - .statusCode(403); - verify(authorizationService).isAuthorized(any(), anyString(), eq(VerifiableCredentialResource.class)); - verifyNoMoreInteractions(credentialStore, authorizationService); - } - - @Test - void findById_whenNotExists_expect404() { - when(credentialStore.query(any())).thenReturn(StoreResult.success(List.of())); - - baseRequest() - .get("/%s".formatted(CREDENTIAL_ID)) - .then() - .log().ifValidationFails() - .statusCode(404); - - verify(credentialStore).query(any()); - verifyNoMoreInteractions(credentialStore); - } - - @Test - void findByType() { - var credential1 = createCredential("test-type").build(); - var credential2 = createCredential("test-type").build(); - when(credentialStore.query(any())).thenReturn(StoreResult.success(List.of(credential1, credential2))); - - var result = baseRequest() - .get("?type=test-type") - .then() - .log().ifValidationFails() - .statusCode(200) - .extract().body().as(VerifiableCredentialResource[].class); - - assertThat(result).usingRecursiveFieldByFieldElementComparatorIgnoringFields("clock").containsExactlyInAnyOrder(credential1, credential2); - verify(credentialStore).query(any()); - verifyNoMoreInteractions(credentialStore); - } - - @Test - void findByType_unauthorized403() { - var credential1 = createCredential("test-type").build(); - var credential2 = createCredential("test-type").build(); - when(credentialStore.query(any())).thenReturn(StoreResult.success(List.of(credential1, credential2))); - when(authorizationService.isAuthorized(any(), eq(credential1.getId()), any())).thenReturn(ServiceResult.unauthorized("test-message")); - - var result = baseRequest() - .get("?type=test-type") - .then() - .log().ifValidationFails() - .statusCode(200) - .extract().body().as(VerifiableCredentialResource[].class); - - assertThat(result).usingRecursiveFieldByFieldElementComparatorIgnoringFields("clock").containsExactlyInAnyOrder(credential2); - verify(credentialStore).query(any()); - verifyNoMoreInteractions(credentialStore); - } - - @Test - void findByType_noResult() { - when(credentialStore.query(any())).thenReturn(StoreResult.success(List.of())); - - var result = baseRequest() - .get("?type=test-type") - .then() - .log().ifValidationFails() - .statusCode(200) - .extract().body().as(VerifiableCredentialResource[].class); - - assertThat(result).isEmpty(); - verify(credentialStore).query(any()); - verifyNoMoreInteractions(credentialStore); - } - - @Test - void deleteCredential() { - when(credentialStore.deleteById(CREDENTIAL_ID)).thenReturn(StoreResult.success()); - - baseRequest() - .delete("/%s".formatted(CREDENTIAL_ID)) - .then() - .log().ifValidationFails() - .statusCode(204); - - verify(credentialStore).deleteById(eq(CREDENTIAL_ID)); - verifyNoMoreInteractions(credentialStore); - } - - @Test - void deleteCredential_unauthorized403() { - when(authorizationService.isAuthorized(any(), anyString(), any())).thenReturn(ServiceResult.unauthorized("test-message")); - - baseRequest() - .delete("/%s".formatted(CREDENTIAL_ID)) - .then() - .log().ifValidationFails() - .statusCode(403); - verify(authorizationService).isAuthorized(any(), anyString(), eq(VerifiableCredentialResource.class)); - verifyNoMoreInteractions(credentialStore, authorizationService); - } - - @Test - void deleteCredential_whenNotExists() { - when(credentialStore.deleteById(CREDENTIAL_ID)).thenReturn(StoreResult.notFound("test-message")); - - baseRequest() - .delete("/%s".formatted(CREDENTIAL_ID)) - .then() - .log().ifValidationFails() - .statusCode(404); - - verify(credentialStore).deleteById(eq(CREDENTIAL_ID)); - verifyNoMoreInteractions(credentialStore); - } - @Override protected Object controller() { - return new VerifiableCredentialsApiController(credentialStore, authorizationService); + return new VerifiableCredentialsApiController(credentialStore, authorizationService, validator, typeTransformerRegistry); } - private VerifiableCredentialResource.Builder createCredential(String... types) { - var cred = VerifiableCredential.Builder.newInstance() + private VerifiableCredential createCredential(String... types) { + return VerifiableCredential.Builder.newInstance() + .id(UUID.randomUUID().toString()) .types(Arrays.asList(types)) .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(); + } + + private VerifiableCredentialResource.Builder createCredentialResource(String... types) { + var cred = createCredential(types); return VerifiableCredentialResource.Builder.newInstance() + .id(UUID.randomUUID().toString()) .credential(new VerifiableCredentialContainer("foobar", CredentialFormat.JSON_LD, cred)) .holderId("test-holder") .issuerId("test-issuer"); } + private VerifiableCredentialManifest createManifest(VerifiableCredential credential) { + return VerifiableCredentialManifest.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .participantId(PARTICIPANT_ID) + .verifiableCredentialContainer(new VerifiableCredentialContainer("rawVc", CredentialFormat.JSON_LD, credential)) + .build(); + } + private RequestSpecification baseRequest() { return given() .contentType("application/json") - .baseUri("http://localhost:" + port + Versions.UNSTABLE + "/participants/test-participant/credentials") + .baseUri("http://localhost:" + port + Versions.UNSTABLE + "/participants/" + Base64.getUrlEncoder().encodeToString(PARTICIPANT_ID.getBytes()) + "/credentials") .when(); } + + @Nested + class Create { + @Test + void success() { + var credential = createCredential("type"); + var manifest = createManifest(credential); + var resource = mock(VerifiableCredentialResource.class); + when(validator.validate(any())).thenReturn(ValidationResult.success()); + when(authorizationService.isAuthorized(any(), eq(PARTICIPANT_ID), eq(ParticipantContext.class))).thenReturn(ServiceResult.success()); + when(typeTransformerRegistry.transform(any(), eq(VerifiableCredentialResource.class))).thenReturn(Result.success(resource)); + when(credentialStore.create(resource)).thenReturn(StoreResult.success()); + + baseRequest() + .contentType(JSON) + .body(manifest) + .post() + .then() + .log().ifValidationFails() + .statusCode(204); + } + + @Test + void validationFails_returns400() { + when(validator.validate(any())).thenReturn(ValidationResult.failure(new Violation("test-message", "test-path", "test-value"))); + + baseRequest() + .contentType(JSON) + .body(createManifest(createCredential("type"))) + .post() + .then() + .log().ifValidationFails() + .statusCode(400); + } + + @Test + void notAuthorized_returns403() { + when(validator.validate(any())).thenReturn(ValidationResult.success()); + when(authorizationService.isAuthorized(any(), eq(PARTICIPANT_ID), eq(ParticipantContext.class))).thenReturn(unauthorized("test-message")); + + baseRequest() + .contentType(JSON) + .body(createManifest(createCredential("type"))) + .post() + .then() + .log().ifValidationFails() + .statusCode(403); + } + + @Test + void transformFails_returns400() { + when(validator.validate(any())).thenReturn(ValidationResult.success()); + when(authorizationService.isAuthorized(any(), eq(PARTICIPANT_ID), eq(ParticipantContext.class))).thenReturn(ServiceResult.success()); + when(typeTransformerRegistry.transform(any(), eq(VerifiableCredentialResource.class))).thenReturn(failure("transform-failure")); + + baseRequest() + .contentType(JSON) + .body(createManifest(createCredential("type"))) + .post() + .then() + .log().ifValidationFails() + .statusCode(400); + } + + @Test + void vcAlreadyExists_returns() { + when(validator.validate(any())).thenReturn(ValidationResult.success()); + when(authorizationService.isAuthorized(any(), eq(PARTICIPANT_ID), eq(ParticipantContext.class))).thenReturn(ServiceResult.success()); + when(typeTransformerRegistry.transform(any(), eq(VerifiableCredentialResource.class))).thenReturn(Result.success(mock(VerifiableCredentialResource.class))); + when(credentialStore.create(any())).thenReturn(alreadyExists("already-exists")); + + baseRequest() + .contentType(JSON) + .body(createManifest(createCredential("type"))) + .post() + .then() + .log().ifValidationFails() + .statusCode(409); + } + } + + @Nested + class Update { + @Test + void success() { + var credential = createCredential("type"); + var manifest = createManifest(credential); + var resource = mock(VerifiableCredentialResource.class); + when(validator.validate(any())).thenReturn(ValidationResult.success()); + when(authorizationService.isAuthorized(any(), eq(manifest.getId()), eq(VerifiableCredentialResource.class))).thenReturn(ServiceResult.success()); + when(typeTransformerRegistry.transform(any(), eq(VerifiableCredentialResource.class))).thenReturn(Result.success(resource)); + when(credentialStore.update(resource)).thenReturn(StoreResult.success()); + + baseRequest() + .contentType(JSON) + .body(manifest) + .put() + .then() + .log().ifValidationFails() + .statusCode(204); + } + + @Test + void validationFails_returns400() { + when(validator.validate(any())).thenReturn(ValidationResult.failure(new Violation("test-message", "test-path", "test-value"))); + + baseRequest() + .contentType(JSON) + .body(createManifest(createCredential("type"))) + .put() + .then() + .log().ifValidationFails() + .statusCode(400); + } + + @Test + void notAuthorized_returns403() { + when(validator.validate(any())).thenReturn(ValidationResult.success()); + when(authorizationService.isAuthorized(any(), any(), eq(VerifiableCredentialResource.class))).thenReturn(unauthorized("test-message")); + + baseRequest() + .contentType(JSON) + .body(createManifest(createCredential("type"))) + .put() + .then() + .log().ifValidationFails() + .statusCode(403); + } + + @Test + void transformFails_returns400() { + var manifest = createManifest(createCredential("type")); + when(validator.validate(any())).thenReturn(ValidationResult.success()); + when(authorizationService.isAuthorized(any(), eq(manifest.getId()), eq(VerifiableCredentialResource.class))).thenReturn(ServiceResult.success()); + when(typeTransformerRegistry.transform(any(), eq(VerifiableCredentialResource.class))).thenReturn(failure("transform-failure")); + + baseRequest() + .contentType(JSON) + .body(manifest) + .put() + .then() + .log().ifValidationFails() + .statusCode(400); + } + + @Test + void vcAlreadyExists_returns() { + var manifest = createManifest(createCredential("type")); + when(validator.validate(any())).thenReturn(ValidationResult.success()); + when(authorizationService.isAuthorized(any(), eq(manifest.getId()), eq(VerifiableCredentialResource.class))).thenReturn(ServiceResult.success()); + when(typeTransformerRegistry.transform(any(), eq(VerifiableCredentialResource.class))).thenReturn(Result.success(mock(VerifiableCredentialResource.class))); + when(credentialStore.create(any())).thenReturn(alreadyExists("already-exists")); + + baseRequest() + .contentType(JSON) + .body(manifest) + .put() + .then() + .log().ifValidationFails() + .statusCode(500); + } + } + + @Nested + class FindByType { + @Test + void success() { + var credential1 = createCredentialResource("test-type").build(); + var credential2 = createCredentialResource("test-type").build(); + when(credentialStore.query(any())).thenReturn(StoreResult.success(List.of(credential1, credential2))); + + var result = baseRequest() + .get("?type=test-type") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(VerifiableCredentialResource[].class); + + assertThat(result).usingRecursiveFieldByFieldElementComparatorIgnoringFields("clock").containsExactlyInAnyOrder(credential1, credential2); + verify(credentialStore).query(any()); + verifyNoMoreInteractions(credentialStore); + } + + @Test + void notAuthorized_returns403() { + var credential1 = createCredentialResource("test-type").build(); + var credential2 = createCredentialResource("test-type").build(); + when(credentialStore.query(any())).thenReturn(StoreResult.success(List.of(credential1, credential2))); + when(authorizationService.isAuthorized(any(), eq(credential1.getId()), eq(VerifiableCredentialResource.class))).thenReturn(unauthorized("test-message")); + when(authorizationService.isAuthorized(any(), eq(credential2.getId()), eq(VerifiableCredentialResource.class))).thenReturn(ServiceResult.success()); + + var result = baseRequest() + .get("?type=test-type") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(VerifiableCredentialResource[].class); + + assertThat(result).usingRecursiveFieldByFieldElementComparatorIgnoringFields("clock").containsExactlyInAnyOrder(credential2); + verify(credentialStore).query(any()); + verifyNoMoreInteractions(credentialStore); + } + + @Test + void emptyResult() { + when(credentialStore.query(any())).thenReturn(StoreResult.success(List.of())); + + var result = baseRequest() + .get("?type=test-type") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(VerifiableCredentialResource[].class); + + assertThat(result).isEmpty(); + verify(credentialStore).query(any()); + verifyNoMoreInteractions(credentialStore); + } + } + + @Nested + class Delete { + + @Test + void success() { + when(credentialStore.deleteById(CREDENTIAL_ID)).thenReturn(StoreResult.success()); + + baseRequest() + .delete("/%s".formatted(CREDENTIAL_ID)) + .then() + .log().ifValidationFails() + .statusCode(204); + + verify(credentialStore).deleteById(eq(CREDENTIAL_ID)); + verifyNoMoreInteractions(credentialStore); + } + + @Test + void notAuthorized_returns403() { + when(authorizationService.isAuthorized(any(), anyString(), any())).thenReturn(unauthorized("test-message")); + + baseRequest() + .delete("/%s".formatted(CREDENTIAL_ID)) + .then() + .log().ifValidationFails() + .statusCode(403); + verify(authorizationService).isAuthorized(any(), anyString(), eq(VerifiableCredentialResource.class)); + verifyNoMoreInteractions(credentialStore, authorizationService); + } + + @Test + void idDoesNotExist_returns404() { + when(credentialStore.deleteById(CREDENTIAL_ID)).thenReturn(StoreResult.notFound("test-message")); + + baseRequest() + .delete("/%s".formatted(CREDENTIAL_ID)) + .then() + .log().ifValidationFails() + .statusCode(404); + + verify(credentialStore).deleteById(eq(CREDENTIAL_ID)); + verifyNoMoreInteractions(credentialStore); + } + } + + @Nested + class FindById { + + @Test + void success() { + var credential = createCredentialResource("VerifiableCredential").build(); + when(credentialStore.query(any())).thenReturn(StoreResult.success(List.of(credential))); + + var result = baseRequest() + .get("/%s".formatted(CREDENTIAL_ID)) + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(VerifiableCredentialResource.class); + + assertThat(result).usingRecursiveComparison().ignoringFields("clock").isEqualTo(credential); + verify(credentialStore).query(any()); + verifyNoMoreInteractions(credentialStore); + } + + + @Test + void notAuthorized_returns403() { + when(authorizationService.isAuthorized(any(), anyString(), any())).thenReturn(unauthorized("test-message")); + baseRequest() + .get("/%s".formatted(CREDENTIAL_ID)) + .then() + .log().ifValidationFails() + .statusCode(403); + verify(authorizationService).isAuthorized(any(), anyString(), eq(VerifiableCredentialResource.class)); + verifyNoMoreInteractions(credentialStore, authorizationService); + } + + @Test + void idDoesNotExist_returns404() { + when(credentialStore.query(any())).thenReturn(StoreResult.success(List.of())); + + baseRequest() + .get("/%s".formatted(CREDENTIAL_ID)) + .then() + .log().ifValidationFails() + .statusCode(404); + + verify(credentialStore).query(any()); + verifyNoMoreInteractions(credentialStore); + } + } } \ No newline at end of file diff --git a/extensions/api/identity-api/verifiable-credentials-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/transformer/VerifiableCredentialManifestToVerifiableCredentialResourceTransformerTest.java b/extensions/api/identity-api/verifiable-credentials-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/transformer/VerifiableCredentialManifestToVerifiableCredentialResourceTransformerTest.java new file mode 100644 index 000000000..6a10cd608 --- /dev/null +++ b/extensions/api/identity-api/verifiable-credentials-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/transformer/VerifiableCredentialManifestToVerifiableCredentialResourceTransformerTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 Amadeus IT Group. + * + * 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: + * Amadeus IT Group - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.api.verifiablecredentials.v1.unstable.transformer; + +import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSubject; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.Issuer; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredentialContainer; +import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialManifest; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; + +class VerifiableCredentialManifestToVerifiableCredentialResourceTransformerTest { + + private final VerifiableCredentialManifestToVerifiableCredentialResourceTransformer transformer = new VerifiableCredentialManifestToVerifiableCredentialResourceTransformer(); + + @Test + void transform_success() { + var credential = VerifiableCredential.Builder.newInstance() + .id("id") + .type("type") + .issuer(new Issuer("issuer")) + .issuanceDate(Instant.now()) + .credentialSubject(CredentialSubject.Builder.newInstance().id("subject").claim("foo", "bar").build()) + .build(); + var manifest = VerifiableCredentialManifest.Builder.newInstance() + .id("id") + .participantId("participantId") + .issuancePolicy(mock()) + .reissuancePolicy(mock()) + .verifiableCredentialContainer(new VerifiableCredentialContainer("rawVc", CredentialFormat.JWT, credential)) + .build(); + + var resource = transformer.transform(manifest, null); + + assertNotNull(resource); + assertThat(resource.getId()).isEqualTo(manifest.getId()); + assertThat(resource.getParticipantId()).isEqualTo(manifest.getParticipantId()); + assertThat(resource.getIssuancePolicy()).isEqualTo(manifest.getIssuancePolicy()); + assertThat(resource.getReissuancePolicy()).isEqualTo(manifest.getReissuancePolicy()); + assertThat(resource.getVerifiableCredential()).isEqualTo(manifest.getVerifiableCredentialContainer()); + assertThat(resource.getIssuerId()).isEqualTo(credential.getIssuer().id()); + assertThat(resource.getHolderId()).isEqualTo(credential.getCredentialSubject().stream().findFirst().get().getId()); + } + +} \ No newline at end of file diff --git a/spi/participant-context-spi/src/main/java/org/eclipse/edc/identityhub/spi/participantcontext/model/IdentityResource.java b/spi/participant-context-spi/src/main/java/org/eclipse/edc/identityhub/spi/participantcontext/model/IdentityResource.java index 4c3114c18..498b94abb 100644 --- a/spi/participant-context-spi/src/main/java/org/eclipse/edc/identityhub/spi/participantcontext/model/IdentityResource.java +++ b/spi/participant-context-spi/src/main/java/org/eclipse/edc/identityhub/spi/participantcontext/model/IdentityResource.java @@ -22,7 +22,7 @@ /** * Abstract class representing an Identity Resource. - * Identity entitys have an ID, a timestamp, an issuer ID, a holder ID, and a clock. + * Identity resources have an ID, a timestamp, an issuer ID, a holder ID, and a clock. * They can be extended with custom properties and behaviors. */ public abstract class IdentityResource extends ParticipantResource { diff --git a/spi/verifiable-credential-spi/src/main/java/org/eclipse/edc/identityhub/spi/verifiablecredentials/model/VerifiableCredentialManifest.java b/spi/verifiable-credential-spi/src/main/java/org/eclipse/edc/identityhub/spi/verifiablecredentials/model/VerifiableCredentialManifest.java new file mode 100644 index 000000000..0abdf7c56 --- /dev/null +++ b/spi/verifiable-credential-spi/src/main/java/org/eclipse/edc/identityhub/spi/verifiablecredentials/model/VerifiableCredentialManifest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2024 Amadeus IT Group. + * + * 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: + * Amadeus IT Group - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.spi.verifiablecredentials.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredentialContainer; +import org.eclipse.edc.policy.model.Policy; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * Manifest (=recipe) for creating the {@link org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential}. + */ +@JsonDeserialize(builder = VerifiableCredentialManifest.Builder.class) +public class VerifiableCredentialManifest { + private String id; + private String participantId; + private VerifiableCredentialContainer verifiableCredentialContainer; + private Policy issuancePolicy; + private Policy reissuancePolicy; + + private VerifiableCredentialManifest() { + } + + /** + * The Verifiable Credential id. + */ + public String getId() { + return id; + } + + /** + * The participant id. + */ + public String getParticipantId() { + return participantId; + } + + /** + * The Verifiable Credential container. + */ + public VerifiableCredentialContainer getVerifiableCredentialContainer() { + return verifiableCredentialContainer; + } + + /** + * The issuance policy for the Verifiable Credential. + */ + @Nullable + public Policy getIssuancePolicy() { + return issuancePolicy; + } + + /** + * The re-issuance policy for the Verifiable Credential. + */ + @Nullable + public Policy getReissuancePolicy() { + return reissuancePolicy; + } + + @JsonPOJOBuilder(withPrefix = "") + public static final class Builder { + + private final VerifiableCredentialManifest manifest; + + private Builder() { + manifest = new VerifiableCredentialManifest(); + } + + @JsonCreator + public static Builder newInstance() { + return new Builder(); + } + + public Builder id(String id) { + manifest.id = id; + return this; + } + + public Builder participantId(String participantId) { + manifest.participantId = participantId; + return this; + } + + public Builder verifiableCredentialContainer(VerifiableCredentialContainer verifiableCredentialContainer) { + manifest.verifiableCredentialContainer = verifiableCredentialContainer; + return this; + } + + public Builder issuancePolicy(Policy issuancePolicy) { + manifest.issuancePolicy = issuancePolicy; + return this; + } + + public Builder reissuancePolicy(Policy reissuancePolicy) { + manifest.reissuancePolicy = reissuancePolicy; + return this; + } + + public VerifiableCredentialManifest build() { + if (manifest.id == null) { + manifest.id = UUID.randomUUID().toString(); + + } + return manifest; + } + } +} diff --git a/spi/verifiable-credential-spi/src/test/java/org/eclipse/edc/identityhub/spi/verifiablecredentials/model/VerifiableCredentialManifestTest.java b/spi/verifiable-credential-spi/src/test/java/org/eclipse/edc/identityhub/spi/verifiablecredentials/model/VerifiableCredentialManifestTest.java new file mode 100644 index 000000000..7380e1283 --- /dev/null +++ b/spi/verifiable-credential-spi/src/test/java/org/eclipse/edc/identityhub/spi/verifiablecredentials/model/VerifiableCredentialManifestTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 Amadeus IT Group. + * + * 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: + * Amadeus IT Group - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.spi.verifiablecredentials.model; + +import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSubject; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.Issuer; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredentialContainer; +import org.eclipse.edc.json.JacksonTypeManager; +import org.eclipse.edc.spi.types.TypeManager; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; + +class VerifiableCredentialManifestTest { + + private final TypeManager typeManager = new JacksonTypeManager(); + + @Test + void serDeser() { + var manifest = VerifiableCredentialManifest.Builder.newInstance() + .id("id") + .participantId("participantId") + .verifiableCredentialContainer(new VerifiableCredentialContainer("rawVc", CredentialFormat.JWT, VerifiableCredential.Builder.newInstance() + .type("type") + .credentialSubject(CredentialSubject.Builder.newInstance().id("id").claim("foo", "bar").build()) + .issuer(new Issuer("issuer")) + .issuanceDate(Instant.now()) + .build())) + .build(); + + var serialized = typeManager.writeValueAsString(manifest); + + var deserialized = typeManager.readValue(serialized, VerifiableCredentialManifest.class); + + assertThat(deserialized).usingRecursiveComparison().isEqualTo(manifest); + } +} \ No newline at end of file