diff --git a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryKeyPairResourceStore.java b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryKeyPairResourceStore.java index a69aff6f0..fa2a8c347 100644 --- a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryKeyPairResourceStore.java +++ b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/defaults/InMemoryKeyPairResourceStore.java @@ -16,8 +16,8 @@ import org.eclipse.edc.connector.core.store.CriterionToPredicateConverterImpl; import org.eclipse.edc.connector.core.store.ReflectionBasedQueryResolver; +import org.eclipse.edc.identityhub.spi.model.KeyPairResource; import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore; -import org.eclipse.edc.identityhub.spi.store.model.KeyPairResource; import org.eclipse.edc.spi.query.QueryResolver; public class InMemoryKeyPairResourceStore extends InMemoryEntityStore implements KeyPairResourceStore { diff --git a/core/identity-hub-keypairs/src/main/java/org/eclipse/edc/identityhub/keypairs/KeyPairServiceImpl.java b/core/identity-hub-keypairs/src/main/java/org/eclipse/edc/identityhub/keypairs/KeyPairServiceImpl.java index 99a063d60..2e439bd57 100644 --- a/core/identity-hub-keypairs/src/main/java/org/eclipse/edc/identityhub/keypairs/KeyPairServiceImpl.java +++ b/core/identity-hub-keypairs/src/main/java/org/eclipse/edc/identityhub/keypairs/KeyPairServiceImpl.java @@ -16,10 +16,10 @@ import org.eclipse.edc.identityhub.security.KeyPairGenerator; import org.eclipse.edc.identityhub.spi.KeyPairService; +import org.eclipse.edc.identityhub.spi.model.KeyPairResource; +import org.eclipse.edc.identityhub.spi.model.KeyPairState; import org.eclipse.edc.identityhub.spi.model.participant.KeyDescriptor; import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore; -import org.eclipse.edc.identityhub.spi.store.model.KeyPairResource; -import org.eclipse.edc.identityhub.spi.store.model.KeyPairState; import org.eclipse.edc.security.token.jwt.CryptoConverter; import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.spi.query.QuerySpec; @@ -29,6 +29,7 @@ import org.jetbrains.annotations.Nullable; import java.time.Instant; +import java.util.Collection; import java.util.Objects; import java.util.Optional; @@ -105,6 +106,11 @@ public ServiceResult revokeKey(String id, @Nullable KeyDescriptor newKeySp return ServiceResult.success(); } + @Override + public ServiceResult> query(QuerySpec querySpec) { + return ServiceResult.from(keyPairResourceStore.query(querySpec)); + } + private KeyPairResource findById(String oldId) { var q = QuerySpec.Builder.newInstance() .filter(new Criterion("id", "=", oldId)).build(); diff --git a/core/identity-hub-keypairs/src/test/java/org/eclipse/edc/identityhub/keypairs/KeyPairServiceImplTest.java b/core/identity-hub-keypairs/src/test/java/org/eclipse/edc/identityhub/keypairs/KeyPairServiceImplTest.java index ab981c646..695eb1872 100644 --- a/core/identity-hub-keypairs/src/test/java/org/eclipse/edc/identityhub/keypairs/KeyPairServiceImplTest.java +++ b/core/identity-hub-keypairs/src/test/java/org/eclipse/edc/identityhub/keypairs/KeyPairServiceImplTest.java @@ -17,10 +17,10 @@ import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.jwk.Curve; import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator; +import org.eclipse.edc.identityhub.spi.model.KeyPairResource; +import org.eclipse.edc.identityhub.spi.model.KeyPairState; import org.eclipse.edc.identityhub.spi.model.participant.KeyDescriptor; import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore; -import org.eclipse.edc.identityhub.spi.store.model.KeyPairResource; -import org.eclipse.edc.identityhub.spi.store.model.KeyPairState; import org.eclipse.edc.spi.security.Vault; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; diff --git a/extensions/api/keypair-mgmt-api/build.gradle.kts b/extensions/api/keypair-mgmt-api/build.gradle.kts new file mode 100644 index 000000000..865ad385b --- /dev/null +++ b/extensions/api/keypair-mgmt-api/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + `java-library` + `maven-publish` + id("io.swagger.core.v3.swagger-gradle-plugin") +} + +dependencies { + api(libs.edc.spi.core) + api(project(":spi:identity-hub-spi")) + api(project(":spi:identity-hub-store-spi")) + implementation(project(":extensions:api:identityhub-management-api-configuration")) + implementation(libs.edc.spi.web) + implementation(libs.edc.util) + implementation(libs.jakarta.rsApi) + + testImplementation(libs.edc.junit) + testImplementation(libs.restAssured) + testImplementation(testFixtures(libs.edc.core.jersey)) +} diff --git a/extensions/api/keypair-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/KeyPairResourceManagementApiExtension.java b/extensions/api/keypair-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/KeyPairResourceManagementApiExtension.java new file mode 100644 index 000000000..27decddcd --- /dev/null +++ b/extensions/api/keypair-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/KeyPairResourceManagementApiExtension.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.api.verifiablecredentials; + +import org.eclipse.edc.identityhub.api.configuration.ManagementApiConfiguration; +import org.eclipse.edc.identityhub.api.verifiablecredentials.v1.KeyPairResourceApiController; +import org.eclipse.edc.identityhub.spi.KeyPairService; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.web.spi.WebService; + +import static org.eclipse.edc.identityhub.api.verifiablecredentials.KeyPairResourceManagementApiExtension.NAME; + +@Extension(NAME) +public class KeyPairResourceManagementApiExtension implements ServiceExtension { + public static final String NAME = "KeyPairResource Management API Extension"; + + @Inject + private ManagementApiConfiguration apiConfiguration; + @Inject + private WebService webService; + @Inject + private KeyPairService keyPairService; + + @Override + public void initialize(ServiceExtensionContext context) { + var controller = new KeyPairResourceApiController(keyPairService); + webService.registerResource(apiConfiguration.getContextAlias(), controller); + } +} diff --git a/extensions/api/keypair-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/KeyPairResourceApi.java b/extensions/api/keypair-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/KeyPairResourceApi.java new file mode 100644 index 000000000..1d0780659 --- /dev/null +++ b/extensions/api/keypair-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/KeyPairResourceApi.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.api.verifiablecredentials.v1; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.eclipse.edc.identityhub.spi.model.KeyPairResource; +import org.eclipse.edc.identityhub.spi.model.participant.KeyDescriptor; +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; +import org.eclipse.edc.web.spi.ApiErrorDetail; + +import java.util.Collection; + +@OpenAPIDefinition(info = @Info(description = "This is the Management API for KeyPairResources", title = "KeyPairResources Management API", version = "1")) +public interface KeyPairResourceApi { + + @Tag(name = "KeyPairResources Management API") + @Operation(description = "Finds a KeyPairResource by ID.", + responses = { + @ApiResponse(responseCode = "200", description = "The KeyPairResource.", + content = @Content(schema = @Schema(implementation = ParticipantContext.class))), + @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "404", description = "A KeyPairResource with the given ID does not exist.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) + } + ) + KeyPairResource findById(String id); + + @Tag(name = "KeyPairResources Management API") + @Operation(description = "Finds all KeyPairResources for a particular ParticipantContext.", + parameters = @Parameter(name = "participantId", description = "ID of the participant context for which to list the keys. May need elevated rights."), + responses = { + @ApiResponse(responseCode = "200", description = "The KeyPairResource.", + content = @Content(schema = @Schema(implementation = ParticipantContext.class))), + @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "404", description = "A KeyPairResource with the given ID does not exist.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) + } + ) + Collection findForParticipant(String participantId); + + @Tag(name = "KeyPairResources Management API") + @Operation(description = "Adds a new key pair to a ParticipantContext. Note that the key pair is either generated, or the private key is expected to be found in the vault.", + requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = KeyDescriptor.class), mediaType = "application/json")), + parameters = @Parameter(name = "makeDefault", description = "Make the new key pair the default key pair"), + responses = { + @ApiResponse(responseCode = "200", description = "The KeyPairResource was successfully created and linked to the participant.", + content = @Content(schema = @Schema(implementation = ParticipantContext.class))), + @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "404", description = "A KeyPairResource with the given ID does not exist.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) + } + ) + void addKeyPair(String participantId, KeyDescriptor keyDescriptor, boolean makeDefault); + + @Tag(name = "KeyPairResources Management API") + @Operation(description = "Rotates (=retires) a particular key pair, identified by their ID and create a new successor key.", + requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = KeyDescriptor.class), mediaType = "application/json")), + parameters = @Parameter(name = "duration", description = "Indicates for how long the public key of the rotated/retired key pair should still be available "), + responses = { + @ApiResponse(responseCode = "200", description = "The KeyPairResource was successfully rotated and linked to the participant.", + content = @Content(schema = @Schema(implementation = ParticipantContext.class))), + @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "404", description = "A KeyPairResource with the given ID does not exist.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) + } + ) + void rotateKeyPair(String id, KeyDescriptor newKey, long duration); + + @Tag(name = "KeyPairResources Management API") + @Operation(description = "Revokes (=removes) a particular key pair, identified by their ID and create a new successor key.", + requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = KeyDescriptor.class), mediaType = "application/json")), + responses = { + @ApiResponse(responseCode = "200", description = "The KeyPairResource was successfully rotated and linked to the participant.", + content = @Content(schema = @Schema(implementation = ParticipantContext.class))), + @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), + @ApiResponse(responseCode = "404", description = "A KeyPairResource with the given ID does not exist.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) + } + ) + void revokeKey(String id, KeyDescriptor newKey); +} diff --git a/extensions/api/keypair-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/KeyPairResourceApiController.java b/extensions/api/keypair-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/KeyPairResourceApiController.java new file mode 100644 index 000000000..ac95cf426 --- /dev/null +++ b/extensions/api/keypair-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/KeyPairResourceApiController.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.api.verifiablecredentials.v1; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import org.eclipse.edc.identityhub.spi.KeyPairService; +import org.eclipse.edc.identityhub.spi.model.KeyPairResource; +import org.eclipse.edc.identityhub.spi.model.participant.KeyDescriptor; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.web.spi.exception.ObjectNotFoundException; + +import java.util.Collection; + +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper; + +@Consumes(APPLICATION_JSON) +@Produces(APPLICATION_JSON) +@Path("/v1/keypairs") +public class KeyPairResourceApiController implements KeyPairResourceApi { + + + private final KeyPairService keyPairService; + + public KeyPairResourceApiController(KeyPairService keyPairService) { + + this.keyPairService = keyPairService; + } + + @GET + @Path("/{keyPairId}") + @Override + public KeyPairResource findById(@PathParam("keyPairId") String id) { + var query = QuerySpec.Builder.newInstance().filter(new Criterion("id", "=", id)).build(); + var result = keyPairService.query(query).orElseThrow(exceptionMapper(KeyPairResource.class, id)); + if (result.isEmpty()) { + throw new ObjectNotFoundException(KeyPairResource.class, id); + } + if (result.size() > 1) { + throw new EdcException("Expected only 1 result, but got %s".formatted(result.size())); + } + return result.iterator().next(); + } + + @GET + @Override + public Collection findForParticipant(@QueryParam("participantId") String participantId) { + var query = QuerySpec.Builder.newInstance().filter(new Criterion("participantId", "=", participantId)).build(); + return keyPairService.query(query).orElseThrow(exceptionMapper(KeyPairResource.class, participantId)); + } + + @PUT + @Override + public void addKeyPair(@QueryParam("participantId") String participantId, KeyDescriptor keyDescriptor, @QueryParam("makeDefault") boolean makeDefault) { + keyPairService.addKeyPair(participantId, keyDescriptor, makeDefault) + .orElseThrow(exceptionMapper(KeyPairResource.class)); + } + + @POST + @Path("/{keyPairId}/rotate") + @Override + public void rotateKeyPair(@PathParam("keyPairId") String id, KeyDescriptor newKey, @QueryParam("duration") long duration) { + keyPairService.rotateKeyPair(id, newKey, duration) + .orElseThrow(exceptionMapper(KeyPairResource.class, id)); + } + + @POST + @Path("/{keyPairId}/revoke") + @Override + public void revokeKey(@PathParam("keyPairId") String id, KeyDescriptor newKey) { + keyPairService.revokeKey(id, newKey) + .orElseThrow(exceptionMapper(KeyPairResource.class, id)); + } +} diff --git a/extensions/api/keypair-mgmt-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/api/keypair-mgmt-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..aa9f9c822 --- /dev/null +++ b/extensions/api/keypair-mgmt-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,15 @@ +# +# Copyright (c) 2024 Metaform Systems, Inc. +# +# 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: +# Metaform Systems, Inc. - initial API and implementation +# +# + +org.eclipse.edc.identityhub.api.verifiablecredentials.KeyPairResourceManagementApiExtension diff --git a/extensions/api/keypair-mgmt-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/KeyPairResourceApiControllerTest.java b/extensions/api/keypair-mgmt-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/KeyPairResourceApiControllerTest.java new file mode 100644 index 000000000..a13c9f4c2 --- /dev/null +++ b/extensions/api/keypair-mgmt-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/KeyPairResourceApiControllerTest.java @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.api.verifiablecredentials.v1; + +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import org.eclipse.edc.identityhub.spi.KeyPairService; +import org.eclipse.edc.identityhub.spi.model.KeyPairResource; +import org.eclipse.edc.identityhub.spi.model.participant.KeyDescriptor; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +class KeyPairResourceApiControllerTest extends RestControllerTestBase { + + private final KeyPairService keyPairService = mock(); + + @Test + void findById() { + var keyPair = createKeyPair().build(); + + when(keyPairService.query(any())).thenReturn(ServiceResult.success(List.of(keyPair))); + + var found = baseRequest() + .get("/test-keypairId") + .then() + .statusCode(200) + .log().ifError() + .extract().body().as(KeyPairResource.class); + assertThat(found).usingRecursiveComparison().isEqualTo(keyPair); + } + + @Test + void findById_notExist() { + when(keyPairService.query(any())).thenReturn(ServiceResult.notFound("tst-msg")); + + var found = baseRequest() + .get("/test-keypairId") + .then() + .log().ifValidationFails() + .statusCode(404); + } + + @Test + void findForParticipant() { + var keyPair = createKeyPair().build(); + + when(keyPairService.query(any())).thenReturn(ServiceResult.success(List.of(keyPair))); + + var found = baseRequest() + .get("?participantId=test-participant") + .then() + .statusCode(200) + .log().ifError() + .extract().body().as(KeyPairResource[].class); + assertThat(found).usingRecursiveFieldByFieldElementComparator().containsExactly(keyPair); + + verify(keyPairService).query(argThat(q -> { + var criterion = q.getFilterExpression().get(0); + return criterion.getOperandLeft().equals("participantId") && + criterion.getOperator().equals("=") && + criterion.getOperandRight().equals("test-participant"); + })); + } + + @Test + void findForParticipant_noResult() { + var keyPair = createKeyPair().build(); + + when(keyPairService.query(any())).thenReturn(ServiceResult.success(List.of())); + + var found = baseRequest() + .get("?participantId=test-participant") + .then() + .statusCode(200) + .log().ifError() + .extract().body().as(KeyPairResource[].class); + assertThat(found).isEmpty(); + + verify(keyPairService).query(argThat(q -> { + var criterion = q.getFilterExpression().get(0); + return criterion.getOperandLeft().equals("participantId") && + criterion.getOperator().equals("=") && + criterion.getOperandRight().equals("test-participant"); + })); + } + + @Test + void findForParticipant_notfound() { + when(keyPairService.query(any())).thenReturn(ServiceResult.notFound("test-message")); + + baseRequest() + .get("?participantId=test-participant") + .then() + .statusCode(404) + .log().ifError(); + + verify(keyPairService).query(argThat(q -> { + var criterion = q.getFilterExpression().get(0); + return criterion.getOperandLeft().equals("participantId") && + criterion.getOperator().equals("=") && + criterion.getOperandRight().equals("test-participant"); + })); + } + + @ParameterizedTest(name = "Make default: {0}") + @ValueSource(booleans = {true, false}) + void addKeyPair(boolean makeDefault) { + var descriptor = createKeyDescriptor() + .build(); + when(keyPairService.addKeyPair(eq("test-participant"), any(), eq(makeDefault))).thenReturn(ServiceResult.success()); + + baseRequest() + .contentType(ContentType.JSON) + .body(descriptor) + .put("?participantId=%s&makeDefault=%s".formatted("test-participant", makeDefault)) + .then() + .log().ifError() + .statusCode(204); + + verify(keyPairService).addKeyPair(eq("test-participant"), argThat(d -> d.getKeyId().equals(descriptor.getKeyId())), eq(makeDefault)); + verifyNoMoreInteractions(keyPairService); + } + + @Test + void rotate() { + var duration = Duration.ofDays(100).toMillis(); + when(keyPairService.rotateKeyPair(eq("old-id"), any(), eq(duration))).thenReturn(ServiceResult.success()); + + var descriptor = createKeyDescriptor().build(); + baseRequest() + .contentType(ContentType.JSON) + .body(descriptor) + .post("/old-id/rotate?duration=" + duration) + .then() + .log().ifError() + .statusCode(204); + + verify(keyPairService).rotateKeyPair(eq("old-id"), argThat(d -> d.getKeyId().equals(descriptor.getKeyId())), eq(duration)); + verifyNoMoreInteractions(keyPairService); + } + + @Test + void rotate_idNotFound() { + var duration = Duration.ofDays(100).toMillis(); + when(keyPairService.rotateKeyPair(eq("old-id"), any(), eq(duration))).thenReturn(ServiceResult.notFound("test-message")); + + var descriptor = createKeyDescriptor().build(); + baseRequest() + .contentType(ContentType.JSON) + .body(descriptor) + .post("/old-id/rotate?duration=" + duration) + .then() + .log().ifValidationFails() + .statusCode(404); + + verify(keyPairService).rotateKeyPair(eq("old-id"), argThat(d -> d.getKeyId().equals(descriptor.getKeyId())), eq(duration)); + verifyNoMoreInteractions(keyPairService); + } + + @Test + void revoke() { + when(keyPairService.revokeKey(eq("old-id"), any())).thenReturn(ServiceResult.success()); + + var descriptor = createKeyDescriptor().build(); + baseRequest() + .contentType(ContentType.JSON) + .body(descriptor) + .post("/old-id/revoke") + .then() + .log().ifError() + .statusCode(204); + + verify(keyPairService).revokeKey(eq("old-id"), argThat(d -> d.getKeyId().equals(descriptor.getKeyId()))); + verifyNoMoreInteractions(keyPairService); + } + + @Test + void revoke_notFound() { + when(keyPairService.revokeKey(eq("old-id"), any())).thenReturn(ServiceResult.notFound("test-message")); + + var descriptor = createKeyDescriptor().build(); + baseRequest() + .contentType(ContentType.JSON) + .body(descriptor) + .post("/old-id/revoke") + .then() + .log().ifError() + .statusCode(404); + + verify(keyPairService).revokeKey(eq("old-id"), argThat(d -> d.getKeyId().equals(descriptor.getKeyId()))); + verifyNoMoreInteractions(keyPairService); + } + + @Override + protected Object controller() { + return new KeyPairResourceApiController(keyPairService); + } + + private KeyPairResource.Builder createKeyPair() { + return KeyPairResource.Builder.newInstance() + .id("test-keypair") + .participantId("test-participant") + .isDefaultPair(true) + .privateKeyAlias("test-alias") + .useDuration(Duration.ofDays(365).toMillis()); + } + + private RequestSpecification baseRequest() { + return given() + .contentType("application/json") + .baseUri("http://localhost:" + port + "/v1/keypairs") + .when(); + } + + @NotNull + private static KeyDescriptor.Builder createKeyDescriptor() { + return KeyDescriptor.Builder.newInstance() + .keyId("new-key-id") + .keyGeneratorParams(Map.of("algorithm", "EC", "curve", "secp256r1")); + } +} \ No newline at end of file diff --git a/extensions/store/sql/identity-hub-keypair-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/keypair/SqlKeyPairResourceStore.java b/extensions/store/sql/identity-hub-keypair-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/keypair/SqlKeyPairResourceStore.java index 677e0ad89..f46bd8a28 100644 --- a/extensions/store/sql/identity-hub-keypair-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/keypair/SqlKeyPairResourceStore.java +++ b/extensions/store/sql/identity-hub-keypair-store-sql/src/main/java/org/eclipse/edc/identityhub/store/sql/keypair/SqlKeyPairResourceStore.java @@ -15,8 +15,8 @@ package org.eclipse.edc.identityhub.store.sql.keypair; import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.edc.identityhub.spi.model.KeyPairResource; import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore; -import org.eclipse.edc.identityhub.spi.store.model.KeyPairResource; import org.eclipse.edc.spi.persistence.EdcPersistenceException; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.StoreResult; diff --git a/settings.gradle.kts b/settings.gradle.kts index 52a13eda3..6bd781974 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -44,6 +44,7 @@ include(":extensions:did:local-did-publisher") include(":extensions:api:identityhub-management-api-configuration") include(":extensions:api:participant-context-mgmt-api") include(":extensions:api:verifiable-credential-mgmt-api") +include(":extensions:api:keypair-mgmt-api") include(":extensions:did:did-management-api") // other modules diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/KeyPairService.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/KeyPairService.java index 9e0422c97..6f85c3796 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/KeyPairService.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/KeyPairService.java @@ -14,11 +14,15 @@ package org.eclipse.edc.identityhub.spi; +import org.eclipse.edc.identityhub.spi.model.KeyPairResource; import org.eclipse.edc.identityhub.spi.model.participant.KeyDescriptor; +import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.ServiceResult; import org.eclipse.edc.spi.security.Vault; import org.jetbrains.annotations.Nullable; +import java.util.Collection; + public interface KeyPairService { /** @@ -61,4 +65,6 @@ public interface KeyPairService { * @return success if rotated, a failure indicated the problem otherwise. */ ServiceResult revokeKey(String id, @Nullable KeyDescriptor newKeySpec); + + ServiceResult> query(QuerySpec querySpec); } diff --git a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/KeyPairResource.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/KeyPairResource.java similarity index 95% rename from spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/KeyPairResource.java rename to spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/KeyPairResource.java index 793740f3a..3dcc1128b 100644 --- a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/KeyPairResource.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/KeyPairResource.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.spi.store.model; +package org.eclipse.edc.identityhub.spi.model; import com.fasterxml.jackson.annotation.JsonIgnore; import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; @@ -31,7 +31,7 @@ public class KeyPairResource { private long timestamp; private String keyId; private String groupName; - private boolean isDefaultPair; + private boolean defaultPair; private long useDuration; private long rotationDuration; private String serializedPublicKey; @@ -56,7 +56,7 @@ public String getId() { * Whether this KeyPair is the default for a {@link ParticipantContext}. */ public boolean isDefaultPair() { - return isDefaultPair; + return defaultPair; } /** @@ -117,12 +117,12 @@ public int getState() { public void rotate(long duration) { state = KeyPairState.ROTATED.code(); rotationDuration = duration; - isDefaultPair = false; + defaultPair = false; } public void revoke() { state = KeyPairState.REVOKED.code(); - isDefaultPair = false; + defaultPair = false; } public static final class Builder { @@ -158,7 +158,7 @@ public Builder keyId(String keyId) { } public Builder isDefaultPair(boolean isDefaultPair) { - keyPairResource.isDefaultPair = isDefaultPair; + keyPairResource.defaultPair = isDefaultPair; return this; } diff --git a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/KeyPairState.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/KeyPairState.java similarity index 96% rename from spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/KeyPairState.java rename to spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/KeyPairState.java index 48127398d..ec4b2952a 100644 --- a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/KeyPairState.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/KeyPairState.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.spi.store.model; +package org.eclipse.edc.identityhub.spi.model; import java.util.Arrays; diff --git a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/KeyPairResourceStore.java b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/KeyPairResourceStore.java index 35c070e91..d88bcaff9 100644 --- a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/KeyPairResourceStore.java +++ b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/KeyPairResourceStore.java @@ -14,7 +14,7 @@ package org.eclipse.edc.identityhub.spi.store; -import org.eclipse.edc.identityhub.spi.store.model.KeyPairResource; +import org.eclipse.edc.identityhub.spi.model.KeyPairResource; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.StoreResult; diff --git a/spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/store/test/KeyPairResourceStoreTestBase.java b/spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/store/test/KeyPairResourceStoreTestBase.java index d2cfa59a3..b90b299b5 100644 --- a/spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/store/test/KeyPairResourceStoreTestBase.java +++ b/spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/store/test/KeyPairResourceStoreTestBase.java @@ -15,9 +15,9 @@ package org.eclipse.edc.identityhub.store.test; import org.assertj.core.api.Assertions; +import org.eclipse.edc.identityhub.spi.model.KeyPairResource; +import org.eclipse.edc.identityhub.spi.model.KeyPairState; import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore; -import org.eclipse.edc.identityhub.spi.store.model.KeyPairResource; -import org.eclipse.edc.identityhub.spi.store.model.KeyPairState; import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.spi.query.QuerySpec; import org.junit.jupiter.api.Test;