From 3f7471c5f92d313c0f5b6b4c0dc5b390b69dfd10 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger <43503240+paullatzelsperger@users.noreply.github.com> Date: Mon, 5 Feb 2024 09:58:07 +0100 Subject: [PATCH] feat: add permission override for super-user/admin role (#255) feat: add permission override for supser-user/admin role --- .../tests/DidManagementApiEndToEndTest.java | 93 ++++++----- .../tests/KeyPairResourceApiEndToEndTest.java | 155 ++++++++++-------- .../ParticipantContextApiEndToEndTest.java | 19 +++ .../AuthorizationServiceImpl.java | 6 + .../AuthorizationServiceImplTest.java | 16 ++ 5 files changed, 178 insertions(+), 111 deletions(-) diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/DidManagementApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/DidManagementApiEndToEndTest.java index 10ec45178..5e49fcca2 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/DidManagementApiEndToEndTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/DidManagementApiEndToEndTest.java @@ -24,11 +24,13 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; +import java.util.Arrays; + import static io.restassured.http.ContentType.JSON; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -81,27 +83,34 @@ void publishDid() { var user = "test-user"; var token = createParticipant(user); - RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", token)) - .body(""" - { - "did": "did:web:test-user" + + assertThat(Arrays.asList(token, getSuperUserApiKey())) + .allSatisfy(t -> { + reset(subscriber); + RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .body(""" + { + "did": "did:web:test-user" + } + """) + .post("/v1/participants/%s/dids/publish".formatted(user)) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(Matchers.notNullValue()); + + // verify that the publish event was fired twice + verify(subscriber).on(argThat(env -> { + if (env.getPayload() instanceof DidDocumentPublished event) { + return event.getDid().equals("did:web:test-user"); } - """) - .post("/v1/participants/%s/dids/publish".formatted(user)) - .then() - .log().ifValidationFails() - .statusCode(204) - .body(Matchers.notNullValue()); + return false; + })); + + }); - // verify that the publish event was fired twice - verify(subscriber, times(2)).on(argThat(env -> { - if (env.getPayload() instanceof DidDocumentPublished event) { - return event.getDid().equals("did:web:test-user"); - } - return false; - })); } @Test @@ -150,27 +159,33 @@ void unpublishDid() { var user = "test-user"; var token = createParticipant(user); - RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", token)) - .body(""" - { - "did": "did:web:test-user" + + assertThat(Arrays.asList(token, getSuperUserApiKey())) + .allSatisfy(t -> { + reset(subscriber); + RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .body(""" + { + "did": "did:web:test-user" + } + """) + .post("/v1/participants/%s/dids/unpublish".formatted(user)) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(Matchers.notNullValue()); + + // verify that the publish event was fired twice + verify(subscriber).on(argThat(env -> { + if (env.getPayload() instanceof DidDocumentUnpublished event) { + return event.getDid().equals("did:web:test-user"); } - """) - .post("/v1/participants/%s/dids/unpublish".formatted(user)) - .then() - .log().ifValidationFails() - .statusCode(204) - .body(Matchers.notNullValue()); + return false; + })); + }); - // verify that the publish event was fired twice - verify(subscriber).on(argThat(env -> { - if (env.getPayload() instanceof DidDocumentUnpublished event) { - return event.getDid().equals("did:web:test-user"); - } - return false; - })); } @Test diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/KeyPairResourceApiEndToEndTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/KeyPairResourceApiEndToEndTest.java index 50e388bc1..ee2c60f34 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/KeyPairResourceApiEndToEndTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/KeyPairResourceApiEndToEndTest.java @@ -28,6 +28,7 @@ import org.eclipse.edc.spi.event.EventSubscriber; import org.junit.jupiter.api.Test; +import java.util.Arrays; import java.util.Map; import java.util.UUID; @@ -37,6 +38,7 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; @EndToEndTest @@ -78,15 +80,15 @@ void findById() { var key = createKeyPair(user1); - // attempt to publish user1's DID document, which should fail - RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", token)) - .get("/v1/participants/%s/keypairs/%s".formatted(user1, key)) - .then() - .log().ifValidationFails() - .statusCode(200) - .body(notNullValue()); + assertThat(Arrays.asList(token, getSuperUserApiKey())) + .allSatisfy(t -> RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .get("/v1/participants/%s/keypairs/%s".formatted(user1, key)) + .then() + .log().ifValidationFails() + .statusCode(200) + .body(notNullValue())); } @Test @@ -128,15 +130,16 @@ void findForParticipant() { var key = createKeyPair(user1); - // attempt to publish user1's DID document, which should fail - RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", token)) - .get("/v1/participants/%s/keypairs".formatted(user1)) - .then() - .log().ifValidationFails() - .statusCode(200) - .body(notNullValue()); + assertThat(Arrays.asList(token, getSuperUserApiKey())) + .allSatisfy(t -> RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .get("/v1/participants/%s/keypairs".formatted(user1)) + .then() + .log().ifValidationFails() + .statusCode(200) + .body(notNullValue())); + } @Test @@ -147,22 +150,24 @@ void addKeyPair() { var user1 = "user1"; var token = createParticipant(user1); - - // attempt to publish user1's DID document, which should fail - var keyDesc = createKeyDescriptor(user1).build(); - RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", token)) - .body(keyDesc) - .put("/v1/participants/%s/keypairs?participantId=%s".formatted(user1, user1)) - .then() - .log().ifValidationFails() - .statusCode(204) - .body(notNullValue()); - verify(subscriber).on(argThat(env -> { - var evt = (KeyPairAdded) env.getPayload(); - return evt.getParticipantId().equals(user1) && evt.getKeyId().equals(keyDesc.getKeyId()); - })); + assertThat(Arrays.asList(token, getSuperUserApiKey())) + .allSatisfy(t -> { + var keyDesc = createKeyDescriptor(user1).build(); + RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .body(keyDesc) + .put("/v1/participants/%s/keypairs?participantId=%s".formatted(user1, user1)) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(notNullValue()); + + verify(subscriber).on(argThat(env -> { + var evt = (KeyPairAdded) env.getPayload(); + return evt.getParticipantId().equals(user1) && evt.getKeyId().equals(keyDesc.getKeyId()); + })); + }); } @Test @@ -208,32 +213,36 @@ void rotate() { var keyId = createKeyPair(user1); - // attempt to publish user1's DID document, which should fail - var keyDesc = createKeyDescriptor(user1).build(); - RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", token)) - .body(keyDesc) - .post("/v1/participants/%s/keypairs/%s/rotate".formatted(user1, keyId)) - .then() - .log().ifValidationFails() - .statusCode(204) - .body(notNullValue()); - - // verify that the "rotated" event fired once - verify(subscriber).on(argThat(env -> { - if (env.getPayload() instanceof KeyPairRotated evt) { - return evt.getParticipantId().equals(user1); - } - return false; - })); - // verify that the correct "added" event fired - verify(subscriber).on(argThat(env -> { - if (env.getPayload() instanceof KeyPairAdded evt) { - return evt.getKeyId().equals(keyDesc.getKeyId()); - } - return false; - })); + assertThat(Arrays.asList(token, getSuperUserApiKey())) + .allSatisfy(t -> { + reset(subscriber); + // attempt to publish user1's DID document, which should fail + var keyDesc = createKeyDescriptor(user1).build(); + RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .body(keyDesc) + .post("/v1/participants/%s/keypairs/%s/rotate".formatted(user1, keyId)) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(notNullValue()); + + // verify that the "rotated" event fired once + verify(subscriber).on(argThat(env -> { + if (env.getPayload() instanceof KeyPairRotated evt) { + return evt.getParticipantId().equals(user1); + } + return false; + })); + // verify that the correct "added" event fired + verify(subscriber).on(argThat(env -> { + if (env.getPayload() instanceof KeyPairAdded evt) { + return evt.getKeyId().equals(keyDesc.getKeyId()); + } + return false; + })); + }); } @Test @@ -277,18 +286,20 @@ void revoke() { var keyId = createKeyPair(user1); - // attempt to publish user1's DID document, which should fail - RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() - .contentType(JSON) - .header(new Header("x-api-key", token)) - .post("/v1/participants/%s/keypairs/%s/revoke".formatted(user1, keyId)) - .then() - .log().ifValidationFails() - .statusCode(204) - .body(notNullValue()); - - assertThat(getDidForParticipant(user1)).hasSize(1) - .allSatisfy(dd -> assertThat(dd.getVerificationMethod()).noneMatch(vm -> vm.getId().equals(keyId))); + assertThat(Arrays.asList(token, getSuperUserApiKey())) + .allSatisfy(t -> { + RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", t)) + .post("/v1/participants/%s/keypairs/%s/revoke".formatted(user1, keyId)) + .then() + .log().ifValidationFails() + .statusCode(204) + .body(notNullValue()); + + assertThat(getDidForParticipant(user1)).hasSize(1) + .allSatisfy(dd -> assertThat(dd.getVerificationMethod()).noneMatch(vm -> vm.getId().equals(keyId))); + }); } @Test 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 85679ff5a..18613508d 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 @@ -27,6 +27,8 @@ import org.eclipse.edc.spi.event.EventSubscriber; import org.junit.jupiter.api.Test; +import java.util.Arrays; + import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.equalTo; @@ -205,4 +207,21 @@ void deleteParticipant() { assertThat(getDidForParticipant(participantId)).isEmpty(); } + @Test + void regenerateToken() { + + var participantId = "another-user"; + var userToken = createParticipant(participantId); + + assertThat(Arrays.asList(userToken, getSuperUserApiKey())) + .allSatisfy(t -> RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() + .header(new Header("x-api-key", t)) + .contentType(ContentType.JSON) + .post("/v1/participants/%s/token".formatted(participantId)) + .then() + .log().ifError() + .statusCode(200) + .body(notNullValue())); + } + } diff --git a/extensions/api/identityhub-api-authorization/src/main/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImpl.java b/extensions/api/identityhub-api-authorization/src/main/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImpl.java index 3c54daeec..2a91a8d2f 100644 --- a/extensions/api/identityhub-api-authorization/src/main/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImpl.java +++ b/extensions/api/identityhub-api-authorization/src/main/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImpl.java @@ -16,6 +16,7 @@ import jakarta.ws.rs.core.SecurityContext; import org.eclipse.edc.identityhub.spi.AuthorizationService; +import org.eclipse.edc.identityhub.spi.authentication.ServicePrincipal; import org.eclipse.edc.identityhub.spi.model.ParticipantResource; import org.eclipse.edc.spi.result.ServiceResult; @@ -31,6 +32,11 @@ public class AuthorizationServiceImpl implements AuthorizationService { @Override public ServiceResult isAuthorized(SecurityContext securityContext, String resourceId, Class resourceClass) { + + if (securityContext.isUserInRole(ServicePrincipal.ROLE_ADMIN)) { + return ServiceResult.success(); + } + var function = resourceLookupFunctions.get(resourceClass); var name = securityContext.getUserPrincipal().getName(); if (function == null) { diff --git a/extensions/api/identityhub-api-authorization/src/test/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImplTest.java b/extensions/api/identityhub-api-authorization/src/test/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImplTest.java index 2cb845dfb..fb2fef7a2 100644 --- a/extensions/api/identityhub-api-authorization/src/test/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImplTest.java +++ b/extensions/api/identityhub-api-authorization/src/test/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImplTest.java @@ -21,7 +21,10 @@ import java.security.Principal; import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +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 AuthorizationServiceImplTest { @@ -71,6 +74,19 @@ public String getParticipantId() { .isFailed(); } + @Test + void isAuthorized_whenSuperUser() { + + var securityContext = mock(SecurityContext.class); + when(securityContext.isUserInRole(eq("admin"))).thenReturn(true); + + assertThat(authorizationService.isAuthorized(securityContext, "test-resource-id", TestResource.class)) + .isSucceeded(); + + verify(securityContext).isUserInRole(eq("admin")); + verifyNoMoreInteractions(securityContext); + } + private static class TestResource extends ParticipantResource { }