From cdb8379cb4f3f7438525d9b2981d4c083ba6b666 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger <43503240+paullatzelsperger@users.noreply.github.com> Date: Wed, 13 Nov 2024 08:38:34 +0100 Subject: [PATCH] fix(api): gracefully handle missing type in query credential by type (#490) * fix(api): gracefully handle missing type in query credential by type * checkstyle * added parameter description [skip ci] --- .../VerifiableCredentialApiEndToEndTest.java | 40 +++++++++++++++++++ .../eclipse/edc/test/bom/BomSmokeTests.java | 31 ++++++++------ .../v1/unstable/VerifiableCredentialsApi.java | 5 ++- .../VerifiableCredentialsApiController.java | 14 ++++--- ...erifiableCredentialsApiControllerTest.java | 18 +++++++++ 5 files changed, 89 insertions(+), 19 deletions(-) diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/VerifiableCredentialApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/VerifiableCredentialApiEndToEndTest.java index 51a854440..91c234a1c 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/VerifiableCredentialApiEndToEndTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/VerifiableCredentialApiEndToEndTest.java @@ -160,6 +160,46 @@ void delete(IdentityHubEndToEndTestContext context) { }); } + @Test + void queryByType(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + var user = "user1"; + var token = context.createParticipant(user); + + var credential = context.createCredential(); + context.storeCredential(credential, user); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .get("/v1alpha/participants/%s/credentials?type=%s".formatted(toBase64(user), credential.getType().get(0))) + .then() + .log().ifValidationFails() + .statusCode(200) + .body(notNullValue())); + } + + @Test + void queryByTyp_noTypeSpecified(IdentityHubEndToEndTestContext context) { + var superUserKey = context.createSuperUser(); + var user = "user1"; + var token = context.createParticipant(user); + + var credential = context.createCredential(); + context.storeCredential(credential, user); + + assertThat(Arrays.asList(token, superUserKey)) + .allSatisfy(t -> context.getIdentityApiEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .get("/v1alpha/participants/%s/credentials".formatted(toBase64(user))) + .then() + .log().ifValidationFails() + .statusCode(200) + .body(notNullValue())); + } + private String toBase64(String s) { return Base64.getUrlEncoder().encodeToString(s.getBytes()); } diff --git a/e2e-tests/bom-tests/src/test/java/org/eclipse/edc/test/bom/BomSmokeTests.java b/e2e-tests/bom-tests/src/test/java/org/eclipse/edc/test/bom/BomSmokeTests.java index b1011f6cf..779ca6aff 100644 --- a/e2e-tests/bom-tests/src/test/java/org/eclipse/edc/test/bom/BomSmokeTests.java +++ b/e2e-tests/bom-tests/src/test/java/org/eclipse/edc/test/bom/BomSmokeTests.java @@ -24,7 +24,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import java.util.HashMap; -import java.util.Map; import static io.restassured.RestAssured.given; import static java.lang.String.valueOf; @@ -58,17 +57,23 @@ class IdentityHub extends SmokeTest { @RegisterExtension protected RuntimeExtension runtime = new RuntimePerMethodExtension(new EmbeddedRuntime("identityhub-bom", - Map.of( - "web.http.port", DEFAULT_PORT, - "web.http.path", DEFAULT_PATH, - "edc.ih.iam.id", "did:web:test", - "edc.ih.iam.publickey.path", "/some/path/to/key.pem", - "web.http.presentation.port", valueOf(getFreePort()), - "web.http.presentation.path", "/api/resolution", - "web.http.identity.port", valueOf(getFreePort()), - "web.http.identity.path", "/api/identity", - "edc.sts.account.api.url", "https://sts.com/accounts", - "edc.sts.accounts.api.auth.header.value", "password"), + new HashMap<>() { + + { + put("web.http.port", DEFAULT_PORT); + put("web.http.path", DEFAULT_PATH); + put("edc.ih.iam.id", "did:web:test"); + put("edc.ih.iam.publickey.path", "/some/path/to/key.pem"); + put("web.http.presentation.port", valueOf(getFreePort())); + put("web.http.presentation.path", "/api/resolution"); + put("web.http.identity.port", valueOf(getFreePort())); + put("web.http.identity.path", "/api/identity"); + put("web.http.version.port", valueOf(getFreePort())); + put("web.http.version.path", "/api/version"); + put("edc.sts.account.api.url", "https://sts.com/accounts"); + put("edc.sts.accounts.api.auth.header.value", "password"); + } + }, ":dist:bom:identityhub-bom" )); } @@ -92,6 +97,8 @@ class IdentityHubWithSts extends SmokeTest { put("web.http.identity.path", "/api/identity"); put("web.http.accounts.port", valueOf(getFreePort())); put("web.http.accounts.path", "/api/accounts"); + put("web.http.version.port", valueOf(getFreePort())); + put("web.http.version.path", "/api/version"); put("edc.api.accounts.key", "password"); } }, diff --git a/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApi.java b/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApi.java index b961fe10e..c0907b3f5 100644 --- a/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApi.java +++ b/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApi.java @@ -95,7 +95,8 @@ public interface VerifiableCredentialsApi { @Operation(description = "Query VerifiableCredentials by type.", operationId = "queryCredentialsByType", parameters = { - @Parameter(name = "participantId", description = "Base64-Url encode Participant Context ID", required = true, in = ParameterIn.PATH) + @Parameter(name = "participantId", description = "Base64-Url encode Participant Context ID", required = true, in = ParameterIn.PATH), + @Parameter(name = "type", description = "Credential type. If omitted, all credentials are returned (limited to 50 elements).") }, responses = { @ApiResponse(responseCode = "200", description = "The list of VerifiableCredentials.", @@ -114,7 +115,7 @@ public interface VerifiableCredentialsApi { @Parameter(name = "participantId", description = "Base64-Url encode Participant Context ID", required = true, in = ParameterIn.PATH), }, responses = { - @ApiResponse(responseCode = "200", description = "The VerifiableCredential was deleted successfully", content = {@Content(schema = @Schema(implementation = String.class))}), + @ApiResponse(responseCode = "200", description = "The VerifiableCredential was deleted successfully", content = { @Content(schema = @Schema(implementation = String.class)) }), @ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), @ApiResponse(responseCode = "403", description = "The request could not be completed, because either the authentication was missing or was not valid.", diff --git a/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiController.java b/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiController.java index eb02a7194..7d5f383f3 100644 --- a/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiController.java +++ b/extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiController.java @@ -37,9 +37,11 @@ import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.ServiceResult; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.eclipse.edc.util.string.StringUtils; import org.eclipse.edc.web.spi.exception.InvalidRequestException; import org.eclipse.edc.web.spi.exception.ObjectNotFoundException; import org.eclipse.edc.web.spi.exception.ValidationFailureException; +import org.jetbrains.annotations.Nullable; import java.util.Collection; @@ -106,12 +108,14 @@ public void updateCredential(VerifiableCredentialManifest manifest, @Context Sec @GET @Override - public Collection queryCredentialsByType(@QueryParam("type") String type, @Context SecurityContext securityContext) { - var query = QuerySpec.Builder.newInstance() - .filter(new Criterion("verifiableCredential.credential.types", "contains", type)) - .build(); + public Collection queryCredentialsByType(@Nullable @QueryParam("type") String type, @Context SecurityContext securityContext) { + var query = QuerySpec.Builder.newInstance(); - return credentialStore.query(query) + if (!StringUtils.isNullOrEmpty(type)) { + query.filter(new Criterion("verifiableCredential.credential.types", "contains", type)); + } + + return credentialStore.query(query.build()) .orElseThrow(InvalidRequestException::new) .stream() .filter(vcr -> authorizationService.isAuthorized(securityContext, vcr.getId(), VerifiableCredentialResource.class).succeeded()) diff --git a/extensions/api/identity-api/verifiable-credentials-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiControllerTest.java b/extensions/api/identity-api/verifiable-credentials-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiControllerTest.java index 320d7cc7a..cb5858ccf 100644 --- a/extensions/api/identity-api/verifiable-credentials-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiControllerTest.java +++ b/extensions/api/identity-api/verifiable-credentials-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiControllerTest.java @@ -316,6 +316,24 @@ void notAuthorized_returns403() { verifyNoMoreInteractions(credentialStore); } + @Test + void typeNotSpecified_returnsAllCredentials() { + var credential1 = createCredentialResource("test-type").build(); + var credential2 = createCredentialResource("test-type").build(); + when(credentialStore.query(any())).thenReturn(StoreResult.success(List.of(credential1, credential2))); + + var result = baseRequest() + .get() + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(VerifiableCredentialResource[].class); + + assertThat(result).usingRecursiveFieldByFieldElementComparatorIgnoringFields("clock").containsExactlyInAnyOrder(credential1, credential2); + verify(credentialStore).query(any()); + verifyNoMoreInteractions(credentialStore); + } + @Test void emptyResult() { when(credentialStore.query(any())).thenReturn(StoreResult.success(List.of()));