diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ManagementApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ManagementApiEndToEndTest.java index 3ea7f729f..bec5feaf2 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ManagementApiEndToEndTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ManagementApiEndToEndTest.java @@ -99,6 +99,12 @@ protected Collection getDidForParticipant(String participantId) { .build()).getContent(); } + protected ParticipantContext getParticipant(String participantId) { + return getService(ParticipantContextService.class) + .getParticipantContext(participantId) + .orElseThrow(f -> new EdcException(f.getFailureDetail())); + } + protected static ParticipantManifest createNewParticipant() { var manifest = ParticipantManifest.Builder.newInstance() .participantId("another-participant") diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ParticipantContextApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ParticipantContextApiEndToEndTest.java index 18613508d..8c2074be1 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ParticipantContextApiEndToEndTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ParticipantContextApiEndToEndTest.java @@ -26,6 +26,10 @@ import org.eclipse.edc.spi.event.EventRouter; import org.eclipse.edc.spi.event.EventSubscriber; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; import java.util.Arrays; @@ -83,7 +87,7 @@ void getUserById_notOwner_expect403() { } @Test - void createNewUser_principalIsAdmin() { + void createNewUser_principalIsSuperser() { var subscriber = mock(EventSubscriber.class); getService(EventRouter.class).registerSync(ParticipantContextCreated.class, subscriber); var apikey = getSuperUserApiKey(); @@ -109,7 +113,7 @@ void createNewUser_principalIsAdmin() { @Test - void createNewUser_principalIsNotAdmin_expect403() { + void createNewUser_principalIsNotSuperser_expect403() { var subscriber = mock(EventSubscriber.class); getService(EventRouter.class).registerSync(ParticipantContextCreated.class, subscriber); @@ -158,7 +162,7 @@ void createNewUser_principalIsKnown_expect401() { } @Test - void activateParticipant_principalIsAdmin() { + void activateParticipant_principalIsSuperser() { var subscriber = mock(EventSubscriber.class); getService(EventRouter.class).registerSync(ParticipantContextUpdated.class, subscriber); @@ -224,4 +228,37 @@ void regenerateToken() { .body(notNullValue())); } + @Test + void updateRoles() { + var participantId = "some-user"; + createParticipant(participantId); + + RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() + .header(new Header("x-api-key", getSuperUserApiKey())) + .contentType(ContentType.JSON) + .body(List.of("role1", "role2", "admin")) + .put("/v1/participants/%s/roles".formatted(participantId)) + .then() + .log().ifError() + .statusCode(204); + + assertThat(getParticipant(participantId).getRoles()).containsExactlyInAnyOrder("role1", "role2", "admin"); + } + + @ParameterizedTest(name = "Expect 403, role = {0}") + @ValueSource(strings = {"some-role", "admin"}) + void updateRoles_whenNotSuperuser(String role) { + var participantId = "some-user"; + var userToken = createParticipant(participantId); + + RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() + .header(new Header("x-api-key", userToken)) + .contentType(ContentType.JSON) + .body(List.of(role)) + .put("/v1/participants/%s/roles".formatted(participantId)) + .then() + .log().ifError() + .statusCode(403); + } + } diff --git a/extensions/api/participant-context-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/v1/ParticipantContextApi.java b/extensions/api/participant-context-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/v1/ParticipantContextApi.java index 2de020b40..7ed5f2b10 100644 --- a/extensions/api/participant-context-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/v1/ParticipantContextApi.java +++ b/extensions/api/participant-context-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/v1/ParticipantContextApi.java @@ -29,6 +29,8 @@ import org.eclipse.edc.identityhub.spi.model.participant.ParticipantManifest; import org.eclipse.edc.web.spi.ApiErrorDetail; +import java.util.List; + @OpenAPIDefinition(info = @Info(description = "This is the Management API for ParticipantContexts", title = "ParticipantContext Management API", version = "1")) public interface ParticipantContextApi { @@ -109,4 +111,19 @@ public interface ParticipantContextApi { } ) void deleteParticipant(String participantId, SecurityContext securityContext); + + @Tag(name = "ParticipantContext Management API") + @Operation(description = "Updates a ParticipantContext's roles. Note that this is an absolute update, that means all roles that the Participant should have must be submitted in the body. Requires elevated privileges.", + requestBody = @RequestBody(content = @Content(array = @ArraySchema(schema = @Schema(implementation = List.class)))), + responses = { + @ApiResponse(responseCode = "200", description = "The ParticipantContext was updated successfully"), + @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 ParticipantContext with the given ID does not exist.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) + } + ) + void updateRoles(String participantId, List roles); } diff --git a/extensions/api/participant-context-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/v1/ParticipantContextApiController.java b/extensions/api/participant-context-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/v1/ParticipantContextApiController.java index e46d24fc3..bb42d2179 100644 --- a/extensions/api/participant-context-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/v1/ParticipantContextApiController.java +++ b/extensions/api/participant-context-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/v1/ParticipantContextApiController.java @@ -19,6 +19,7 @@ import jakarta.ws.rs.DELETE; 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; @@ -33,6 +34,8 @@ import org.eclipse.edc.identityhub.spi.model.participant.ParticipantManifest; import org.eclipse.edc.web.spi.exception.ValidationFailureException; +import java.util.List; + import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; import static org.eclipse.edc.identityhub.spi.AuthorizationResultHandler.exceptionMapper; @@ -100,4 +103,15 @@ public void deleteParticipant(@PathParam("participantId") String participantId, .orElseThrow(exceptionMapper(ParticipantContext.class, participantId)); } + @Override + @PUT + @Path("/{participantId}/roles") + @RolesAllowed(ServicePrincipal.ROLE_ADMIN) + public void updateRoles(@PathParam("participantId") String participantId, List roles) { + participantContextService.updateParticipant(participantId, participantContext -> { + participantContext.getRoles().clear(); + participantContext.getRoles().addAll(roles); + }).orElseThrow(exceptionMapper(ParticipantContext.class, participantId)); + } + } diff --git a/extensions/api/participant-context-mgmt-api/src/test/java/org/eclipse/edc/identityhub/api/participantcontext/v1/ParticipantContextApiControllerTest.java b/extensions/api/participant-context-mgmt-api/src/test/java/org/eclipse/edc/identityhub/api/participantcontext/v1/ParticipantContextApiControllerTest.java index 4690f8f57..815c94dc0 100644 --- a/extensions/api/participant-context-mgmt-api/src/test/java/org/eclipse/edc/identityhub/api/participantcontext/v1/ParticipantContextApiControllerTest.java +++ b/extensions/api/participant-context-mgmt-api/src/test/java/org/eclipse/edc/identityhub/api/participantcontext/v1/ParticipantContextApiControllerTest.java @@ -34,6 +34,7 @@ import org.junit.jupiter.api.Test; import java.time.Instant; +import java.util.List; import java.util.Map; import static io.restassured.RestAssured.given; @@ -41,6 +42,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -200,6 +202,38 @@ void delete_notFound() { verify(participantContextServiceMock).deleteParticipantContext(eq("test-participant")); } + @Test + void updateRoles() { + when(participantContextServiceMock.updateParticipant(anyString(), any())).thenReturn(ServiceResult.success()); + + baseRequest() + .body(List.of("role1", "role2")) + .put("/test-participant/roles") + .then() + .log().ifValidationFails() + .statusCode(204); + + verify(participantContextServiceMock).updateParticipant(anyString(), argThat(con -> { + var pc = createParticipantContext().build(); + con.accept(pc); + return pc.getRoles().containsAll(List.of("role1", "role2")); + })); + + } + + @Test + void updateRoles_notFound() { + when(participantContextServiceMock.updateParticipant(anyString(), any())).thenReturn(ServiceResult.notFound("foobar")); + + baseRequest() + .body(List.of("role1", "role2")) + .put("/test-participant/roles") + .then() + .log().ifValidationFails() + .statusCode(404); + verify(participantContextServiceMock).updateParticipant(anyString(), any()); + } + @Override protected Object controller() { return new ParticipantContextApiController(new ParticipantManifestValidator(), participantContextServiceMock, authService);