Skip to content

Commit

Permalink
feat: add Management API for KeyPairResources (#239)
Browse files Browse the repository at this point in the history
* feat: add Management API for KeyPairResources

* moved resource classes to SPI

* fixed import

* add auth service to KeyPair API

* added E2E test for KeyPair API

* checkstyle

* rotate: successor key is optional

* incorporate latest upstream changes
  • Loading branch information
paullatzelsperger authored Jan 29, 2024
1 parent f33e3ba commit 2536037
Show file tree
Hide file tree
Showing 48 changed files with 1,044 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import org.eclipse.edc.identityhub.spi.generator.VerifiablePresentationService;
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationQuery;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationQueryMessage;
import org.eclipse.edc.jsonld.spi.JsonLd;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
Expand Down Expand Up @@ -68,7 +68,7 @@ public String name() {
@Override
public void initialize(ServiceExtensionContext context) {
// setup validator
validatorRegistry.register(PresentationQuery.PRESENTATION_QUERY_TYPE_PROPERTY, new PresentationQueryValidator());
validatorRegistry.register(PresentationQueryMessage.PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY, new PresentationQueryValidator());


var controller = new PresentationApiController(validatorRegistry, typeTransformer, credentialResolver, accessTokenVerifier, verifiablePresentationService, context.getMonitor());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.json.JsonObject;
import jakarta.ws.rs.core.Response;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationResponse;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationResponseMessage;

@OpenAPIDefinition(
info = @Info(description = "This represents the Presentation API as per IATP specification. It serves endpoints to query for specific VerifiablePresentations.", title = "Resolution API",
Expand All @@ -45,13 +45,13 @@ public interface PresentationApi {
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = ApiSchema.PresentationQuerySchema.class), mediaType = "application/ld+json")),
responses = {
@ApiResponse(responseCode = "200", description = "The query was successfully processed, the response contains the VerifiablePresentation",
content = @Content(schema = @Schema(implementation = PresentationResponse.class), mediaType = "application/ld+json")),
content = @Content(schema = @Schema(implementation = PresentationResponseMessage.class), mediaType = "application/ld+json")),
@ApiResponse(responseCode = "400", description = "Request body was malformed, for example when both scope and presentationDefinition are given",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)), mediaType = "application/json")),
@ApiResponse(responseCode = "401", description = "No Authorization header was given.",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)), mediaType = "application/json")),
@ApiResponse(responseCode = "403", description = "The given authentication token could not be validated. This can happen, when the request body " +
"calls for a broader query scope than the granted scope in the auth token",
"calls for a broader query scope than the granted scope in the auth token",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)), mediaType = "application/json")),
@ApiResponse(responseCode = "501", description = "When the request contained a presentationDefinition object, but the implementation does not support it.",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)), mediaType = "application/json"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import org.eclipse.edc.identityhub.spi.generator.VerifiablePresentationService;
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationQuery;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationQueryMessage;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.transform.spi.TypeTransformerRegistry;
Expand All @@ -42,7 +42,7 @@

import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
import static org.eclipse.edc.identitytrust.model.credentialservice.PresentationQuery.PRESENTATION_QUERY_TYPE_PROPERTY;
import static org.eclipse.edc.identitytrust.model.credentialservice.PresentationQueryMessage.PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY;

@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
Expand Down Expand Up @@ -74,9 +74,9 @@ public Response queryPresentation(JsonObject query, @HeaderParam(AUTHORIZATION)
if (token == null) {
throw new AuthenticationFailedException("Authorization header missing");
}
validatorRegistry.validate(PRESENTATION_QUERY_TYPE_PROPERTY, query).orElseThrow(ValidationFailureException::new);
validatorRegistry.validate(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY, query).orElseThrow(ValidationFailureException::new);

var presentationQuery = transformerRegistry.transform(query, PresentationQuery.class).orElseThrow(InvalidRequestException::new);
var presentationQuery = transformerRegistry.transform(query, PresentationQueryMessage.class).orElseThrow(InvalidRequestException::new);

if (presentationQuery.getPresentationDefinition() != null) {
monitor.warning("DIF Presentation Queries are not supported yet. This will get implemented in future iterations.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonValue;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationQuery;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationQueryMessage;
import org.eclipse.edc.jsonld.spi.JsonLdKeywords;
import org.eclipse.edc.validator.spi.ValidationResult;
import org.eclipse.edc.validator.spi.Validator;
Expand All @@ -27,7 +27,7 @@
import static org.eclipse.edc.validator.spi.Violation.violation;

/**
* Validates, that a JsonObject representing a {@link PresentationQuery} contains <em>either</em> a {@code scope} property,
* Validates, that a JsonObject representing a {@link PresentationQueryMessage} contains <em>either</em> a {@code scope} property,
* <em>or</em> a {@code presentationDefinition} query.
*/
public class PresentationQueryValidator implements Validator<JsonObject> {
Expand All @@ -36,9 +36,9 @@ public ValidationResult validate(JsonObject input) {
if (input == null) {
return failure(violation("Presentation query was null", "."));
}
var scope = input.get(PresentationQuery.PRESENTATION_QUERY_SCOPE_PROPERTY);
var scope = input.get(PresentationQueryMessage.PRESENTATION_QUERY_MESSAGE_SCOPE_PROPERTY);

var presentationDef = input.get(PresentationQuery.PRESENTATION_QUERY_DEFINITION_PROPERTY);
var presentationDef = input.get(PresentationQueryMessage.PRESENTATION_QUERY_MESSAGE_DEFINITION_PROPERTY);

if (isNullObject(scope) && isNullObject(presentationDef)) {
return failure(violation("Must contain either a 'scope' or a 'presentationDefinition' property.", null));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
import org.eclipse.edc.identityhub.spi.resolution.QueryResult;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
import org.eclipse.edc.identitytrust.model.credentialservice.InputDescriptorMapping;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationQuery;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationResponse;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationQueryMessage;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationResponseMessage;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationSubmission;
import org.eclipse.edc.identitytrust.model.presentationdefinition.PresentationDefinition;
import org.eclipse.edc.junit.annotations.ApiTest;
Expand Down Expand Up @@ -51,7 +51,7 @@
import static org.eclipse.edc.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.buildSignedJwt;
import static org.eclipse.edc.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.generateEcKey;
import static org.eclipse.edc.identityhub.spi.resolution.QueryResult.success;
import static org.eclipse.edc.identitytrust.model.credentialservice.PresentationQuery.PRESENTATION_QUERY_TYPE_PROPERTY;
import static org.eclipse.edc.identitytrust.model.credentialservice.PresentationQueryMessage.PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY;
import static org.eclipse.edc.validator.spi.ValidationResult.failure;
import static org.eclipse.edc.validator.spi.ValidationResult.success;
import static org.eclipse.edc.validator.spi.Violation.violation;
Expand Down Expand Up @@ -84,7 +84,7 @@ void query_tokenNotPresent_shouldReturn401() {

@Test
void query_validationError_shouldReturn400() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(failure(violation("foo", "bar")));
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(failure(violation("foo", "bar")));

assertThatThrownBy(() -> controller().queryPresentation(createObjectBuilder().build(), generateJwt()))
.isInstanceOf(ValidationFailureException.class)
Expand All @@ -93,8 +93,8 @@ void query_validationError_shouldReturn400() {

@Test
void query_transformationError_shouldReturn400() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(success());
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.failure("cannot transform"));
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(success());
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.failure("cannot transform"));

assertThatThrownBy(() -> controller().queryPresentation(createObjectBuilder().build(), generateJwt()))
.isInstanceOf(InvalidRequestException.class)
Expand All @@ -104,10 +104,10 @@ void query_transformationError_shouldReturn400() {

@Test
void query_withPresentationDefinition_shouldReturn501() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(success());
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(success());
var presentationQueryBuilder = createPresentationQueryBuilder()
.presentationDefinition(PresentationDefinition.Builder.newInstance().id(UUID.randomUUID().toString()).build());
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.success(presentationQueryBuilder.build()));
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.success(presentationQueryBuilder.build()));

var response = controller().queryPresentation(createObjectBuilder().build(), generateJwt());
assertThat(response.getStatus()).isEqualTo(503);
Expand All @@ -121,9 +121,9 @@ void query_withPresentationDefinition_shouldReturn501() {

@Test
void query_tokenVerificationFails_shouldReturn401() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(success());
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(success());
var presentationQueryBuilder = createPresentationQueryBuilder().build();
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.success(presentationQueryBuilder));
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.success(presentationQueryBuilder));
when(accessTokenVerifier.verify(anyString())).thenReturn(Result.failure("test-failure"));

assertThatThrownBy(() -> controller().queryPresentation(createObjectBuilder().build(), generateJwt()))
Expand All @@ -134,9 +134,9 @@ void query_tokenVerificationFails_shouldReturn401() {

@Test
void query_queryResolutionFails_shouldReturn403() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(success());
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(success());
var presentationQueryBuilder = createPresentationQueryBuilder().build();
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.success(presentationQueryBuilder));
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.success(presentationQueryBuilder));
when(accessTokenVerifier.verify(anyString())).thenReturn(Result.success(List.of("test-scope1")));
when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(QueryResult.unauthorized("test-failure"));

Expand All @@ -148,9 +148,9 @@ void query_queryResolutionFails_shouldReturn403() {

@Test
void query_presentationGenerationFails_shouldReturn500() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(success());
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(success());
var presentationQueryBuilder = createPresentationQueryBuilder().build();
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.success(presentationQueryBuilder));
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.success(presentationQueryBuilder));
when(accessTokenVerifier.verify(anyString())).thenReturn(Result.success(List.of("test-scope1")));
when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(success(Stream.empty()));

Expand All @@ -163,13 +163,15 @@ void query_presentationGenerationFails_shouldReturn500() {

@Test
void query_success() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(success());
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(success());
var presentationQueryBuilder = createPresentationQueryBuilder().build();
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.success(presentationQueryBuilder));
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.success(presentationQueryBuilder));
when(accessTokenVerifier.verify(anyString())).thenReturn(Result.success(List.of("test-scope1")));
when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(success(Stream.empty()));

var pres = new PresentationResponse(new Object[] {generateJwt()}, new PresentationSubmission("id", "def-id", List.of(new InputDescriptorMapping("id", "ldp_vp", "$.verifiableCredentials[0]"))));
var pres = PresentationResponseMessage.Builder.newinstance().presentation(List.of(generateJwt()))
.presentationSubmission(new PresentationSubmission("id", "def-id", List.of(new InputDescriptorMapping("id", "ldp_vp", "$.verifiableCredentials[0]"))))
.build();
when(generator.createPresentation(anyList(), any(), any())).thenReturn(Result.success(pres));

var response = controller().queryPresentation(createObjectBuilder().build(), generateJwt());
Expand All @@ -195,8 +197,8 @@ private String generateJwt() {
return jwt.serialize();
}

private PresentationQuery.Builder createPresentationQueryBuilder() {
return PresentationQuery.Builder.newinstance()
private PresentationQueryMessage.Builder createPresentationQueryBuilder() {
return PresentationQueryMessage.Builder.newinstance()
.scopes(List.of("test-scope1", "test-scope2"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import org.eclipse.edc.identityhub.api.validation.PresentationQueryValidator;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationQuery;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationQueryMessage;
import org.eclipse.edc.identitytrust.model.presentationdefinition.Constraints;
import org.eclipse.edc.identitytrust.model.presentationdefinition.Field;
import org.eclipse.edc.identitytrust.model.presentationdefinition.InputDescriptor;
Expand Down Expand Up @@ -48,7 +48,7 @@ class PresentationQueryValidatorTest {
@Test
void validate_withScope_success() {
var jo = createObjectBuilder()
.add(PresentationQuery.PRESENTATION_QUERY_SCOPE_PROPERTY, createScopeArray())
.add(PresentationQueryMessage.PRESENTATION_QUERY_MESSAGE_SCOPE_PROPERTY, createScopeArray())
.build();

assertThat(validator.validate(jo)).isSucceeded();
Expand All @@ -61,7 +61,7 @@ void validate_withPresentationDefinition_success() throws JsonProcessingExceptio
.inputDescriptors(List.of(InputDescriptor.Builder.newInstance().id(UUID.randomUUID().toString()).constraints(new Constraints(List.of(Field.Builder.newInstance().build()))).build()))
.build();
var jo = createObjectBuilder()
.add(PresentationQuery.PRESENTATION_QUERY_DEFINITION_PROPERTY, createPresentationDefArray(presDef))
.add(PresentationQueryMessage.PRESENTATION_QUERY_MESSAGE_DEFINITION_PROPERTY, createPresentationDefArray(presDef))
.build();

assertThat(validator.validate(jo)).isSucceeded();
Expand All @@ -81,8 +81,8 @@ void validate_withBoth_fails() throws JsonProcessingException {
.inputDescriptors(List.of(InputDescriptor.Builder.newInstance().id(UUID.randomUUID().toString()).constraints(new Constraints(List.of(Field.Builder.newInstance().build()))).build()))
.build();
var jo = createObjectBuilder()
.add(PresentationQuery.PRESENTATION_QUERY_SCOPE_PROPERTY, createScopeArray())
.add(PresentationQuery.PRESENTATION_QUERY_DEFINITION_PROPERTY, createPresentationDefArray(presDef))
.add(PresentationQueryMessage.PRESENTATION_QUERY_MESSAGE_SCOPE_PROPERTY, createScopeArray())
.add(PresentationQueryMessage.PRESENTATION_QUERY_MESSAGE_DEFINITION_PROPERTY, createPresentationDefArray(presDef))
.build();

assertThat(validator.validate(jo)).isFailed().detail().contains("Must contain either a 'scope' or a 'presentationDefinition', not both.");
Expand Down
Loading

0 comments on commit 2536037

Please sign in to comment.