Skip to content

Commit

Permalink
feat: add Management API for KeyPairResources
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger committed Jan 25, 2024
1 parent 3928c05 commit a10c644
Show file tree
Hide file tree
Showing 16 changed files with 570 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<KeyPairResource> implements KeyPairResourceStore {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -105,6 +106,11 @@ public ServiceResult<Void> revokeKey(String id, @Nullable KeyDescriptor newKeySp
return ServiceResult.success();
}

@Override
public ServiceResult<Collection<KeyPairResource>> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
19 changes: 19 additions & 0 deletions extensions/api/keypair-mgmt-api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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))
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<KeyPairResource> 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);
}
Original file line number Diff line number Diff line change
@@ -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<KeyPairResource> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit a10c644

Please sign in to comment.