diff --git a/client-cli/build.gradle.kts b/client-cli/build.gradle.kts index 8761b9adc..6ba7639ae 100644 --- a/client-cli/build.gradle.kts +++ b/client-cli/build.gradle.kts @@ -8,15 +8,15 @@ plugins { dependencies { api(libs.picocli.core) annotationProcessor(libs.picocli.codegen) - + implementation(project(":core:identity-hub-client")) implementation(project(":extensions:credentials:identity-hub-credentials-jwt")) implementation(project(":extensions:identity-hub-verifier-jwt")) + implementation(edc.core.connector) implementation(edc.ext.identity.did.crypto) implementation(edc.spi.identity.did) implementation(libs.jackson.databind) - implementation(libs.okhttp) implementation(libs.nimbus.jwt) testImplementation(testFixtures(project(":spi:identity-hub-spi"))) diff --git a/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/AddVerifiableCredentialCommand.java b/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/AddVerifiableCredentialCommand.java index 2cc72fe7a..f1c0b79d8 100644 --- a/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/AddVerifiableCredentialCommand.java +++ b/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/AddVerifiableCredentialCommand.java @@ -21,10 +21,17 @@ import org.eclipse.edc.identityhub.credentials.jwt.JwtCredentialEnvelope; import org.eclipse.edc.identityhub.credentials.jwt.JwtCredentialFactory; import org.eclipse.edc.identityhub.spi.credentials.model.Credential; +import org.eclipse.edc.identityhub.spi.credentials.model.CredentialSubject; +import org.eclipse.edc.identityhub.spi.credentials.model.VerifiableCredential; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.ParentCommand; +import java.sql.Date; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Map; +import java.util.UUID; import java.util.concurrent.Callable; import static org.eclipse.edc.identityhub.cli.CryptoUtils.readEcKeyPemFile; @@ -34,6 +41,7 @@ class AddVerifiableCredentialCommand implements Callable { private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final JwtCredentialFactory JWT_CREDENTIAL_FACTORY = new JwtCredentialFactory(MAPPER); @ParentCommand private VerifiableCredentialsCommand command; @@ -41,8 +49,8 @@ class AddVerifiableCredentialCommand implements Callable { @CommandLine.Spec private CommandLine.Model.CommandSpec spec; - @CommandLine.Option(names = { "-c", "--verifiable-credential" }, required = true, description = "Verifiable Credential as JSON") - private String verifiableCredentialJson; + @CommandLine.Option(names = { "-c", "--claims" }, required = true, description = "Claims of the Verifiable Credential") + private String claims; @CommandLine.Option(names = { "-i", "--issuer" }, required = true, description = "DID of the Verifiable Credential issuer") private String issuer; @@ -56,8 +64,8 @@ class AddVerifiableCredentialCommand implements Callable { @Override public Integer call() throws Exception { var out = spec.commandLine().getOut(); - - var credential = toCredential(); + var credentialSubject = createCredentialSubject(); + var credential = toCredential(credentialSubject); var jwt = toJwt(credential); command.cli.identityHubClient.addVerifiableCredential(command.cli.hubUrl, new JwtCredentialEnvelope(jwt)) @@ -68,18 +76,34 @@ public Integer call() throws Exception { return 0; } - private Credential toCredential() { + private CredentialSubject createCredentialSubject() { + Map claimsMap; try { - return MAPPER.readValue(verifiableCredentialJson, Credential.class); + claimsMap = MAPPER.readValue(claims, Map.class); } catch (JsonProcessingException e) { throw new CliException("Error while processing request json."); } + var builder = CredentialSubject.Builder.newInstance() + .id(subject); + claimsMap.forEach(builder::claim); + return builder.build(); + } + + private Credential toCredential(CredentialSubject credentialSubject) { + return Credential.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .issuer(issuer) + .issuanceDate(Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS))) + .context(VerifiableCredential.DEFAULT_CONTEXT) + .type(VerifiableCredential.DEFAULT_TYPE) + .credentialSubject(credentialSubject) + .build(); } private SignedJWT toJwt(Credential credential) { try { var privateKey = readEcKeyPemFile(privateKeyPemFile); - return JwtCredentialFactory.buildSignedJwt(credential, issuer, subject, new EcPrivateKeyWrapper(privateKey), MAPPER); + return JWT_CREDENTIAL_FACTORY.buildSignedJwt(credential, new EcPrivateKeyWrapper(privateKey)); } catch (Exception e) { throw new CliException("Error while signing Verifiable Credential", e); } diff --git a/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/CryptoUtils.java b/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/CryptoUtils.java index 07e2ba551..ebf6f3e07 100644 --- a/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/CryptoUtils.java +++ b/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/CryptoUtils.java @@ -29,6 +29,9 @@ private CryptoUtils() { /** * Read {@link ECKey} from a PEM file. + * + * @throws IOException if file cannot be read. + * @throws JOSEException if {@link ECKey} cannot be parsed from PEM. */ public static ECKey readEcKeyPemFile(String file) throws IOException, JOSEException { var contents = readString(Path.of(file)); diff --git a/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/GetSelfDescriptionCommand.java b/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/GetSelfDescriptionCommand.java deleted file mode 100644 index a95eafe55..000000000 --- a/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/GetSelfDescriptionCommand.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2022 Amadeus - * - * 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 - initial API and implementation - * - */ - -package org.eclipse.edc.identityhub.cli; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import picocli.CommandLine; -import picocli.CommandLine.Command; -import picocli.CommandLine.ParentCommand; - -import java.util.concurrent.Callable; - - -@Command(name = "get", description = "Display Self-Description document.") -class GetSelfDescriptionCommand implements Callable { - - private static final ObjectMapper MAPPER = new ObjectMapper() - .enable(SerializationFeature.INDENT_OUTPUT); - - @ParentCommand - SelfDescriptionCommand command; - - @CommandLine.Spec - private CommandLine.Model.CommandSpec spec; - - @Override - public Integer call() throws Exception { - var out = spec.commandLine().getOut(); - var result = command.cli.identityHubClient.getSelfDescription(command.cli.hubUrl); - if (result.failed()) { - throw new CliException("Error while getting Self-Description: " + result.getFailureDetail()); - } - - MAPPER.writeValue(out, result.getContent()); - out.println(); - return 0; - } -} - - diff --git a/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/IdentityHubCli.java b/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/IdentityHubCli.java index b5b917b10..1e9021cf4 100644 --- a/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/IdentityHubCli.java +++ b/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/IdentityHubCli.java @@ -14,26 +14,37 @@ package org.eclipse.edc.identityhub.cli; -import com.fasterxml.jackson.databind.ObjectMapper; +import dev.failsafe.RetryPolicy; import okhttp3.OkHttpClient; +import okhttp3.Response; +import org.eclipse.edc.connector.core.base.EdcHttpClientImpl; import org.eclipse.edc.identityhub.client.IdentityHubClientImpl; import org.eclipse.edc.identityhub.client.spi.IdentityHubClient; import org.eclipse.edc.identityhub.credentials.jwt.JwtCredentialEnvelopeTransformer; import org.eclipse.edc.identityhub.spi.credentials.transformer.CredentialEnvelopeTransformerRegistryImpl; import org.eclipse.edc.spi.monitor.ConsoleMonitor; +import org.eclipse.edc.spi.types.TypeManager; import picocli.CommandLine; import picocli.CommandLine.Command; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; + @Command(name = "identity-hub-cli", mixinStandardHelpOptions = true, description = "Client utility for MVD identity hub.", subcommands = { VerifiableCredentialsCommand.class, - SelfDescriptionCommand.class }) public class IdentityHubCli { @CommandLine.Option(names = { "-s", "--identity-hub-url" }, required = true, description = "Identity Hub URL", defaultValue = "http://localhost:8181/api/identity-hub") String hubUrl; + private static final int RETRIES = 3; + private static final int CONNECT_TIMEOUT_SECONDS = 30; + private static final int READ_TIMEOUT_SECONDS = 30; + private static final int MIN_BACKOFF_MILLIS = 500; + private static final int MAX_BACKOFF_MILLIS = 10000; + IdentityHubClient identityHubClient; public static void main(String... args) { @@ -53,13 +64,24 @@ private int executionStrategy(CommandLine.ParseResult parseResult) { } private void init() { - var okHttpClient = new OkHttpClient.Builder().build(); - var objectMapper = new ObjectMapper(); + var typeManager = new TypeManager(); var monitor = new ConsoleMonitor(); + var okHttpClient = new OkHttpClient.Builder() + .connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .build(); + + var retryPolicy = RetryPolicy.builder() + .withMaxRetries(RETRIES) + .withBackoff(MIN_BACKOFF_MILLIS, MAX_BACKOFF_MILLIS, ChronoUnit.MILLIS) + .build(); + + var client = new EdcHttpClientImpl(okHttpClient, retryPolicy, monitor); + var registry = new CredentialEnvelopeTransformerRegistryImpl(); - registry.register(new JwtCredentialEnvelopeTransformer(objectMapper)); + registry.register(new JwtCredentialEnvelopeTransformer(typeManager.getMapper())); - identityHubClient = new IdentityHubClientImpl(okHttpClient, objectMapper, monitor, registry); + identityHubClient = new IdentityHubClientImpl(client, typeManager, registry); } } diff --git a/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/SelfDescriptionCommand.java b/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/SelfDescriptionCommand.java deleted file mode 100644 index 2193c5bbe..000000000 --- a/client-cli/src/main/java/org/eclipse/edc/identityhub/cli/SelfDescriptionCommand.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2022 Microsoft Corporation - * - * 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: - * Microsoft Corporation - initial API and implementation - * - */ - -package org.eclipse.edc.identityhub.cli; - -import picocli.CommandLine.Command; -import picocli.CommandLine.ParentCommand; - -@Command(name = "sd", mixinStandardHelpOptions = true, - description = "Manage Self-Description.", - subcommands = { - GetSelfDescriptionCommand.class, - }) -class SelfDescriptionCommand { - @ParentCommand - IdentityHubCli cli; -} diff --git a/client-cli/src/test/java/org/eclipse/edc/identityhub/cli/CliTestUtils.java b/client-cli/src/test/java/org/eclipse/edc/identityhub/cli/CliTestUtils.java index ab8217fcc..6ab484eb3 100644 --- a/client-cli/src/test/java/org/eclipse/edc/identityhub/cli/CliTestUtils.java +++ b/client-cli/src/test/java/org/eclipse/edc/identityhub/cli/CliTestUtils.java @@ -24,7 +24,7 @@ import static org.eclipse.edc.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.buildSignedJwt; class CliTestUtils { - + public static final String PUBLIC_KEY_PATH = "src/test/resources/test-public-key.pem"; public static final String PRIVATE_KEY_PATH = "src/test/resources/test-private-key.pem"; public static final ECKey PUBLIC_KEY; @@ -44,10 +44,9 @@ private CliTestUtils() { public static SignedJWT toJwtVerifiableCredential(Credential vc) { try { - return buildSignedJwt(vc, - "identity-hub-test-issuer", - "identity-hub-test-subject", + vc.getIssuer(), + vc.getCredentialSubject().getId(), PRIVATE_KEY); } catch (Exception e) { throw new RuntimeException(e); diff --git a/client-cli/src/test/java/org/eclipse/edc/identityhub/cli/VerifiableCredentialsCommandTest.java b/client-cli/src/test/java/org/eclipse/edc/identityhub/cli/VerifiableCredentialsCommandTest.java index 1eadfe790..16544d90e 100644 --- a/client-cli/src/test/java/org/eclipse/edc/identityhub/cli/VerifiableCredentialsCommandTest.java +++ b/client-cli/src/test/java/org/eclipse/edc/identityhub/cli/VerifiableCredentialsCommandTest.java @@ -21,6 +21,7 @@ import org.eclipse.edc.identityhub.credentials.jwt.JwtCredentialEnvelope; import org.eclipse.edc.identityhub.junit.testfixtures.VerifiableCredentialTestUtil; import org.eclipse.edc.identityhub.spi.credentials.model.Credential; +import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.spi.types.TypeManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -37,7 +38,6 @@ import static org.eclipse.edc.identityhub.cli.CliTestUtils.PRIVATE_KEY_PATH; import static org.eclipse.edc.identityhub.cli.CliTestUtils.toJwtVerifiableCredential; import static org.eclipse.edc.identityhub.cli.CliTestUtils.verifyVerifiableCredentialSignature; -import static org.eclipse.edc.spi.response.StatusResult.success; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doReturn; @@ -67,29 +67,10 @@ void setUp() { cmd.setErr(new PrintWriter(err)); } - @Test - void getSelfDescription() throws JsonProcessingException { - var selfDescription = MAPPER.createObjectNode(); - selfDescription.put("key1", "value1"); - // arrange - when(app.identityHubClient.getSelfDescription(app.hubUrl)).thenReturn(success(selfDescription)); - - // act - var exitCode = executeGetSelfDescription(); - var outContent = out.toString(); - var errContent = err.toString(); - - // assert - assertThat(exitCode).isZero(); - assertThat(errContent).isEmpty(); - - assertThat(MAPPER.readTree(outContent)).usingRecursiveComparison().isEqualTo(selfDescription); - } - @Test void list() throws Exception { // arrange - when(app.identityHubClient.getVerifiableCredentials(app.hubUrl)).thenReturn(success(List.of(VC1, VC2))); + when(app.identityHubClient.getVerifiableCredentials(app.hubUrl)).thenReturn(Result.success(List.of(VC1, VC2))); // act var exitCode = executeList(); @@ -114,12 +95,12 @@ void list() throws Exception { @Test void add() throws Exception { // arrange - var json = MAPPER.writeValueAsString(CREDENTIAL1); + var json = MAPPER.writeValueAsString(CREDENTIAL1.getCredentialSubject().getClaims()); var vcArgCaptor = ArgumentCaptor.forClass(JwtCredentialEnvelope.class); - doReturn(success()).when(app.identityHubClient).addVerifiableCredential(eq(app.hubUrl), vcArgCaptor.capture()); + doReturn(Result.success()).when(app.identityHubClient).addVerifiableCredential(eq(app.hubUrl), vcArgCaptor.capture()); // act - var exitCode = executeAdd(json, PRIVATE_KEY_PATH); + var exitCode = executeAdd(json, CREDENTIAL1.getIssuer(), CREDENTIAL1.getCredentialSubject().getId(), PRIVATE_KEY_PATH); var outContent = out.toString(); var errContent = err.toString(); @@ -138,7 +119,10 @@ void add() throws Exception { var result = envelope.toVerifiableCredential(MAPPER); assertThat(result.succeeded()).isTrue(); - assertThat(result.getContent().getItem()).usingRecursiveComparison().isEqualTo(CREDENTIAL1); + assertThat(result.getContent().getItem()).usingRecursiveComparison() + .ignoringFields("id") + .ignoringFields("issuanceDate") + .isEqualTo(CREDENTIAL1); } @Test @@ -147,7 +131,7 @@ void add_invalidJson_fails() { var json = "Invalid json"; // act - var exitCode = executeAdd(json, PRIVATE_KEY_PATH); + var exitCode = executeAdd(json, "issuer", "subject", PRIVATE_KEY_PATH); var outContent = out.toString(); var errContent = err.toString(); @@ -160,10 +144,10 @@ void add_invalidJson_fails() { @Test void add_invalidPrivateKey_fails() throws JsonProcessingException { // arrange - var json = MAPPER.writeValueAsString(CREDENTIAL1); + var json = MAPPER.writeValueAsString(CREDENTIAL1.getCredentialSubject().getClaims()); // act - var exitCode = executeAdd(json, "non-existing-key"); + var exitCode = executeAdd(json, CREDENTIAL1.getIssuer(), CREDENTIAL2.getCredentialSubject().getId(), "non-existing-key"); var outContent = out.toString(); var errContent = err.toString(); @@ -173,15 +157,11 @@ void add_invalidPrivateKey_fails() throws JsonProcessingException { assertThat(errContent).contains("Error while signing Verifiable Credential"); } - private int executeGetSelfDescription() { - return cmd.execute("-s", HUB_URL, "sd", "get"); - } - private int executeList() { return cmd.execute("-s", HUB_URL, "vc", "list"); } - private int executeAdd(String json, String privateKey) { - return cmd.execute("-s", HUB_URL, "vc", "add", "-c", json, "-i", "identity-hub-test-issuer", "-b", "identity-hub-test-subject", "-k", privateKey); + private int executeAdd(String json, String issuer, String subject, String privateKey) { + return cmd.execute("-s", HUB_URL, "vc", "add", "-c", json, "-i", issuer, "-b", subject, "-k", privateKey); } } diff --git a/core/identity-hub-client/build.gradle.kts b/core/identity-hub-client/build.gradle.kts index c865a53ed..0d3439745 100644 --- a/core/identity-hub-client/build.gradle.kts +++ b/core/identity-hub-client/build.gradle.kts @@ -20,8 +20,7 @@ plugins { dependencies { api(project(":spi:identity-hub-spi")) api(project(":spi:identity-hub-client-spi")) - api(edc.spi.core) - implementation(edc.ext.http) + api(edc.spi.http) implementation(libs.okhttp) implementation(libs.jackson.databind) implementation(edc.spi.core) @@ -37,8 +36,7 @@ dependencies { publishing { publications { - create("identity-hub-client") { - artifactId = "identity-hub-client" + create(project.name) { from(components["java"]) } } diff --git a/core/identity-hub-client/src/main/java/org/eclipse/edc/identityhub/client/IdentityHubClientImpl.java b/core/identity-hub-client/src/main/java/org/eclipse/edc/identityhub/client/IdentityHubClientImpl.java index da04b9524..fdec59e09 100644 --- a/core/identity-hub-client/src/main/java/org/eclipse/edc/identityhub/client/IdentityHubClientImpl.java +++ b/core/identity-hub-client/src/main/java/org/eclipse/edc/identityhub/client/IdentityHubClientImpl.java @@ -15,10 +15,6 @@ package org.eclipse.edc.identityhub.client; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; @@ -31,12 +27,11 @@ import org.eclipse.edc.identityhub.spi.model.Record; import org.eclipse.edc.identityhub.spi.model.RequestObject; import org.eclipse.edc.identityhub.spi.model.ResponseObject; -import org.eclipse.edc.spi.EdcException; -import org.eclipse.edc.spi.monitor.Monitor; -import org.eclipse.edc.spi.response.ResponseStatus; -import org.eclipse.edc.spi.response.StatusResult; +import org.eclipse.edc.spi.http.EdcHttpClient; import org.eclipse.edc.spi.result.AbstractResult; import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.types.TypeManager; +import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.time.Instant; @@ -48,153 +43,109 @@ import static java.lang.String.format; import static org.eclipse.edc.identityhub.spi.model.WebNodeInterfaceMethod.COLLECTIONS_QUERY; import static org.eclipse.edc.identityhub.spi.model.WebNodeInterfaceMethod.COLLECTIONS_WRITE; +import static org.eclipse.edc.spi.http.FallbackFactories.statusMustBe; +import static org.eclipse.edc.spi.result.Result.failure; public class IdentityHubClientImpl implements IdentityHubClient { - public static final String DATA_FORMAT = "application/vc+jwt"; - private final OkHttpClient httpClient; - private final ObjectMapper objectMapper; - private final Monitor monitor; + + private final EdcHttpClient httpClient; + private final TypeManager typeManager; private final CredentialEnvelopeTransformerRegistry transformerRegistry; - public IdentityHubClientImpl(OkHttpClient httpClient, ObjectMapper objectMapper, Monitor monitor, CredentialEnvelopeTransformerRegistry transformerRegistry) { + public IdentityHubClientImpl(EdcHttpClient httpClient, TypeManager typeManager, CredentialEnvelopeTransformerRegistry transformerRegistry) { this.httpClient = httpClient; - this.objectMapper = objectMapper; - this.monitor = monitor; + this.typeManager = typeManager; this.transformerRegistry = transformerRegistry; } - private static Descriptor.Builder defaultDescriptor(String method) { - return Descriptor.Builder.newInstance() - .method(method); - } - - private static StatusResult identityHubCallError(Response response) throws IOException { - return StatusResult.failure(ResponseStatus.FATAL_ERROR, format("IdentityHub error response code: %s, response headers: %s, response body: %s", response.code(), response.headers(), response.body().string())); - } - - @Override - public StatusResult getSelfDescription(String hubBaseUrl) { - try (var response = httpClient.newCall( - new Request.Builder() - .url(hubBaseUrl + "/self-description") - .get() - .build()) - .execute()) { - - return (response.code() == 200) ? - StatusResult.success(objectMapper.readTree(response.body().byteStream())) : - identityHubCallError(response); - } catch (IOException e) { - return StatusResult.failure(ResponseStatus.FATAL_ERROR, e.getMessage()); - } - } - @Override - public StatusResult> getVerifiableCredentials(String hubBaseUrl) { - var descriptor = defaultDescriptor(COLLECTIONS_QUERY.getName()).build(); - try (var response = httpClient.newCall( - new Request.Builder() - .url(hubBaseUrl) - .post(buildRequestBody(descriptor)) - .build()) - .execute()) { - - if (response.code() != 200) { - return identityHubCallError(response); - } - - try (var body = response.body()) { - var responseObject = objectMapper.readValue(body.string(), ResponseObject.class); - var verifiableCredentials = responseObject - .getReplies() - .stream() - .flatMap(r -> r.getEntries().stream()) - .map(this::parse) - .filter(AbstractResult::succeeded) - .map(AbstractResult::getContent) - .collect(Collectors.toList()); - return StatusResult.success(verifiableCredentials); - } - - - } catch (IOException e) { - return StatusResult.failure(ResponseStatus.FATAL_ERROR, e.getMessage()); - } - + public Result> getVerifiableCredentials(String hubBaseUrl) { + var descriptor = Descriptor.Builder.newInstance() + .method(COLLECTIONS_QUERY.getName()) + .build(); + var body = toRequestBody(descriptor); + var request = new Request.Builder() + .url(hubBaseUrl) + .post(body) + .build(); + return httpClient.execute(request, List.of(statusMustBe(200)), this::extractCredentials); } - + @Override - public StatusResult addVerifiableCredential(String hubBaseUrl, CredentialEnvelope verifiableCredential) { + public Result addVerifiableCredential(String hubBaseUrl, CredentialEnvelope verifiableCredential) { var transformer = transformerRegistry.resolve(verifiableCredential.format()); if (transformer == null) { - return StatusResult.failure(ResponseStatus.FATAL_ERROR, format("Transformer not found for format %s", verifiableCredential.format())); + return failure(format("Transformer not found for format %s", verifiableCredential.format())); } Result result = transformer.serialize(verifiableCredential); - if (result.failed()) { - return StatusResult.failure(ResponseStatus.FATAL_ERROR, result.getFailureDetail()); + return failure(result.getFailureDetail()); } - var descriptor = defaultDescriptor(COLLECTIONS_WRITE.getName()) + var descriptor = Descriptor.Builder.newInstance() + .method(COLLECTIONS_WRITE.getName()) .recordId(UUID.randomUUID().toString()) - .dataFormat(DATA_FORMAT) + .dataFormat(verifiableCredential.format()) .dateCreated(Instant.now().getEpochSecond()) // TODO: this should be passed from input .build(); - try (var response = httpClient.newCall(new Request.Builder() - .url(hubBaseUrl) - .post(buildRequestBody(descriptor, result.getContent())) - .build()) - .execute()) { - if (response.code() != 200) { - return identityHubCallError(response); - } - - try (var body = response.body()) { - var responseObject = objectMapper.readValue(body.string(), ResponseObject.class); - - // If the status of Response object is not success return error - if (responseObject.getStatus() != null && !responseObject.getStatus().isSuccess()) { - return StatusResult.failure(ResponseStatus.FATAL_ERROR, responseObject.getStatus().getDetail()); - } - - // If the status of one of the replies is not success return error - return responseObject.getReplies() - .stream() - .map(MessageResponseObject::getStatus) - .filter(status -> !status.isSuccess()) - .map(status -> StatusResult.failure(ResponseStatus.FATAL_ERROR, status.getDetail())) - .findFirst() - .orElseGet(() -> StatusResult.success()); - - } + var request = new Request.Builder() + .url(hubBaseUrl) + .post(toRequestBody(descriptor, result.getContent())) + .build(); - } catch (IOException e) { - return StatusResult.failure(ResponseStatus.FATAL_ERROR, e.getMessage()); - } + return httpClient.execute(request, List.of(statusMustBe(200)), this::handleAddResponse); } private Result parse(Object entry) { - var record = objectMapper.convertValue(entry, Record.class); + var record = typeManager.getMapper().convertValue(entry, Record.class); var t = transformerRegistry.resolve(record.getDataFormat()); if (t == null) { - return Result.failure(format("Transformer not found for format %s", record.getDataFormat())); + return failure(format("Transformer not found for format %s", record.getDataFormat())); } return t.parse(record.getData()); } - private RequestBody buildRequestBody(Descriptor descriptor) { - try { - return buildRequestBody(descriptor, null); - } catch (JsonProcessingException e) { - throw new EdcException(e); // Should never happen. + private Result handleAddResponse(Response response) { + var result = extractResponseObject(response); + if (result.failed()) { + return Result.failure(result.getFailureMessages()); } + var responseObject = result.getContent(); + // If the status of Response object is not success return error + if (responseObject.getStatus() != null && !responseObject.getStatus().isSuccess()) { + return Result.failure(responseObject.getStatus().getDetail()); + } + + // If the status of one of the replies is not success return error + return responseObject.getReplies() + .stream() + .map(MessageResponseObject::getStatus) + .filter(status -> !status.isSuccess()) + .map(status -> Result.failure(status.getDetail())) + .findFirst() + .orElseGet(Result::success); + } + + private Result> extractCredentials(Response response) { + return extractResponseObject(response) + .map(ResponseObject::getReplies) + .map(replies -> replies.stream() + .flatMap(r -> r.getEntries().stream()) + .map(this::parse) + .filter(AbstractResult::succeeded) + .map(AbstractResult::getContent) + .collect(Collectors.toList())); + } + + private RequestBody toRequestBody(Descriptor descriptor) { + return toRequestBody(descriptor, null); } - private RequestBody buildRequestBody(Descriptor descriptor, byte[] data) throws JsonProcessingException { + private RequestBody toRequestBody(Descriptor descriptor, byte[] data) { var requestObject = RequestObject.Builder.newInstance() .messages(List.of(MessageRequestObject.Builder.newInstance() .descriptor(descriptor) @@ -202,7 +153,21 @@ private RequestBody buildRequestBody(Descriptor descriptor, byte[] data) throws .build()) ) .build(); - var payload = objectMapper.writeValueAsString(requestObject); + var payload = typeManager.writeValueAsString(requestObject); return RequestBody.create(payload, okhttp3.MediaType.get("application/json")); } + + @NotNull + private Result extractResponseObject(Response response) { + try (var body = response.body()) { + if (body != null) { + return Result.success(typeManager.readValue(body.string(), ResponseObject.class)); + } else { + return failure("Body is null"); + } + } catch (IOException e) { + return failure("Cannot read response body as String: " + e.getMessage()); + } + + } } diff --git a/core/identity-hub-client/src/test/java/org/eclipse/edc/identityhub/client/IdentityHubClientImplIntegrationTest.java b/core/identity-hub-client/src/test/java/org/eclipse/edc/identityhub/client/IdentityHubClientImplIntegrationTest.java index ddefd0cb4..67b9c48ae 100644 --- a/core/identity-hub-client/src/test/java/org/eclipse/edc/identityhub/client/IdentityHubClientImplIntegrationTest.java +++ b/core/identity-hub-client/src/test/java/org/eclipse/edc/identityhub/client/IdentityHubClientImplIntegrationTest.java @@ -14,7 +14,6 @@ package org.eclipse.edc.identityhub.client; -import com.fasterxml.jackson.databind.ObjectMapper; import org.eclipse.edc.identityhub.client.spi.IdentityHubClient; import org.eclipse.edc.identityhub.credentials.jwt.JwtCredentialEnvelope; import org.eclipse.edc.identityhub.credentials.jwt.JwtCredentialEnvelopeTransformer; @@ -23,7 +22,7 @@ import org.eclipse.edc.identityhub.spi.credentials.transformer.CredentialEnvelopeTransformerRegistry; import org.eclipse.edc.junit.extensions.EdcExtension; import org.eclipse.edc.junit.testfixtures.TestUtils; -import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.types.TypeManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -42,23 +41,15 @@ class IdentityHubClientImplIntegrationTest { private static final String API_URL = "http://localhost:8181/api/identity-hub"; private static final Credential VERIFIABLE_CREDENTIAL = generateCredential(); - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final TypeManager TYPE_MANAGER = new TypeManager(); private final CredentialEnvelopeTransformerRegistry registry = mock(CredentialEnvelopeTransformerRegistry.class); private IdentityHubClient client; @BeforeEach void setUp() { - var okHttpClient = TestUtils.testOkHttpClient(); - when(registry.resolve(any())).thenReturn(new JwtCredentialEnvelopeTransformer(OBJECT_MAPPER)); - client = new IdentityHubClientImpl(okHttpClient, OBJECT_MAPPER, mock(Monitor.class), registry); - } - - @Test - void getSelfDescription() { - var statusResult = client.getSelfDescription(API_URL); - - assertThat(statusResult.succeeded()).isTrue(); - assertThat(statusResult.getContent().get("selfDescriptionCredential")).isNotNull(); + var okHttpClient = TestUtils.testHttpClient(); + when(registry.resolve(any())).thenReturn(new JwtCredentialEnvelopeTransformer(TYPE_MANAGER.getMapper())); + client = new IdentityHubClientImpl(okHttpClient, TYPE_MANAGER, registry); } @Test diff --git a/core/identity-hub-client/src/test/java/org/eclipse/edc/identityhub/client/IdentityHubClientImplTest.java b/core/identity-hub-client/src/test/java/org/eclipse/edc/identityhub/client/IdentityHubClientImplTest.java index d611dd2b9..1cc3786ee 100644 --- a/core/identity-hub-client/src/test/java/org/eclipse/edc/identityhub/client/IdentityHubClientImplTest.java +++ b/core/identity-hub-client/src/test/java/org/eclipse/edc/identityhub/client/IdentityHubClientImplTest.java @@ -14,13 +14,12 @@ package org.eclipse.edc.identityhub.client; -import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.Interceptor; import okhttp3.MediaType; -import okhttp3.OkHttpClient; import okhttp3.Protocol; import okhttp3.Response; import okhttp3.ResponseBody; +import org.eclipse.edc.identityhub.credentials.jwt.JwtCredentialConstants; import org.eclipse.edc.identityhub.credentials.jwt.JwtCredentialEnvelope; import org.eclipse.edc.identityhub.credentials.jwt.JwtCredentialEnvelopeTransformer; import org.eclipse.edc.identityhub.junit.testfixtures.VerifiableCredentialTestUtil; @@ -30,9 +29,10 @@ import org.eclipse.edc.identityhub.spi.model.Record; import org.eclipse.edc.identityhub.spi.model.RequestStatus; import org.eclipse.edc.identityhub.spi.model.ResponseObject; -import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.junit.testfixtures.TestUtils; import org.eclipse.edc.spi.response.ResponseStatus; import org.eclipse.edc.spi.response.StatusResult; +import org.eclipse.edc.spi.types.TypeManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -50,59 +50,13 @@ class IdentityHubClientImplTest { private static final String HUB_URL = "http://some.test.url"; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final TypeManager TYPE_MANAGER = new TypeManager(); private final CredentialEnvelopeTransformerRegistry registry = mock(CredentialEnvelopeTransformerRegistry.class); @BeforeEach void setup() { - when(registry.resolve(any())).thenReturn(new JwtCredentialEnvelopeTransformer(OBJECT_MAPPER)); - } - - @Test - void getSelfDescription() { - var selfDescription = OBJECT_MAPPER.createObjectNode(); - selfDescription.put("key1", "value1"); - - Interceptor interceptor = chain -> { - var request = chain.request(); - return new Response.Builder() - .body(ResponseBody.create(OBJECT_MAPPER.writeValueAsString(selfDescription), MediaType.get("application/json"))) - .request(request) - .protocol(Protocol.HTTP_2) - .code(200) - .message("") - .build(); - }; - - var client = createClient(interceptor); - var statusResult = client.getSelfDescription(HUB_URL); - assertThat(statusResult.succeeded()).isTrue(); - assertThat(statusResult.getContent()).isEqualTo(selfDescription); - } - - @Test - void getSelfDescriptionServerError() { - var errorMessage = "test-error-message"; - var body = "{}"; - var code = 500; - - Interceptor interceptor = chain -> { - var request = chain.request(); - return new Response.Builder() - .body(ResponseBody.create(body, MediaType.get("application/json"))) - .request(request) - .protocol(Protocol.HTTP_2) - .code(code) - .message(errorMessage) - .build(); - }; - - var client = createClient(interceptor); - var statusResult = client.getSelfDescription(HUB_URL); - - var expectedResult = StatusResult.failure(ResponseStatus.FATAL_ERROR, String.format("IdentityHub error response code: %s, response headers: , response body: %s", code, body)); - assertThat(statusResult).usingRecursiveComparison().isEqualTo(expectedResult); + when(registry.resolve(any())).thenReturn(new JwtCredentialEnvelopeTransformer(TYPE_MANAGER.getMapper())); } @Test @@ -112,7 +66,7 @@ void getVerifiableCredentials() { var record = Record.Builder.newInstance() .id(UUID.randomUUID().toString()) - .dataFormat(IdentityHubClientImpl.DATA_FORMAT) + .dataFormat(JwtCredentialConstants.DATA_FORMAT) .createdAt(System.currentTimeMillis()) .data(jws.serialize().getBytes(StandardCharsets.UTF_8)) .build(); @@ -126,7 +80,7 @@ var record = Record.Builder.newInstance() .status(RequestStatus.OK) .replies(List.of(replies)) .build(); - var body = ResponseBody.create(OBJECT_MAPPER.writeValueAsString(responseObject), MediaType.get("application/json")); + var body = ResponseBody.create(TYPE_MANAGER.writeValueAsString(responseObject), MediaType.get("application/json")); return new Response.Builder() .body(body) @@ -147,7 +101,7 @@ var record = Record.Builder.newInstance() void getVerifiableCredentialsServerError() { var errorMessage = "test-message"; - var body = "{}"; + var body = "{\"foo\":\"bar\"}"; var code = 500; Interceptor interceptor = chain -> { @@ -162,10 +116,9 @@ void getVerifiableCredentialsServerError() { }; var client = createClient(interceptor); - var statusResult = client.getVerifiableCredentials(HUB_URL); - - var expectedResult = StatusResult.failure(ResponseStatus.FATAL_ERROR, String.format("IdentityHub error response code: %s, response headers: , response body: %s", code, body)); - assertThat(statusResult).usingRecursiveComparison().isEqualTo(expectedResult); + var result = client.getVerifiableCredentials(HUB_URL); + assertThat(result.failed()).isTrue(); + assertThat(result.getFailureDetail()).contains(body); } @Test @@ -184,8 +137,8 @@ void getVerifiableCredentialsDeserializationError() { }; var client = createClient(interceptor); - var statusResult = client.getVerifiableCredentials(HUB_URL); - assertThat(statusResult.fatalError()).isTrue(); + var result = client.getVerifiableCredentials(HUB_URL); + assertThat(result.failed()).isTrue(); } @Test @@ -199,7 +152,7 @@ void addVerifiableCredentialsServerError() { Interceptor interceptor = chain -> { var request = chain.request(); return new Response.Builder() - .body(ResponseBody.create("{}", MediaType.get("application/json"))) + .body(ResponseBody.create(body, MediaType.get("application/json"))) .request(request) .protocol(Protocol.HTTP_2) .code(code) @@ -208,10 +161,9 @@ void addVerifiableCredentialsServerError() { }; var client = createClient(interceptor); - var statusResult = client.addVerifiableCredential(HUB_URL, new JwtCredentialEnvelope(jws)); - - var expectedResult = StatusResult.failure(ResponseStatus.FATAL_ERROR, String.format("IdentityHub error response code: %s, response headers: , response body: %s", code, body)); - assertThat(statusResult).usingRecursiveComparison().isEqualTo(expectedResult); + var result = client.addVerifiableCredential(HUB_URL, new JwtCredentialEnvelope(jws)); + assertThat(result.failed()).isTrue(); + assertThat(result.getFailureDetail()).contains("500"); } @Test @@ -231,10 +183,6 @@ void addVerifiableCredentialsIoException() { } private IdentityHubClientImpl createClient(Interceptor interceptor) { - var okHttpClient = new OkHttpClient.Builder() - .addInterceptor(interceptor) - .build(); - - return new IdentityHubClientImpl(okHttpClient, OBJECT_MAPPER, mock(Monitor.class), registry); + return new IdentityHubClientImpl(TestUtils.testHttpClient(interceptor), TYPE_MANAGER, registry); } } diff --git a/core/identity-hub-verifier/src/main/java/org/eclipse/edc/identityhub/verifier/CredentialsVerifierExtension.java b/core/identity-hub-verifier/src/main/java/org/eclipse/edc/identityhub/verifier/CredentialsVerifierExtension.java index 7816c919d..6c2379df4 100644 --- a/core/identity-hub-verifier/src/main/java/org/eclipse/edc/identityhub/verifier/CredentialsVerifierExtension.java +++ b/core/identity-hub-verifier/src/main/java/org/eclipse/edc/identityhub/verifier/CredentialsVerifierExtension.java @@ -14,7 +14,6 @@ package org.eclipse.edc.identityhub.verifier; -import okhttp3.OkHttpClient; import org.eclipse.edc.iam.did.spi.credentials.CredentialsVerifier; import org.eclipse.edc.identityhub.client.IdentityHubClientImpl; import org.eclipse.edc.identityhub.spi.credentials.transformer.CredentialEnvelopeTransformerRegistry; @@ -22,6 +21,7 @@ import org.eclipse.edc.identityhub.spi.credentials.verifier.CredentialEnvelopeVerifierRegistryImpl; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.http.EdcHttpClient; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; @@ -33,7 +33,7 @@ public class CredentialsVerifierExtension implements ServiceExtension { @Inject - private OkHttpClient httpClient; + private EdcHttpClient httpClient; @Inject private TypeManager typeManager; @@ -41,12 +41,11 @@ public class CredentialsVerifierExtension implements ServiceExtension { @Inject private Monitor monitor; - - private CredentialEnvelopeVerifierRegistry credentialEnvelopeVerifierRegistry; - @Inject private CredentialEnvelopeTransformerRegistry credentialEnvelopeTransformerRegistry; + private CredentialEnvelopeVerifierRegistry credentialEnvelopeVerifierRegistry; + @Override public void initialize(ServiceExtensionContext context) { credentialEnvelopeVerifierRegistry = new CredentialEnvelopeVerifierRegistryImpl(); @@ -59,11 +58,10 @@ public String name() { @Provider public CredentialsVerifier createCredentialsVerifier() { - var client = new IdentityHubClientImpl(httpClient, typeManager.getMapper(), monitor, credentialEnvelopeTransformerRegistry); + var client = new IdentityHubClientImpl(httpClient, typeManager, credentialEnvelopeTransformerRegistry); return new IdentityHubCredentialsVerifier(client, monitor, credentialEnvelopeVerifierRegistry); } - @Provider public CredentialEnvelopeVerifierRegistry credentialVerifierRegistry() { return credentialEnvelopeVerifierRegistry; diff --git a/core/identity-hub-verifier/src/test/java/org/eclipse/edc/identityhub/verifier/IdentityHubCredentialsVerifierTest.java b/core/identity-hub-verifier/src/test/java/org/eclipse/edc/identityhub/verifier/IdentityHubCredentialsVerifierTest.java index a8e70f9a5..6b53a96d6 100644 --- a/core/identity-hub-verifier/src/test/java/org/eclipse/edc/identityhub/verifier/IdentityHubCredentialsVerifierTest.java +++ b/core/identity-hub-verifier/src/test/java/org/eclipse/edc/identityhub/verifier/IdentityHubCredentialsVerifierTest.java @@ -24,8 +24,6 @@ import org.eclipse.edc.identityhub.spi.credentials.verifier.CredentialEnvelopeVerifier; import org.eclipse.edc.identityhub.spi.credentials.verifier.CredentialEnvelopeVerifierRegistry; import org.eclipse.edc.spi.monitor.Monitor; -import org.eclipse.edc.spi.response.ResponseStatus; -import org.eclipse.edc.spi.response.StatusResult; import org.eclipse.edc.spi.result.Result; import org.junit.jupiter.api.Test; @@ -53,7 +51,7 @@ class IdentityHubCredentialsVerifierTest { void getVerifiableCredentials_shouldReturnError_withEmptyRegistry() { var envelope = mock(CredentialEnvelope.class); - when(identityHubClientMock.getVerifiableCredentials(HUB_BASE_URL)).thenReturn(StatusResult.success(List.of(envelope))); + when(identityHubClientMock.getVerifiableCredentials(HUB_BASE_URL)).thenReturn(Result.success(List.of(envelope))); var result = credentialsVerifier.getVerifiedCredentials(DID_DOCUMENT); assertThat(result.failed()).isTrue(); @@ -62,7 +60,7 @@ void getVerifiableCredentials_shouldReturnError_withEmptyRegistry() { @Test void getVerifiableCredentials_shouldReturnError_FailedRequest() { - when(identityHubClientMock.getVerifiableCredentials(HUB_BASE_URL)).thenReturn(StatusResult.failure(ResponseStatus.FATAL_ERROR, "Failure")); + when(identityHubClientMock.getVerifiableCredentials(HUB_BASE_URL)).thenReturn(Result.failure("Failure")); var result = credentialsVerifier.getVerifiedCredentials(DID_DOCUMENT); assertThat(result.failed()).isTrue(); @@ -71,12 +69,12 @@ void getVerifiableCredentials_shouldReturnError_FailedRequest() { @Test void getVerifiableCredentials_shouldReturnEmptyCredentials() { - when(identityHubClientMock.getVerifiableCredentials(HUB_BASE_URL)).thenReturn(StatusResult.success(Collections.emptyList())); + when(identityHubClientMock.getVerifiableCredentials(HUB_BASE_URL)).thenReturn(Result.success(Collections.emptyList())); var result = credentialsVerifier.getVerifiedCredentials(DID_DOCUMENT); assertThat(result.succeeded()).isTrue(); - assertThat(result.getContent().size()).isZero(); + assertThat(result.getContent()).isEmpty(); } @Test @@ -85,7 +83,7 @@ void getVerifiableCredentials_shouldReturnValidCredentials() { var envelope = mock(CredentialEnvelope.class); var verifier = mock(CredentialEnvelopeVerifier.class); - when(identityHubClientMock.getVerifiableCredentials(HUB_BASE_URL)).thenReturn(StatusResult.success(List.of(envelope))); + when(identityHubClientMock.getVerifiableCredentials(HUB_BASE_URL)).thenReturn(Result.success(List.of(envelope))); when(verifierRegistry.resolve(any())).thenReturn(verifier); var credential = VerifiableCredentialTestUtil.generateCredential(); when(verifier.verify(envelope, DID_DOCUMENT)).thenReturn(Result.success(credential)); diff --git a/extensions/credentials/identity-hub-credentials-jwt/src/main/java/org/eclipse/edc/identityhub/credentials/jwt/JwtCredentialFactory.java b/extensions/credentials/identity-hub-credentials-jwt/src/main/java/org/eclipse/edc/identityhub/credentials/jwt/JwtCredentialFactory.java index 0511c0364..d991d86b7 100644 --- a/extensions/credentials/identity-hub-credentials-jwt/src/main/java/org/eclipse/edc/identityhub/credentials/jwt/JwtCredentialFactory.java +++ b/extensions/credentials/identity-hub-credentials-jwt/src/main/java/org/eclipse/edc/identityhub/credentials/jwt/JwtCredentialFactory.java @@ -14,7 +14,6 @@ package org.eclipse.edc.identityhub.credentials.jwt; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; @@ -31,7 +30,10 @@ public class JwtCredentialFactory { - private JwtCredentialFactory() { + private final ObjectMapper mapper; + + public JwtCredentialFactory(ObjectMapper mapper) { + this.mapper = mapper; } /** @@ -51,22 +53,19 @@ private JwtCredentialFactory() { * } * * @param credential the credential - * @param issuer issuer of the credential - * @param subject credential subject * @param privateKey private key for signing the JWT - * @param mapper object mapper * @return the verifiable credential in JWT format. */ - public static SignedJWT buildSignedJwt(Credential credential, String issuer, String subject, PrivateKeyWrapper privateKey, ObjectMapper mapper) throws JOSEException, ParseException, JsonProcessingException { + public SignedJWT buildSignedJwt(Credential credential, PrivateKeyWrapper privateKey) throws JOSEException, ParseException { var jwsHeader = new JWSHeader.Builder(JWSAlgorithm.ES256).build(); // this step of preparatory mapping is required as nimbus relies on Gson which will not respect the format annotation defined in the @Credential object. var mapped = mapper.convertValue(credential, Map.class); var claims = new JWTClaimsSet.Builder() .claim(VERIFIABLE_CREDENTIALS_KEY, mapped) - .issuer(issuer) + .issuer(credential.getIssuer()) .issueTime(credential.getIssuanceDate()) .expirationTime(credential.getExpirationDate()) - .subject(subject) + .subject(credential.getCredentialSubject().getId()) .build(); var jws = new SignedJWT(jwsHeader, claims); diff --git a/extensions/credentials/identity-hub-credentials-jwt/src/test/java/org/eclipse/edc/identityhub/credentials/jwt/JwtCredentialFactoryTest.java b/extensions/credentials/identity-hub-credentials-jwt/src/test/java/org/eclipse/edc/identityhub/credentials/jwt/JwtCredentialFactoryTest.java index fcd2195bc..eaa91300b 100644 --- a/extensions/credentials/identity-hub-credentials-jwt/src/test/java/org/eclipse/edc/identityhub/credentials/jwt/JwtCredentialFactoryTest.java +++ b/extensions/credentials/identity-hub-credentials-jwt/src/test/java/org/eclipse/edc/identityhub/credentials/jwt/JwtCredentialFactoryTest.java @@ -30,32 +30,29 @@ class JwtCredentialFactoryTest { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final Credential CREDENTIAL = generateCredential(); + + private JwtCredentialFactory jwtCredentialFactory; private EcPrivateKeyWrapper privateKey; private EcPublicKeyWrapper publicKey; @BeforeEach public void setUp() { var key = generateEcKey(); + jwtCredentialFactory = new JwtCredentialFactory(OBJECT_MAPPER); privateKey = new EcPrivateKeyWrapper(key); publicKey = new EcPublicKeyWrapper(key); } @Test void buildSignedJwt_success() throws Exception { - // Arrange - var issuer = "test-issuer"; - var subject = "test-subject"; - - // Act - var signedJwt = JwtCredentialFactory.buildSignedJwt(CREDENTIAL, issuer, subject, privateKey, OBJECT_MAPPER); + var signedJwt = jwtCredentialFactory.buildSignedJwt(CREDENTIAL, privateKey); - // Assert boolean result = signedJwt.verify(publicKey.verifier()); assertThat(result).isTrue(); assertThat(signedJwt.getJWTClaimsSet().getClaims()) - .containsEntry("iss", issuer) - .containsEntry("sub", subject) + .containsEntry("iss", CREDENTIAL.getIssuer()) + .containsEntry("sub", CREDENTIAL.getCredentialSubject().getId()) .extractingByKey(VERIFIABLE_CREDENTIALS_KEY) .satisfies(c -> assertThat(OBJECT_MAPPER.convertValue(c, Credential.class)) .usingRecursiveComparison() diff --git a/extensions/identity-hub-api/build.gradle.kts b/extensions/identity-hub-api/build.gradle.kts index 3abc40c94..d4e810be8 100644 --- a/extensions/identity-hub-api/build.gradle.kts +++ b/extensions/identity-hub-api/build.gradle.kts @@ -39,8 +39,7 @@ dependencies { publishing { publications { - create("identity-hub-api") { - artifactId = "identity-hub-api" + create(project.name) { from(components["java"]) } } diff --git a/extensions/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/IdentityHubApiExtension.java b/extensions/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/IdentityHubApiExtension.java index 63b9881cc..7c2aa0164 100644 --- a/extensions/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/IdentityHubApiExtension.java +++ b/extensions/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/IdentityHubApiExtension.java @@ -15,11 +15,10 @@ package org.eclipse.edc.identityhub.api; import org.eclipse.edc.identityhub.api.controller.IdentityHubController; -import org.eclipse.edc.identityhub.selfdescription.SelfDescriptionLoader; import org.eclipse.edc.identityhub.spi.processor.MessageProcessorRegistry; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; -import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.eclipse.edc.web.spi.WebServer; @@ -27,8 +26,6 @@ import org.eclipse.edc.web.spi.configuration.WebServiceConfigurer; import org.eclipse.edc.web.spi.configuration.WebServiceSettings; -import java.util.Optional; - /** * EDC extension for Identity Hub API */ @@ -36,9 +33,7 @@ public class IdentityHubApiExtension implements ServiceExtension { public static final String NAME = "Identity Hub API"; - @Setting - private static final String SELF_DESCRIPTION_DOCUMENT_PATH_SETTING = "edc.self.description.document.path"; - private static final String DEFAULT_SELF_DESCRIPTION_FILE_NAME = "default-self-description.json"; + private static final String IDENTITY_CONTEXT_ALIAS = "identity"; private static final String DEFAULT_IDENTITY_API_CONTEXT_PATH = "/api/v1/identity"; private static final int DEFAULT_IDENTITY_API_PORT = 8188; @@ -59,6 +54,8 @@ public class IdentityHubApiExtension implements ServiceExtension { @Inject private WebServer webServer; + private IdentityHubApiConfiguration configuration; + @Override public String name() { return NAME; @@ -66,17 +63,14 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { - var mapper = context.getTypeManager().getMapper(); - var loader = new SelfDescriptionLoader(mapper); - var selfDescription = Optional.ofNullable(context.getSetting(SELF_DESCRIPTION_DOCUMENT_PATH_SETTING, null)) - .map(loader::fromFile) - .orElse(loader.fromClasspath(DEFAULT_SELF_DESCRIPTION_FILE_NAME)); - var identityHubController = new IdentityHubController(messageProcessorRegistry, selfDescription); - + var identityHubController = new IdentityHubController(messageProcessorRegistry); var webServiceConfig = configurer.configure(context, webServer, SETTINGS); - - context.registerService(IdentityHubApiConfiguration.class, new IdentityHubApiConfiguration(webServiceConfig)); + configuration = new IdentityHubApiConfiguration(webServiceConfig); webService.registerResource(webServiceConfig.getContextAlias(), identityHubController); } + @Provider + public IdentityHubApiConfiguration identityHubApiConfiguration() { + return configuration; + } } diff --git a/extensions/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/controller/IdentityHubController.java b/extensions/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/controller/IdentityHubController.java index f8e6711bb..d618c0407 100644 --- a/extensions/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/controller/IdentityHubController.java +++ b/extensions/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/controller/IdentityHubController.java @@ -14,11 +14,9 @@ package org.eclipse.edc.identityhub.api.controller; -import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; @@ -46,11 +44,9 @@ public class IdentityHubController { private final MessageProcessorRegistry messageProcessorRegistry; - private final JsonNode selfDescription; - public IdentityHubController(MessageProcessorRegistry messageProcessorRegistry, JsonNode selfDescription) { + public IdentityHubController(MessageProcessorRegistry messageProcessorRegistry) { this.messageProcessorRegistry = messageProcessorRegistry; - this.selfDescription = selfDescription; } @Operation(description = "A Decentralized Web Node (https://identity.foundation/decentralized-web-node/spec) compatible endpoint supporting operations to read and write Verifiable Credentials into an Identity Hub") @@ -67,13 +63,6 @@ public ResponseObject handleRequest(RequestObject requestObject) { .build(); } - @Operation(description = "Serve Self-Description document as defined in Gaia-X Trust Framework (https://gaia-x.gitlab.io/policy-rules-committee/trust-framework/)") - @GET - @Path("/self-description") - public JsonNode getSelfDescription() { - return selfDescription; - } - private MessageResponseObject processMessage(MessageRequestObject messageRequestObject) { var method = WebNodeInterfaceMethod.fromName(messageRequestObject.getDescriptor().getMethod()); var processor = messageProcessorRegistry.resolve(method); diff --git a/extensions/identity-hub-api/src/test/java/org/eclipse/edc/identityhub/api/IdentityHubApiTest.java b/extensions/identity-hub-api/src/test/java/org/eclipse/edc/identityhub/api/IdentityHubApiTest.java index 7a0f97b39..0ddc56aad 100644 --- a/extensions/identity-hub-api/src/test/java/org/eclipse/edc/identityhub/api/IdentityHubApiTest.java +++ b/extensions/identity-hub-api/src/test/java/org/eclipse/edc/identityhub/api/IdentityHubApiTest.java @@ -110,18 +110,6 @@ void writeMalformedMessage() { .body("replies[0].status.detail", equalTo("The message was malformed or improperly constructed")); } - @Test - void getSelfDescription() { - given() - .baseUri(apiBasePath) - .basePath(IDENTITY_HUB_PATH) - .get("/self-description") - .then() - .assertThat() - .statusCode(200) - .body("selfDescriptionCredential.credentialSubject.gx-participant:headquarterAddress.gx-participant:country.@value", equalTo("FR")); - } - protected abstract String configureApi(EdcExtension extension); private RequestSpecification baseRequest() { diff --git a/extensions/identity-hub-verifier-jwt/src/test/java/org/eclipse/edc/identityhub/verifier/jwt/CredentialsVerifierExtensionTest.java b/extensions/identity-hub-verifier-jwt/src/test/java/org/eclipse/edc/identityhub/verifier/jwt/CredentialsVerifierExtensionTest.java index 64bc3dfcc..7e35ad1b2 100644 --- a/extensions/identity-hub-verifier-jwt/src/test/java/org/eclipse/edc/identityhub/verifier/jwt/CredentialsVerifierExtensionTest.java +++ b/extensions/identity-hub-verifier-jwt/src/test/java/org/eclipse/edc/identityhub/verifier/jwt/CredentialsVerifierExtensionTest.java @@ -14,9 +14,6 @@ package org.eclipse.edc.identityhub.verifier.jwt; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.jwk.ECKey; import org.eclipse.edc.iam.did.spi.credentials.CredentialsVerifier; import org.eclipse.edc.iam.did.spi.document.DidConstants; @@ -33,8 +30,8 @@ import org.eclipse.edc.identityhub.spi.credentials.transformer.CredentialEnvelopeTransformerRegistry; import org.eclipse.edc.junit.extensions.EdcExtension; import org.eclipse.edc.junit.testfixtures.TestUtils; -import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.types.TypeManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -57,8 +54,7 @@ @ExtendWith(EdcExtension.class) class CredentialsVerifierExtensionTest { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + private static final TypeManager TYPE_MANAGER = new TypeManager(); private static final int PORT = getFreePort(); private static final String API_URL = format("http://localhost:%d/api/identity-hub", PORT); private static final String DID_METHOD = "test-method"; @@ -69,8 +65,8 @@ class CredentialsVerifierExtensionTest { private IdentityHubClient identityHubClient; - private static DidDocument createDidDocument(ECKey jwk) throws JsonProcessingException { - var ecKey = OBJECT_MAPPER.readValue(jwk.toJSONString(), EllipticCurvePublicKey.class); + private static DidDocument createDidDocument(ECKey jwk) { + var ecKey = TYPE_MANAGER.readValue(jwk.toJSONString(), EllipticCurvePublicKey.class); return DidDocument.Builder.newInstance() .id(SUBJECT) .service(List.of(new Service("IdentityHub", "IdentityHub", API_URL))) @@ -80,13 +76,13 @@ private static DidDocument createDidDocument(ECKey jwk) throws JsonProcessingExc @BeforeEach void setUp(EdcExtension extension) { - when(registry.resolve(any())).thenReturn(new JwtCredentialEnvelopeTransformer(OBJECT_MAPPER)); - identityHubClient = new IdentityHubClientImpl(TestUtils.testOkHttpClient(), OBJECT_MAPPER, mock(Monitor.class), registry); + when(registry.resolve(any())).thenReturn(new JwtCredentialEnvelopeTransformer(TYPE_MANAGER.getMapper())); + identityHubClient = new IdentityHubClientImpl(TestUtils.testHttpClient(), TYPE_MANAGER, registry); extension.setConfiguration(Map.of("web.http.port", String.valueOf(PORT))); } @Test - void getVerifiedClaims_getValidClaims(CredentialsVerifier verifier, DidResolverRegistry registry) throws JsonProcessingException { + void getVerifiedClaims_getValidClaims(CredentialsVerifier verifier, DidResolverRegistry registry) { // Arrange var jwk = generateEcKey(); var didDocument = createDidDocument(jwk); diff --git a/extensions/identity-hub-verifier-jwt/src/test/java/org/eclipse/edc/identityhub/verifier/jwt/IdentityHubJwtCredentialsVerifierTest.java b/extensions/identity-hub-verifier-jwt/src/test/java/org/eclipse/edc/identityhub/verifier/jwt/IdentityHubJwtCredentialsVerifierTest.java index c4323c434..2ed325129 100644 --- a/extensions/identity-hub-verifier-jwt/src/test/java/org/eclipse/edc/identityhub/verifier/jwt/IdentityHubJwtCredentialsVerifierTest.java +++ b/extensions/identity-hub-verifier-jwt/src/test/java/org/eclipse/edc/identityhub/verifier/jwt/IdentityHubJwtCredentialsVerifierTest.java @@ -28,8 +28,6 @@ import org.eclipse.edc.identityhub.spi.credentials.verifier.CredentialEnvelopeVerifierRegistry; import org.eclipse.edc.identityhub.verifier.IdentityHubCredentialsVerifier; import org.eclipse.edc.spi.monitor.Monitor; -import org.eclipse.edc.spi.response.ResponseStatus; -import org.eclipse.edc.spi.response.StatusResult; import org.eclipse.edc.spi.result.Result; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; @@ -116,7 +114,7 @@ void getVerifiedClaims_hubUrlNotResolved() { void getVerifiedClaims_idHubCallFails() { // Arrange - when(identityHubClientMock.getVerifiableCredentials(HUB_BASE_URL)).thenReturn(StatusResult.failure(ResponseStatus.FATAL_ERROR)); + when(identityHubClientMock.getVerifiableCredentials(HUB_BASE_URL)).thenReturn(Result.failure("error")); // Act var credentials = credentialsVerifier.getVerifiedCredentials(DID_DOCUMENT); @@ -163,7 +161,7 @@ void getVerifiedClaims_verifiableCredentialsWithMissingId() { private void setUpMocks(SignedJWT jws, boolean isSigned, boolean claimsValid) { when(credentialsVerifierRegistry.resolve("application/vc+jwt")).thenReturn(new JwtCredentialEnvelopeVerifier(jwtCredentialsVerifierMock, OBJECT_MAPPER)); - when(identityHubClientMock.getVerifiableCredentials(HUB_BASE_URL)).thenReturn(StatusResult.success(List.of(new JwtCredentialEnvelope(jws)))); + when(identityHubClientMock.getVerifiableCredentials(HUB_BASE_URL)).thenReturn(Result.success(List.of(new JwtCredentialEnvelope(jws)))); when(jwtCredentialsVerifierMock.isSignedByIssuer(jws)).thenReturn(isSigned ? Result.success() : Result.failure("JWT not signed")); when(jwtCredentialsVerifierMock.verifyClaims(eq(jws), any())).thenReturn(claimsValid ? Result.success() : Result.failure("VC not valid")); } diff --git a/extensions/self-description-api/build.gradle.kts b/extensions/self-description-api/build.gradle.kts new file mode 100644 index 000000000..3846a6d0e --- /dev/null +++ b/extensions/self-description-api/build.gradle.kts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 Amadeus + * + * 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 - initial API and implementation + * + */ + +plugins { + `java-library` + id("io.swagger.core.v3.swagger-gradle-plugin") + `maven-publish` +} + +dependencies { + implementation(project(":extensions:identity-hub-api")) + + implementation(edc.ext.http) + testImplementation(edc.core.junit) + testImplementation(libs.restAssured) +} + + +publishing { + publications { + create(project.name) { + from(components["java"]) + } + } +} + diff --git a/extensions/self-description-api/src/main/java/org/eclipse/edc/identityhub/api/selfdescription/SelfDescriptionApiExtension.java b/extensions/self-description-api/src/main/java/org/eclipse/edc/identityhub/api/selfdescription/SelfDescriptionApiExtension.java new file mode 100644 index 000000000..8b68ea89d --- /dev/null +++ b/extensions/self-description-api/src/main/java/org/eclipse/edc/identityhub/api/selfdescription/SelfDescriptionApiExtension.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.api.selfdescription; + +import org.eclipse.edc.identityhub.api.IdentityHubApiConfiguration; +import org.eclipse.edc.identityhub.api.selfdescription.controller.SelfDescriptionController; +import org.eclipse.edc.identityhub.api.selfdescription.loader.SelfDescriptionLoader; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.web.spi.WebService; + +import java.util.Optional; + +/** + * Temporary extension for serving a static self-description document. + */ +@Extension(value = SelfDescriptionApiExtension.NAME) +public class SelfDescriptionApiExtension implements ServiceExtension { + + public static final String NAME = "Self Description API"; + @Setting + private static final String SELF_DESCRIPTION_DOCUMENT_PATH_SETTING = "edc.self.description.document.path"; + private static final String DEFAULT_SELF_DESCRIPTION_FILE_NAME = "default-self-description.json"; + + @Inject + private WebService webService; + + @Inject + private IdentityHubApiConfiguration configuration; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var mapper = context.getTypeManager().getMapper(); + var loader = new SelfDescriptionLoader(mapper); + var selfDescription = Optional.ofNullable(context.getSetting(SELF_DESCRIPTION_DOCUMENT_PATH_SETTING, null)) + .map(loader::fromFile) + .orElse(loader.fromClasspath(DEFAULT_SELF_DESCRIPTION_FILE_NAME)); + var controller = new SelfDescriptionController(selfDescription); + webService.registerResource(configuration.getContextAlias(), controller); + } +} diff --git a/extensions/self-description-api/src/main/java/org/eclipse/edc/identityhub/api/selfdescription/controller/SelfDescriptionApi.java b/extensions/self-description-api/src/main/java/org/eclipse/edc/identityhub/api/selfdescription/controller/SelfDescriptionApi.java new file mode 100644 index 000000000..f2545c851 --- /dev/null +++ b/extensions/self-description-api/src/main/java/org/eclipse/edc/identityhub/api/selfdescription/controller/SelfDescriptionApi.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Amadeus + * + * 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 - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.api.selfdescription.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +@OpenAPIDefinition +@Tag(name = "SelfDescription") +public interface SelfDescriptionApi { + + @Operation(description = "Serve Self-Description document.") + JsonNode getSelfDescription(); +} + diff --git a/extensions/self-description-api/src/main/java/org/eclipse/edc/identityhub/api/selfdescription/controller/SelfDescriptionController.java b/extensions/self-description-api/src/main/java/org/eclipse/edc/identityhub/api/selfdescription/controller/SelfDescriptionController.java new file mode 100644 index 000000000..c162cbd57 --- /dev/null +++ b/extensions/self-description-api/src/main/java/org/eclipse/edc/identityhub/api/selfdescription/controller/SelfDescriptionController.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 Amadeus + * + * 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 - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.api.selfdescription.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Path("/identity-hub") +public class SelfDescriptionController implements SelfDescriptionApi { + + private final JsonNode selfDescription; + + public SelfDescriptionController(JsonNode selfDescription) { + this.selfDescription = selfDescription; + } + + @GET + @Override + @Path("/self-description") + public JsonNode getSelfDescription() { + return selfDescription; + } +} + diff --git a/core/identity-hub/src/main/java/org/eclipse/edc/identityhub/selfdescription/SelfDescriptionLoader.java b/extensions/self-description-api/src/main/java/org/eclipse/edc/identityhub/api/selfdescription/loader/SelfDescriptionLoader.java similarity index 96% rename from core/identity-hub/src/main/java/org/eclipse/edc/identityhub/selfdescription/SelfDescriptionLoader.java rename to extensions/self-description-api/src/main/java/org/eclipse/edc/identityhub/api/selfdescription/loader/SelfDescriptionLoader.java index 369dc03cf..a1c55f92f 100644 --- a/core/identity-hub/src/main/java/org/eclipse/edc/identityhub/selfdescription/SelfDescriptionLoader.java +++ b/extensions/self-description-api/src/main/java/org/eclipse/edc/identityhub/api/selfdescription/loader/SelfDescriptionLoader.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.selfdescription; +package org.eclipse.edc.identityhub.api.selfdescription.loader; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/extensions/self-description-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/self-description-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..2fae77180 --- /dev/null +++ b/extensions/self-description-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,15 @@ +# +# Copyright (c) 2023 Amadeus +# +# 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 - initial API and implementation +# +# + +org.eclipse.edc.identityhub.api.selfdescription.SelfDescriptionApiExtension diff --git a/core/identity-hub/src/main/resources/default-self-description.json b/extensions/self-description-api/src/main/resources/default-self-description.json similarity index 100% rename from core/identity-hub/src/main/resources/default-self-description.json rename to extensions/self-description-api/src/main/resources/default-self-description.json diff --git a/extensions/self-description-api/src/test/java/org/eclipse/edc/identityhub/api/selfdescription/controller/SelfDescriptionApiTest.java b/extensions/self-description-api/src/test/java/org/eclipse/edc/identityhub/api/selfdescription/controller/SelfDescriptionApiTest.java new file mode 100644 index 000000000..b61deecfd --- /dev/null +++ b/extensions/self-description-api/src/test/java/org/eclipse/edc/identityhub/api/selfdescription/controller/SelfDescriptionApiTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2022 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.api.selfdescription.controller; + +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +@ExtendWith(EdcExtension.class) +abstract class SelfDescriptionApiTest { + + private static final String IDENTITY_HUB_PATH = "/identity-hub"; + + private String apiBasePath; + + @BeforeEach + void setUp(EdcExtension extension) { + apiBasePath = configureApi(extension); + } + + @Test + void getSelfDescription() { + given() + .baseUri(apiBasePath) + .basePath(IDENTITY_HUB_PATH) + .get("/self-description") + .then() + .assertThat() + .statusCode(200) + .body("selfDescriptionCredential.credentialSubject.gx-participant:headquarterAddress.gx-participant:country.@value", equalTo("FR")); + } + + protected abstract String configureApi(EdcExtension extension); +} diff --git a/extensions/self-description-api/src/test/java/org/eclipse/edc/identityhub/api/selfdescription/controller/SelfDescriptionApiWithConfigTest.java b/extensions/self-description-api/src/test/java/org/eclipse/edc/identityhub/api/selfdescription/controller/SelfDescriptionApiWithConfigTest.java new file mode 100644 index 000000000..05beded65 --- /dev/null +++ b/extensions/self-description-api/src/test/java/org/eclipse/edc/identityhub/api/selfdescription/controller/SelfDescriptionApiWithConfigTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.api.selfdescription.controller; + +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeAll; + +import java.util.Map; + +import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; + +public class SelfDescriptionApiWithConfigTest extends SelfDescriptionApiTest { + + private static int port; + + @BeforeAll + static void prepare() { + port = getFreePort(); + } + + @Override + protected String configureApi(EdcExtension extension) { + extension.setConfiguration(Map.of( + "web.http.identity.port", String.valueOf(port), + "web.http.identity.path", "/api/v1/identity/testpath")); + return String.format("http://localhost:%s/api/v1/identity/testpath", port); + } +} diff --git a/extensions/self-description-api/src/test/java/org/eclipse/edc/identityhub/api/selfdescription/controller/SelfDescriptionDefaultApiTest.java b/extensions/self-description-api/src/test/java/org/eclipse/edc/identityhub/api/selfdescription/controller/SelfDescriptionDefaultApiTest.java new file mode 100644 index 000000000..4761cd49a --- /dev/null +++ b/extensions/self-description-api/src/test/java/org/eclipse/edc/identityhub/api/selfdescription/controller/SelfDescriptionDefaultApiTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.api.selfdescription.controller; + +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.junit.jupiter.api.BeforeAll; + +import java.util.Map; + +import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; + +public class SelfDescriptionDefaultApiTest extends SelfDescriptionApiTest { + private static int port; + + @BeforeAll + static void prepare() { + port = getFreePort(); + } + + @Override + protected String configureApi(EdcExtension extension) { + extension.setConfiguration(Map.of("web.http.port", String.valueOf(port))); + return String.format("http://localhost:%s/api", port); + } +} diff --git a/core/identity-hub/src/test/java/org/eclipse/edc/identityhub/selfdescription/SelfDescriptionLoaderTest.java b/extensions/self-description-api/src/test/java/org/eclipse/edc/identityhub/api/selfdescription/loader/SelfDescriptionLoaderTest.java similarity index 97% rename from core/identity-hub/src/test/java/org/eclipse/edc/identityhub/selfdescription/SelfDescriptionLoaderTest.java rename to extensions/self-description-api/src/test/java/org/eclipse/edc/identityhub/api/selfdescription/loader/SelfDescriptionLoaderTest.java index d4a1be0be..4558ffab4 100644 --- a/core/identity-hub/src/test/java/org/eclipse/edc/identityhub/selfdescription/SelfDescriptionLoaderTest.java +++ b/extensions/self-description-api/src/test/java/org/eclipse/edc/identityhub/api/selfdescription/loader/SelfDescriptionLoaderTest.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.selfdescription; +package org.eclipse.edc.identityhub.api.selfdescription.loader; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonNode; diff --git a/core/identity-hub/src/test/resources/invalid-self-description.txt b/extensions/self-description-api/src/test/resources/invalid-self-description.txt similarity index 100% rename from core/identity-hub/src/test/resources/invalid-self-description.txt rename to extensions/self-description-api/src/test/resources/invalid-self-description.txt diff --git a/core/identity-hub/src/test/resources/self-description.json b/extensions/self-description-api/src/test/resources/self-description.json similarity index 100% rename from core/identity-hub/src/test/resources/self-description.json rename to extensions/self-description-api/src/test/resources/self-description.json diff --git a/launcher/build.gradle.kts b/launcher/build.gradle.kts index 93518ca30..94773fb8a 100644 --- a/launcher/build.gradle.kts +++ b/launcher/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(project(":core:identity-hub")) implementation(project(":core:identity-hub-verifier")) implementation(project(":extensions:identity-hub-api")) + implementation(project(":extensions:self-description-api")) implementation(edc.ext.observability) implementation(edc.ext.identity.did.core) implementation(edc.ext.identity.did.web) diff --git a/resources/openapi/yaml/identity-hub-api.yaml b/resources/openapi/yaml/identity-hub-api.yaml index 2d06fe71f..18f5b04e9 100644 --- a/resources/openapi/yaml/identity-hub-api.yaml +++ b/resources/openapi/yaml/identity-hub-api.yaml @@ -23,20 +23,6 @@ paths: description: default response tags: - IdentityHub - /identity-hub/self-description: - get: - description: Serve Self-Description document as defined in Gaia-X Trust Framework - (https://gaia-x.gitlab.io/policy-rules-committee/trust-framework/) - operationId: getSelfDescription - responses: - default: - content: - application/json: - schema: - $ref: '#/components/schemas/JsonNode' - description: default response - tags: - - IdentityHub components: schemas: Descriptor: @@ -68,9 +54,6 @@ components: type: string description: Unique identifier of the record example: null - JsonNode: - type: object - example: null MessageRequestObject: type: object example: null diff --git a/resources/openapi/yaml/identity-hub.yaml b/resources/openapi/yaml/self-description-api.yaml similarity index 80% rename from resources/openapi/yaml/identity-hub.yaml rename to resources/openapi/yaml/self-description-api.yaml index bd301536b..9ef014f5a 100644 --- a/resources/openapi/yaml/identity-hub.yaml +++ b/resources/openapi/yaml/self-description-api.yaml @@ -3,24 +3,8 @@ info: title: Eclipse Dataspace Connector Identity Hub version: 0.0.1 paths: - /identity-hub/self-description: - get: - tags: - - IdentityHub - description: Serve Self-Description document as defined in Gaia-X Trust Framework - (https://gaia-x.gitlab.io/policy-rules-committee/trust-framework/) - operationId: getSelfDescription - responses: - default: - description: default response - content: - application/json: - schema: - $ref: '#/components/schemas/JsonNode' /identity-hub: post: - tags: - - IdentityHub description: A Decentralized Web Node (https://identity.foundation/decentralized-web-node/spec) compatible endpoint supporting operations to read and write Verifiable Credentials into an Identity Hub @@ -32,114 +16,136 @@ paths: $ref: '#/components/schemas/RequestObject' responses: default: - description: default response content: application/json: schema: $ref: '#/components/schemas/ResponseObject' + description: default response + tags: + - IdentityHub + /identity-hub/self-description: + get: + description: Serve Self-Description document. + operationId: getSelfDescription + responses: + default: + content: + application/json: + schema: + $ref: '#/components/schemas/JsonNode' + description: default response + tags: + - SelfDescription components: schemas: + Descriptor: + type: object + example: null + properties: + dataCid: + type: string + description: "[UNSUPPORTED] If data is available, this field should contain\ + \ stringified Version 1 CID of the DAG PB encoded data" + example: null + dataFormat: + type: string + description: "if data is available, this field should contain a registered\ + \ IANA Media Type data format. Use 'application/vc+ldp' for Verifiable\ + \ Credentials." + example: null + dateCreated: + type: integer + format: int64 + description: Unix epoch timestamp interpreted as the time the logical entry + was created by the DID owner or another permitted party + example: null + method: + type: string + description: A string that matches a Decentralized Web Node Interface method + example: null + recordId: + type: string + description: Unique identifier of the record + example: null JsonNode: type: object + example: null + MessageRequestObject: + type: object + example: null + properties: + data: + type: array + description: Optional base64Url encoded string of the message data + example: null + items: + type: string + format: byte + description: Optional base64Url encoded string of the message data + example: null + descriptor: + $ref: '#/components/schemas/Descriptor' MessageResponseObject: type: object + example: null properties: - messageId: - type: string - description: "[UNSUPPORTED] Stringified Version 1 CID of the associated\ - \ message" - status: - $ref: '#/components/schemas/MessageStatus' entries: type: array description: Resulting message entries returned from the invocation of the corresponding message as free form objects + example: null items: type: object description: Resulting message entries returned from the invocation of the corresponding message as free form objects + example: null + status: + $ref: '#/components/schemas/MessageStatus' MessageStatus: type: object + example: null properties: code: type: integer + format: int32 description: An integer set to the HTTP Status Code appropriate for the status of the response - format: int32 + example: null detail: type: string description: A string that describes a terse summary of the status + example: null + RequestObject: + type: object + example: null + properties: + messages: + type: array + example: null + items: + $ref: '#/components/schemas/MessageRequestObject' RequestStatus: type: object + example: null properties: code: type: integer + format: int32 description: An integer set to the HTTP Status Code appropriate for the status of the response - format: int32 + example: null detail: type: string description: A string that describes a terse summary of the status + example: null ResponseObject: type: object + example: null properties: - requestId: - type: string - status: - $ref: '#/components/schemas/RequestStatus' replies: type: array + example: null items: $ref: '#/components/schemas/MessageResponseObject' - Descriptor: - type: object - properties: - method: - type: string - description: A string that matches a Decentralized Web Node Interface method - nonce: - type: string - description: "[UNSUPPORTED] Cryptographically random string that ensures\ - \ each object is unique" - dateCreated: - type: integer - description: Unix epoch timestamp interpreted as the time the logical entry - was created by the DID owner or another permitted party - format: int64 - recordId: - type: string - description: Unique identifier of the record - dataCid: - type: string - description: "[UNSUPPORTED] If data is available, this field should contain\ - \ stringified Version 1 CID of the DAG PB encoded data" - dataFormat: - type: string - description: "[UNSUPPORTED] if data is available, this field should contain\ - \ a registered IANA Media Type data format. Use 'application/vc+ldp' for\ - \ Verifiable Credentials." - MessageRequestObject: - type: object - properties: - descriptor: - $ref: '#/components/schemas/Descriptor' - data: - type: array - description: Optional base64Url encoded string of the message data - items: - type: string - description: Optional base64Url encoded string of the message data - format: byte - RequestObject: - type: object - properties: - requestId: - type: string - target: - type: string - description: "[UNSUPPORTED] Decentralized Identifier base URI of the DID-relative\ - \ URL" - messages: - type: array - items: - $ref: '#/components/schemas/MessageRequestObject' + status: + $ref: '#/components/schemas/RequestStatus' diff --git a/settings.gradle.kts b/settings.gradle.kts index 562213bd0..7bef80442 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,6 +35,7 @@ dependencyResolutionManagement { library("spi-transaction", "org.eclipse.edc", "transaction-spi").versionRef("edc") library("spi-transaction-datasource", "org.eclipse.edc", "transaction-datasource-spi").versionRef("edc") library("spi-identity-did", "org.eclipse.edc", "identity-did-spi").versionRef("edc") + library("spi-http", "org.eclipse.edc", "http-spi").versionRef("edc") library("core-connector", "org.eclipse.edc", "connector-core").versionRef("edc") library("core-sql", "org.eclipse.edc", "sql-core").versionRef("edc") library("core-identity-did", "org.eclipse.edc", "identity-did-core").versionRef("edc") @@ -57,7 +58,7 @@ include(":core:identity-hub-verifier") include(":extensions:credentials:identity-hub-credentials-jwt") include(":extensions:identity-hub-api") include(":extensions:identity-hub-verifier-jwt") -include(":extensions:identity-hub-verifier-jwt") +include(":extensions:self-description-api") include(":extensions:store:cosmos:identity-hub-store-cosmos") include(":extensions:store:sql:identity-hub-store-sql") include(":launcher") diff --git a/spi/identity-hub-client-spi/src/main/java/org/eclipse/edc/identityhub/client/spi/IdentityHubClient.java b/spi/identity-hub-client-spi/src/main/java/org/eclipse/edc/identityhub/client/spi/IdentityHubClient.java index 4f6464e23..32b91b02a 100644 --- a/spi/identity-hub-client-spi/src/main/java/org/eclipse/edc/identityhub/client/spi/IdentityHubClient.java +++ b/spi/identity-hub-client-spi/src/main/java/org/eclipse/edc/identityhub/client/spi/IdentityHubClient.java @@ -14,43 +14,31 @@ package org.eclipse.edc.identityhub.client.spi; -import com.fasterxml.jackson.databind.JsonNode; import org.eclipse.edc.identityhub.spi.credentials.model.CredentialEnvelope; -import org.eclipse.edc.spi.response.StatusResult; +import org.eclipse.edc.spi.result.Result; import java.util.Collection; /** - * IdentityHub Client - * This client is used to call the IdentityHub endpoints in order query and write VerifiableCredentials, and display the - * Self-Description document. + * This client is used to call the IdentityHub endpoints in order query and write VerifiableCredentials. * Eventually, this may be expanded to handle other types of objects and operations. */ public interface IdentityHubClient { - /** - * Display the Self-Description document. - * - * @param hubBaseUrl Base URL of the IdentityHub instance. - * @return status result containing the Self-Description document if request successful. - */ - StatusResult getSelfDescription(String hubBaseUrl); - /** * Get VerifiableCredentials provided by an Identity Hub instance. * * @param hubBaseUrl Base URL of the IdentityHub instance. - * @return status result containing VerifiableCredentials if request successful. + * @return result containing VerifiableCredentials if request successful. */ - StatusResult> getVerifiableCredentials(String hubBaseUrl); + Result> getVerifiableCredentials(String hubBaseUrl); /** * Write a VerifiableCredential. * * @param hubBaseUrl Base URL of the IdentityHub instance. * @param verifiableCredential A verifiable credential to be saved. - * @return status result. + * @return result. */ - StatusResult addVerifiableCredential(String hubBaseUrl, CredentialEnvelope verifiableCredential); - + Result addVerifiableCredential(String hubBaseUrl, CredentialEnvelope verifiableCredential); } diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/credentials/model/VerifiableCredential.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/credentials/model/VerifiableCredential.java index 466a3e5de..83ab3c695 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/credentials/model/VerifiableCredential.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/credentials/model/VerifiableCredential.java @@ -25,8 +25,8 @@ */ public class VerifiableCredential extends Verifiable { - private static final String DEFAULT_CONTEXT = "https://www.w3.org/2018/credentials/v1"; - private static final String DEFAULT_TYPE = "VerifiableCredential"; + public static final String DEFAULT_CONTEXT = "https://www.w3.org/2018/credentials/v1"; + public static final String DEFAULT_TYPE = "VerifiableCredential"; @JsonCreator public VerifiableCredential(@JsonProperty("proof") Proof proof) { diff --git a/spi/identity-hub-spi/src/testFixtures/java/org/eclipse/edc/identityhub/junit/testfixtures/VerifiableCredentialTestUtil.java b/spi/identity-hub-spi/src/testFixtures/java/org/eclipse/edc/identityhub/junit/testfixtures/VerifiableCredentialTestUtil.java index fd08920db..6f9da7356 100644 --- a/spi/identity-hub-spi/src/testFixtures/java/org/eclipse/edc/identityhub/junit/testfixtures/VerifiableCredentialTestUtil.java +++ b/spi/identity-hub-spi/src/testFixtures/java/org/eclipse/edc/identityhub/junit/testfixtures/VerifiableCredentialTestUtil.java @@ -28,6 +28,7 @@ import org.eclipse.edc.identityhub.spi.credentials.model.Credential; import org.eclipse.edc.identityhub.spi.credentials.model.CredentialSubject; import org.eclipse.edc.identityhub.spi.credentials.model.Proof; +import org.eclipse.edc.identityhub.spi.credentials.model.VerifiableCredential; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -39,7 +40,7 @@ * Util class to manipulate VerifiableCredentials in tests. */ public class VerifiableCredentialTestUtil { - + private static final ECKeyGenerator EC_KEY_GENERATOR = new ECKeyGenerator(Curve.P_256); private static final ObjectMapper MAPPER = new ObjectMapper(); @@ -48,11 +49,11 @@ private VerifiableCredentialTestUtil() { public static Credential generateCredential() { return Credential.Builder.newInstance() - .context("https://www.w3.org/2018/credentials/v1") + .context(VerifiableCredential.DEFAULT_CONTEXT) .id(UUID.randomUUID().toString()) .issuer("issuer") .issuanceDate(Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS))) - .type("test") + .type(VerifiableCredential.DEFAULT_TYPE) .credentialSubject(CredentialSubject.Builder.newInstance() .id("id-test") .claim("cred1", UUID.randomUUID().toString()) diff --git a/system-tests/src/test/java/org/eclipse/edc/identityhub/systemtests/VerifiableCredentialsIntegrationTest.java b/system-tests/src/test/java/org/eclipse/edc/identityhub/systemtests/VerifiableCredentialsIntegrationTest.java index 14fe91ae4..5747d1694 100644 --- a/system-tests/src/test/java/org/eclipse/edc/identityhub/systemtests/VerifiableCredentialsIntegrationTest.java +++ b/system-tests/src/test/java/org/eclipse/edc/identityhub/systemtests/VerifiableCredentialsIntegrationTest.java @@ -19,8 +19,9 @@ import org.eclipse.edc.iam.did.spi.credentials.CredentialsVerifier; import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry; import org.eclipse.edc.identityhub.cli.IdentityHubCli; -import org.eclipse.edc.identityhub.junit.testfixtures.VerifiableCredentialTestUtil; import org.eclipse.edc.identityhub.spi.credentials.model.Credential; +import org.eclipse.edc.identityhub.spi.credentials.model.CredentialSubject; +import org.eclipse.edc.identityhub.spi.credentials.model.VerifiableCredential; import org.eclipse.edc.junit.extensions.EdcExtension; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -29,7 +30,11 @@ import java.io.PrintWriter; import java.io.StringWriter; +import java.sql.Date; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Map; +import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -42,7 +47,7 @@ class VerifiableCredentialsIntegrationTest { private static final String PARTICIPANT_DID = "did:web:localhost%3A8080:participant"; private static final String AUTHORITY_PRIVATE_KEY_PATH = "resources/jwt/authority/private-key.pem"; private static final ObjectMapper MAPPER = new ObjectMapper(); - private static final Credential CREDENTIAL = VerifiableCredentialTestUtil.generateCredential(); + private static final Credential CREDENTIAL = createCredential(); private final CommandLine cmd = IdentityHubCli.getCommandLine(); private final StringWriter out = new StringWriter(); @@ -65,16 +70,9 @@ void push_and_get_verifiable_credentials(CredentialsVerifier verifier, DidResolv assertGetVerifiedCredentials(verifier, resolverRegistry); } - @Test - void get_self_description() { - int result = cmd.execute("-s", HUB_URL, "sd", "get"); - assertThat(result).isZero(); - assertThat(out.toString()).contains("did:web:test.delta-dao.com"); - } - private void addVerifiableCredentialWithCli() throws JsonProcessingException { - var json = MAPPER.writeValueAsString(CREDENTIAL); - int result = cmd.execute("-s", HUB_URL, "vc", "add", "-c", json, "-i", AUTHORITY_DID, "-b", PARTICIPANT_DID, "-k", AUTHORITY_PRIVATE_KEY_PATH); + var json = MAPPER.writeValueAsString(CREDENTIAL.getCredentialSubject().getClaims()); + int result = cmd.execute("-s", HUB_URL, "vc", "add", "-c", json, "-i", CREDENTIAL.getIssuer(), "-b", CREDENTIAL.getCredentialSubject().getId(), "-k", AUTHORITY_PRIVATE_KEY_PATH); assertThat(result).isZero(); } @@ -86,11 +84,28 @@ private void assertGetVerifiedCredentials(CredentialsVerifier verifier, DidResol assertThat(verifiedCredentials.succeeded()).isTrue(); var vcs = verifiedCredentials.getContent(); - assertThat(vcs).extractingByKey(CREDENTIAL.getId()) - .satisfies(o -> { - assertThat(o).isInstanceOf(Credential.class); - var cred = (Credential) o; - assertThat(cred).usingRecursiveComparison().isEqualTo(CREDENTIAL); - }); + assertThat(vcs).hasSize(1); + var verifiedCredential = vcs.values().stream().findFirst() + .orElseThrow(() -> new AssertionError("Failed to find verified credential")); + + assertThat(verifiedCredential).isInstanceOf(Credential.class); + assertThat((Credential) verifiedCredential).usingRecursiveComparison() + .ignoringFields("id", "issuanceDate") + .isEqualTo(CREDENTIAL); + } + + private static Credential createCredential() { + return Credential.Builder.newInstance() + .context(VerifiableCredential.DEFAULT_CONTEXT) + .id(UUID.randomUUID().toString()) + .type(VerifiableCredential.DEFAULT_TYPE) + .issuer(AUTHORITY_DID) + .issuanceDate(Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS))) + .credentialSubject(CredentialSubject.Builder.newInstance() + .id(PARTICIPANT_DID) + .claim("hello", "world") + .claim("foo", "bar") + .build()) + .build(); } }