diff --git a/DEPENDENCIES b/DEPENDENCIES index bea447e5c..13a742fb1 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -54,14 +54,14 @@ maven/mavencentral/com.google.code.gson/gson/2.10.1, Apache-2.0, approved, #6159 maven/mavencentral/com.google.crypto.tink/tink/1.14.1, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.errorprone/error_prone_annotations/2.11.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.errorprone/error_prone_annotations/2.22.0, Apache-2.0, approved, #10661 -maven/mavencentral/com.google.errorprone/error_prone_annotations/2.26.1, Apache-2.0, approved, #13657 +maven/mavencentral/com.google.errorprone/error_prone_annotations/2.28.0, Apache-2.0, approved, #15107 maven/mavencentral/com.google.guava/failureaccess/1.0.1, Apache-2.0, approved, CQ22654 maven/mavencentral/com.google.guava/failureaccess/1.0.2, Apache-2.0, approved, CQ22654 maven/mavencentral/com.google.guava/guava/28.1-android, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.guava/guava/28.2-android, Apache-2.0 AND LicenseRef-Public-Domain, approved, CQ22437 maven/mavencentral/com.google.guava/guava/31.0.1-android, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.guava/guava/31.1-jre, Apache-2.0, approved, clearlydefined -maven/mavencentral/com.google.guava/guava/33.2.0-jre, Apache-2.0 AND CC0-1.0 AND (Apache-2.0 AND CC-PDDC), approved, #14607 +maven/mavencentral/com.google.guava/guava/33.3.0-jre, Apache-2.0 AND CC0-1.0 AND (Apache-2.0 AND CC-PDDC) AND (Apache-2.0 AND CC0-1.0), approved, #15952 maven/mavencentral/com.google.guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava, Apache-2.0, approved, CQ22657 maven/mavencentral/com.google.j2objc/j2objc-annotations/1.3, Apache-2.0, approved, CQ21195 maven/mavencentral/com.google.protobuf/protobuf-java/3.25.3, BSD-3-Clause, approved, clearlydefined @@ -72,7 +72,7 @@ maven/mavencentral/com.lmax/disruptor/3.4.4, Apache-2.0, approved, clearlydefine maven/mavencentral/com.networknt/json-schema-validator/1.0.76, Apache-2.0, approved, CQ22638 maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.28, Apache-2.0, approved, clearlydefined maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.40, Apache-2.0, approved, #15156 -maven/mavencentral/com.puppycrawl.tools/checkstyle/10.17.0, LGPL-2.1-or-later AND (Apache-2.0 AND LGPL-2.1-or-later) AND Apache-2.0, approved, #15077 +maven/mavencentral/com.puppycrawl.tools/checkstyle/10.18.0, LGPL-2.1-or-later, restricted, clearlydefined maven/mavencentral/com.samskivert/jmustache/1.15, BSD-2-Clause AND BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.squareup.okhttp3/okhttp-dnsoverhttps/4.12.0, Apache-2.0, approved, #11159 maven/mavencentral/com.squareup.okhttp3/okhttp/4.12.0, Apache-2.0, approved, #15227 @@ -180,8 +180,8 @@ maven/mavencentral/net.javacrumbs.json-unit/json-unit-core/2.36.0, Apache-2.0, a maven/mavencentral/net.minidev/accessors-smart/2.4.7, Apache-2.0, approved, #7515 maven/mavencentral/net.minidev/json-smart/2.4.7, Apache-2.0, approved, #3288 maven/mavencentral/net.sf.jopt-simple/jopt-simple/5.0.4, MIT, approved, CQ13174 -maven/mavencentral/net.sf.saxon/Saxon-HE/12.4, MPL-2.0 AND (MPL-2.0 AND Apache-2.0) AND (MPL-2.0 AND LicenseRef-X11-style) AND MPL-1.0 AND W3C, approved, #12716 -maven/mavencentral/org.antlr/antlr4-runtime/4.13.1, BSD-3-Clause, approved, #10767 +maven/mavencentral/net.sf.saxon/Saxon-HE/12.5, NOASSERTION, restricted, clearlydefined +maven/mavencentral/org.antlr/antlr4-runtime/4.13.2, BSD-3-Clause, approved, #10767 maven/mavencentral/org.apache.commons/commons-compress/1.24.0, Apache-2.0 AND BSD-3-Clause AND bzip2-1.0.6 AND LicenseRef-Public-Domain, approved, #10368 maven/mavencentral/org.apache.commons/commons-digester3/3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.apache.commons/commons-lang3/3.10, Apache-2.0, approved, clearlydefined @@ -224,7 +224,7 @@ maven/mavencentral/org.bouncycastle/bcutil-jdk18on/1.78.1, MIT, approved, #14435 maven/mavencentral/org.ccil.cowan.tagsoup/tagsoup/1.2.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.checkerframework/checker-qual/3.12.0, MIT, approved, clearlydefined maven/mavencentral/org.checkerframework/checker-qual/3.42.0, MIT, approved, clearlydefined -maven/mavencentral/org.checkerframework/checker-qual/3.43.0, MIT, approved, clearlydefined +maven/mavencentral/org.checkerframework/checker-qual/3.46.0, MIT, approved, clearlydefined maven/mavencentral/org.codehaus.plexus/plexus-classworlds/2.6.0, Apache-2.0 AND Plexus, approved, CQ22821 maven/mavencentral/org.codehaus.plexus/plexus-component-annotations/2.1.0, Apache-2.0, approved, #809 maven/mavencentral/org.codehaus.plexus/plexus-container-default/2.1.0, Apache-2.0, approved, clearlydefined @@ -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/docs/developer/decision-records/2024-08-23-identity_hub_identity_write_credentials_api/README.md b/docs/developer/decision-records/2024-08-23-identity_hub_identity_write_credentials_api/README.md new file mode 100644 index 000000000..9f10c9a5e --- /dev/null +++ b/docs/developer/decision-records/2024-08-23-identity_hub_identity_write_credentials_api/README.md @@ -0,0 +1,16 @@ +# Identity Hub - Identity Write Credentials API + +## Decision + +Write endpoints (create/update) will be added to the Identity Hub identity API. + +## Rationale + +As of now the issuance of VCs into the Identity Hub is not defined as part of the DCP specification. Thus, it is +impossible +for now to add VCs within the Identity Hub at run time. + +To circumvent this issue, we will add write endpoints (create/update) to the Identity Hub identity API. + +> **_NOTE:_** It is strongly discouraged to expose these write endpoints public over the internet as they can be used +> by malicious actors to compromise your VCs. The same warning applies for all identity APIs. \ No newline at end of file diff --git a/docs/developer/decision-records/README.md b/docs/developer/decision-records/README.md index 8665b8fc5..1683cc1b0 100644 --- a/docs/developer/decision-records/README.md +++ b/docs/developer/decision-records/README.md @@ -5,3 +5,4 @@ - [2022-07-29 Self-description](2022-07-29-self-description/) - [2022-08-12 Code Quality Tooling](2022-08-12-code-quality-tooling/) - [2023-01-20 Credentials Verifier Output Format](2023-01-20-credentials-verifier-output-format/) +- [2024-08-23 Identity Hub Write Credentials API](2024-08-23-identity_hub_identity_write_credentials_api/) 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..3a071ba4a --- /dev/null +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/VerifiableCredentialApiEndToEndTest.java @@ -0,0 +1,174 @@ +/* + * 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 vc = context.createCredential(); + + 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), vc)) + .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 manifest = createManifest(user, vc); + 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(vc.getId()).orElseThrow(() -> new EdcException("Failed to credential with id %s".formatted(vc.getId()))); + assertThat(resource.getVerifiableCredential().credential()).usingRecursiveComparison().isEqualTo(vc); + }); + } + + @Test + void update(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + var user = "user1"; + var token = context.createParticipant(user); + + var vc1 = context.createCredential(); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> { + var vc2 = context.createCredential(vc1.getId()); + var manifest = createManifest(user, vc2); + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .body(vc1) + .put("/v1alpha/participants/%s/credentials".formatted(toBase64(user))) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(notNullValue()); + + var resource = context.getCredential(vc2.getId()).orElseThrow(() -> new EdcException("Failed to credential with id %s".formatted(vc2.getId()))); + assertThat(resource.getVerifiableCredential().credential()).usingRecursiveComparison().isEqualTo(vc2); + }); + } + + @Test + void delete(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + var user = "user1"; + var token = context.createParticipant(user); + + var vc = context.createCredential(); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> { + context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .body(vc) + .delete("/v1alpha/participants/%s/credentials/%s".formatted(toBase64(user), vc.getId())) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(notNullValue()); + + var resource = context.getCredential(vc.getId()); + assertThat(resource.isEmpty()).isTrue(); + }); + } + + private String toBase64(String s) { + return Base64.getUrlEncoder().encodeToString(s.getBytes()); + } + + private VerifiableCredentialManifest createManifest(String participantId, VerifiableCredential vc) { + return VerifiableCredentialManifest.Builder.newInstance() + .verifiableCredentialContainer(new VerifiableCredentialContainer("rawVc", CredentialFormat.JWT, vc)) + .id(UUID.randomUUID().toString()) + .participantId(participantId) + .build(); + } + + } + + @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 04e0704ec..141ab14d2 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(String id) { + var credential = VerifiableCredential.Builder.newInstance() + .id(id) + .type("test-type") + .issuanceDate(Instant.now()) + .issuer(new Issuer("did:web:issuer")) + .credentialSubject(CredentialSubject.Builder.newInstance().id("id").claim("foo", "bar").build()) + .build(); + var resource = VerifiableCredentialResource.Builder.newInstance() + .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 credential; + } + + public VerifiableCredential createCredential() { + return createCredential(UUID.randomUUID().toString()); + } 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..ba2be4e27 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 @@ -1,6 +1,6 @@ [ { - "version": "1.0.0-alpha", + "version": "1.1.0-alpha", "urlPath": "/v1alpha", "lastUpdated": "2024-08-22T09:20:00Z", "maturity": null 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..49cf4f4d2 --- /dev/null +++ b/extensions/api/identity-api/validators/src/test/java/org/eclipse/edc/identityhub/api/v1/validation/VerifiableCredentialManifestValidatorTest.java @@ -0,0 +1,102 @@ +/* + * 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.assertj.core.api.Assertions.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.succeeded()).isTrue(); + } + + @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.failed()).isTrue(); + assertThat(result.getFailureDetail()).contains("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.failed()).isTrue(); + assertThat(result.getFailureDetail()).contains("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.failed()).isTrue(); + assertThat(result.getFailureDetail()).contains("VerifiableCredential was null"); + } + + @Test + void validate_nullManifest_shouldFailValidation() { + var result = validator.validate(null); + + assertThat(result.failed()).isTrue(); + assertThat(result.getFailureDetail()).contains("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..a33f31701 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,7 +44,7 @@ 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.", @@ -55,6 +55,44 @@ 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 = "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 = 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")), + @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 = "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 = 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(String participantId, VerifiableCredentialManifest manifest, SecurityContext securityContext); + @Operation(description = "Query VerifiableCredentials by type.", operationId = "queryCredentialsByType", @@ -63,7 +101,7 @@ public interface VerifiableCredentialsApi { }, responses = { @ApiResponse(responseCode = "200", description = "The list of VerifiableCredentials.", - content = @Content(array = @ArraySchema(schema = @Schema(implementation = DidDocument.class)))), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = VerifiableCredentialResource.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 = ApiErrorDetail.class)), mediaType = "application/json")), @ApiResponse(responseCode = "400", description = "The query was malformed or was not understood by the server.", @@ -78,7 +116,7 @@ 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.", 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..b27b4d0af 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(@PathParam("participantId") String participantId, VerifiableCredentialManifest manifest, @Context SecurityContext securityContext) { + validator.validate(manifest).orElseThrow(ValidationFailureException::new); + + authorizationService.isAuthorized(securityContext, manifest.getVerifiableCredentialContainer().credential().getId(), ParticipantContext.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) { @@ -87,4 +128,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..b2a6dff5d 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,366 @@ 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() .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(resource.getId()).thenReturn(CREDENTIAL_ID); + when(validator.validate(any())).thenReturn(ValidationResult.success()); + when(authorizationService.isAuthorized(any(), eq(CREDENTIAL_ID), eq(ParticipantContext.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(ParticipantContext.class))).thenReturn(unauthorized("test-message")); + + baseRequest() + .contentType(JSON) + .body(createManifest(createCredential("type"))) + .put() + .then() + .log().ifValidationFails() + .statusCode(403); + } + + @Test + void transformFails_returns400() { + when(validator.validate(any())).thenReturn(ValidationResult.success()); + when(authorizationService.isAuthorized(any(), eq(CREDENTIAL_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"))) + .put() + .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"))) + .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()), any())).thenReturn(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 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/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..4be0a02b7 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 entities 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..3c118aca4 --- /dev/null +++ b/spi/verifiable-credential-spi/src/main/java/org/eclipse/edc/identityhub/spi/verifiablecredentials/model/VerifiableCredentialManifest.java @@ -0,0 +1,118 @@ +/* + * 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; + +/** + * 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() { + 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