From 7192e264f515bafcb661353a7a967aa5fec766c9 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Fri, 26 Jan 2024 15:01:48 +0100 Subject: [PATCH 1/4] feat: add authorizationService to mgmt api --- .../did/DidDocumentServiceImpl.java | 3 +- .../did/DidDocumentServiceImplTest.java | 4 +- .../ParticipantContextServiceImpl.java | 9 +- .../ParticipantContextServiceImplTest.java | 8 +- .../tests/DidManagementApiEndToEndTest.java | 101 +++ .../tests/ManagementApiEndToEndTest.java | 93 ++ .../ParticipantContextApiEndToEndTest.java | 143 ++-- .../AuthorizationServiceImpl.java | 50 ++ .../ManagementApiConfigurationExtension.java | 26 + ...ticipantContextManagementApiExtension.java | 8 +- .../v1/ParticipantContextApi.java | 7 +- .../v1/ParticipantContextApiController.java | 28 +- .../ParticipantContextApiControllerTest.java | 11 +- .../VerifiableCredentialApiExtension.java | 17 +- .../v1/VerifiableCredentialsApi.java | 7 +- .../VerifiableCredentialsApiController.java | 25 +- ...erifiableCredentialsApiControllerTest.java | 57 +- .../DidManagementApiExtension.java | 9 +- .../didmanagement/v1/DidManagementApi.java | 17 +- .../v1/DidManagementApiController.java | 50 +- .../v1/DidManagementApiControllerTest.java | 793 ++++++++++-------- .../did/spi/DidDocumentService.java | 2 +- .../identithub/did/spi/model/DidResource.java | 2 +- spi/identity-hub-spi/build.gradle.kts | 1 + .../spi/AuthorizationResultHandler.java | 34 + .../identityhub/spi/AuthorizationService.java | 28 + .../spi}/model/ParticipantResource.java | 2 +- .../model/participant/ParticipantContext.java | 47 +- .../participant/ParticipantManifest.java | 12 + .../spi/store/model/IdentityResource.java | 1 + .../spi/store/model/KeyPairResource.java | 1 + 31 files changed, 1103 insertions(+), 493 deletions(-) create mode 100644 e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/DidManagementApiEndToEndTest.java create mode 100644 e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ManagementApiEndToEndTest.java create mode 100644 extensions/api/identityhub-api-auth/src/main/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImpl.java create mode 100644 spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/AuthorizationResultHandler.java create mode 100644 spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/AuthorizationService.java rename spi/{identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store => identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi}/model/ParticipantResource.java (96%) diff --git a/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/DidDocumentServiceImpl.java b/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/DidDocumentServiceImpl.java index 6727d5f76..343284162 100644 --- a/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/DidDocumentServiceImpl.java +++ b/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/DidDocumentServiceImpl.java @@ -44,11 +44,12 @@ public DidDocumentServiceImpl(TransactionContext transactionContext, DidResource } @Override - public ServiceResult store(DidDocument document) { + public ServiceResult store(DidDocument document, String participantId) { return transactionContext.execute(() -> { var res = DidResource.Builder.newInstance() .document(document) .did(document.getId()) + .participantId(participantId) .build(); var result = didResourceStore.save(res); return result.succeeded() ? diff --git a/core/identity-hub-did/src/test/java/org/eclipse/edc/identityhub/did/DidDocumentServiceImplTest.java b/core/identity-hub-did/src/test/java/org/eclipse/edc/identityhub/did/DidDocumentServiceImplTest.java index 3ab531567..5717e3a22 100644 --- a/core/identity-hub-did/src/test/java/org/eclipse/edc/identityhub/did/DidDocumentServiceImplTest.java +++ b/core/identity-hub-did/src/test/java/org/eclipse/edc/identityhub/did/DidDocumentServiceImplTest.java @@ -60,14 +60,14 @@ void setUp() { void store() { var doc = createDidDocument().build(); when(storeMock.save(argThat(dr -> dr.getDocument().equals(doc)))).thenReturn(StoreResult.success()); - assertThat(service.store(doc)).isSucceeded(); + assertThat(service.store(doc, "test-participant")).isSucceeded(); } @Test void store_alreadyExists() { var doc = createDidDocument().build(); when(storeMock.save(argThat(dr -> dr.getDocument().equals(doc)))).thenReturn(StoreResult.alreadyExists("foo")); - assertThat(service.store(doc)).isFailed().detail().isEqualTo("foo"); + assertThat(service.store(doc, "test-participant")).isFailed().detail().isEqualTo("foo"); verify(storeMock).save(any()); verifyNoInteractions(publisherMock); } diff --git a/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImpl.java b/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImpl.java index a5466de62..8a8c2f194 100644 --- a/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImpl.java +++ b/core/identity-hub-participants/src/main/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImpl.java @@ -38,10 +38,10 @@ import java.security.PublicKey; import java.text.ParseException; import java.util.List; -import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import static java.util.Optional.ofNullable; import static org.eclipse.edc.spi.result.ServiceResult.badRequest; import static org.eclipse.edc.spi.result.ServiceResult.conflict; import static org.eclipse.edc.spi.result.ServiceResult.fromFailure; @@ -103,7 +103,7 @@ public ServiceResult getParticipantContext(String participan @Override public ServiceResult deleteParticipantContext(String participantId) { return transactionContext.execute(() -> { - var did = Optional.ofNullable(findByIdInternal(participantId)).map(ParticipantContext::getDid); + var did = ofNullable(findByIdInternal(participantId)).map(ParticipantContext::getDid); var res = participantContextStore.deleteById(participantId); if (res.failed()) { return fromFailure(res); @@ -155,7 +155,7 @@ private ServiceResult generateDidDocument(ParticipantManifest manifest, JW .publicKeyJwk(publicKey.toJSONObject()) .build())) .build(); - return didDocumentService.store(doc) + return didDocumentService.store(doc, manifest.getParticipantId()) .compose(u -> manifest.isActive() ? didDocumentService.publish(doc.getId()) : success()); } @@ -204,7 +204,7 @@ private ServiceResult createParticipantContext(ParticipantContext context) } private ParticipantContext findByIdInternal(String participantId) { - var resultStream = participantContextStore.query(QuerySpec.Builder.newInstance().filter(new Criterion("participantContext", "=", participantId)).build()); + var resultStream = participantContextStore.query(QuerySpec.Builder.newInstance().filter(new Criterion("participantId", "=", participantId)).build()); if (resultStream.failed()) return null; return resultStream.getContent().stream().findFirst().orElse(null); } @@ -213,6 +213,7 @@ private ParticipantContext findByIdInternal(String participantId) { private ParticipantContext convert(ParticipantManifest manifest) { return ParticipantContext.Builder.newInstance() .participantId(manifest.getParticipantId()) + .roles(manifest.getRoles()) .apiTokenAlias("%s-%s".formatted(manifest.getParticipantId(), API_KEY_ALIAS_SUFFIX)) .state(manifest.isActive() ? ParticipantContextState.ACTIVATED : ParticipantContextState.CREATED) .build(); diff --git a/core/identity-hub-participants/src/test/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImplTest.java b/core/identity-hub-participants/src/test/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImplTest.java index df212697f..607255694 100644 --- a/core/identity-hub-participants/src/test/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImplTest.java +++ b/core/identity-hub-participants/src/test/java/org/eclipse/edc/identityhub/participantcontext/ParticipantContextServiceImplTest.java @@ -63,7 +63,7 @@ class ParticipantContextServiceImplTest { @BeforeEach void setUp() { didDocumentService = mock(); - when(didDocumentService.store(any())).thenReturn(success()); + when(didDocumentService.store(any(), anyString())).thenReturn(success()); when(didDocumentService.publish(anyString())).thenReturn(success()); var keyParserRegistry = new KeyParserRegistryImpl(); keyParserRegistry.register(new PemParser(mock())); @@ -93,7 +93,7 @@ void createParticipantContext_withPublicKeyPem(boolean isActive) { .isSucceeded(); verify(participantContextStore).create(any()); - verify(didDocumentService).store(argThat(dd -> dd.getId().equals(ctx.getDid()))); + verify(didDocumentService).store(argThat(dd -> dd.getId().equals(ctx.getDid())), anyString()); verify(didDocumentService, times(isActive ? 1 : 0)).publish(anyString()); verify(vault).storeSecret(eq(ctx.getParticipantId() + "-apikey"), anyString()); verifyNoMoreInteractions(vault, participantContextStore); @@ -111,7 +111,7 @@ void createParticipantContext_withPublicKeyJwk(boolean isActive) { .isSucceeded(); verify(participantContextStore).create(any()); - verify(didDocumentService).store(argThat(dd -> dd.getId().equals(ctx.getDid()))); + verify(didDocumentService).store(argThat(dd -> dd.getId().equals(ctx.getDid())), anyString()); verify(didDocumentService, times(isActive ? 1 : 0)).publish(anyString()); verify(vault).storeSecret(eq(ctx.getParticipantId() + "-apikey"), anyString()); verifyNoMoreInteractions(vault, participantContextStore); @@ -135,7 +135,7 @@ void createParticipantContext_withKeyGenParams(boolean isActive) { verify(vault).storeSecret(eq(ctx.getKey().getPrivateKeyAlias()), anyString()); verify(vault).storeSecret(eq(ctx.getParticipantId() + "-apikey"), anyString()); - verify(didDocumentService).store(argThat(dd -> dd.getId().equals(ctx.getDid()))); + verify(didDocumentService).store(argThat(dd -> dd.getId().equals(ctx.getDid())), anyString()); verify(didDocumentService, times(isActive ? 1 : 0)).publish(anyString()); verifyNoMoreInteractions(vault, participantContextStore); } 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 new file mode 100644 index 000000000..b712cb72b --- /dev/null +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/DidManagementApiEndToEndTest.java @@ -0,0 +1,101 @@ +/* + * 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.tests; + +import io.restassured.http.Header; +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; +import org.eclipse.edc.junit.annotations.EndToEndTest; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import static io.restassured.http.ContentType.JSON; + +@EndToEndTest +public class DidManagementApiEndToEndTest extends ManagementApiEndToEndTest { + + @Test + void publishDid_notOwner_expect403() { + var user1 = "user1"; + createParticipant(user1); + + + // create second user + var user2 = "user2"; + var user2Context = ParticipantContext.Builder.newInstance() + .participantId(user2) + .did("did:web:" + user2) + .apiTokenAlias(user2 + "-alias") + .build(); + var user2Token = storeParticipant(user2Context); + + // attempt to publish user1's DID document, which should fail + RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() + .contentType(JSON) + .header(new Header("x-api-key", user2Token)) + .body(""" + { + "did": "did:web:user1" + } + """) + .post("/v1/dids/publish") + .then() + .log().ifValidationFails() + .statusCode(403) + .body(Matchers.notNullValue()); + } + + @Test + 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" + } + """) + .post("/v1/dids/publish") + .then() + .log().ifValidationFails() + .statusCode(204) + .body(Matchers.notNullValue()); + } + + @Test + void getState_nowOwner_expect403() { + var user1 = "user1"; + createParticipant(user1); + + var user2 = "user2"; + var token2 = createParticipant(user2); + + RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() + .header(new Header("x-api-key", token2)) + .contentType(JSON) + .body(""" + { + "did": "did:web:user1" + } + """) + .post("/v1/dids/state") + .then() + .log().ifValidationFails() + .statusCode(403); + } + +} 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 new file mode 100644 index 000000000..9cd378fda --- /dev/null +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ManagementApiEndToEndTest.java @@ -0,0 +1,93 @@ +/* + * 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.tests; + +import org.eclipse.edc.iam.did.spi.document.Service; +import org.eclipse.edc.identityhub.participantcontext.ApiTokenGenerator; +import org.eclipse.edc.identityhub.spi.ParticipantContextService; +import org.eclipse.edc.identityhub.spi.model.participant.KeyDescriptor; +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantManifest; +import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore; +import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubRuntimeConfiguration; +import org.eclipse.edc.junit.extensions.EdcRuntimeExtension; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.security.Vault; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.util.Map; + +/** + * Base class for all management API tests + */ +public abstract class ManagementApiEndToEndTest { + public static final String SUPER_USER = "super-user"; + protected static final IdentityHubRuntimeConfiguration RUNTIME_CONFIGURATION = IdentityHubRuntimeConfiguration.Builder.newInstance() + .name("identity-hub") + .id("identity-hub") + .build(); + @RegisterExtension + protected static final EdcRuntimeExtension RUNTIME = new EdcRuntimeExtension(":launcher", "identity-hub", RUNTIME_CONFIGURATION.controlPlaneConfiguration()); + + protected String getSuperUserApiKey() { + var vault = RUNTIME.getContext().getService(Vault.class); + return vault.resolveSecret("super-user-apikey"); + } + + protected String storeParticipant(ParticipantContext pc) { + var store = RUNTIME.getContext().getService(ParticipantContextStore.class); + + var vault = RUNTIME.getContext().getService(Vault.class); + var token = createTokenFor(pc.getParticipantId()); + vault.storeSecret(pc.getApiTokenAlias(), token); + store.create(pc).orElseThrow(f -> new RuntimeException(f.getFailureDetail())); + return token; + } + + protected String createParticipant(String participantId) { + var manifest = ParticipantManifest.Builder.newInstance() + .participantId(participantId) + .active(true) + .serviceEndpoint(new Service("test-service-id", "test-type", "http://foo.bar.com")) + .did("did:web:" + participantId) + .key(KeyDescriptor.Builder.newInstance() + .privateKeyAlias(participantId + "-alias") + .keyId(participantId + "-key") + .keyGeneratorParams(Map.of("algorithm", "EC", "curve", "secp256r1")) + .build()) + .build(); + var srv = RUNTIME.getContext().getService(ParticipantContextService.class); + return srv.createParticipantContext(manifest).orElseThrow(f -> new EdcException(f.getFailureDetail())); + } + + protected String createTokenFor(String userId) { + return new ApiTokenGenerator().generate(userId); + } + + protected static ParticipantManifest createNewParticipant() { + var manifest = ParticipantManifest.Builder.newInstance() + .participantId("another-participant") + .active(false) + .did("did:web:another:participant") + .serviceEndpoint(new Service("test-service", "test-service-type", "https://test.com")) + .key(KeyDescriptor.Builder.newInstance() + .privateKeyAlias("another-alias") + .keyGeneratorParams(Map.of("algorithm", "EdDSA", "curve", "Ed25519")) + .keyId("another-keyid") + .build()) + .build(); + return manifest; + } +} 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 488b37c54..60f5b16eb 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 @@ -16,21 +16,12 @@ import io.restassured.http.ContentType; import io.restassured.http.Header; -import org.eclipse.edc.iam.did.spi.document.Service; -import org.eclipse.edc.identityhub.participantcontext.ApiTokenGenerator; -import org.eclipse.edc.identityhub.spi.model.participant.KeyDescriptor; +import org.eclipse.edc.identityhub.spi.ParticipantContextService; import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; -import org.eclipse.edc.identityhub.spi.model.participant.ParticipantManifest; -import org.eclipse.edc.identityhub.spi.store.ParticipantContextStore; -import org.eclipse.edc.identityhub.tests.fixtures.IdentityHubRuntimeConfiguration; +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContextState; import org.eclipse.edc.junit.annotations.EndToEndTest; -import org.eclipse.edc.junit.extensions.EdcRuntimeExtension; -import org.eclipse.edc.spi.security.Vault; +import org.eclipse.edc.spi.EdcException; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import java.util.List; -import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.anyOf; @@ -38,25 +29,11 @@ import static org.hamcrest.Matchers.notNullValue; @EndToEndTest -public class ParticipantContextApiEndToEndTest { - - public static final String SUPER_USER = "super-user"; - public static final String SUPER_USER_ALIAS = "super-alias"; - protected static final IdentityHubRuntimeConfiguration RUNTIME_CONFIGURATION = IdentityHubRuntimeConfiguration.Builder.newInstance() - .name("identity-hub") - .id("identity-hub") - .build(); - @RegisterExtension - private static final EdcRuntimeExtension RUNTIME = new EdcRuntimeExtension(":launcher", "identity-hub", RUNTIME_CONFIGURATION.controlPlaneConfiguration()); +public class ParticipantContextApiEndToEndTest extends ManagementApiEndToEndTest { @Test void getUserById() { - var pc = ParticipantContext.Builder.newInstance() - .participantId(SUPER_USER) - .did("did:web:superuser") - .apiTokenAlias(SUPER_USER_ALIAS) - .build(); - var apikey = storeParticipant(pc); + var apikey = getSuperUserApiKey(); var su = RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() .header(new Header("x-api-key", apikey)) @@ -64,23 +41,45 @@ void getUserById() { .then() .statusCode(200) .extract().body().as(ParticipantContext.class); - assertThat(su).usingRecursiveComparison().isEqualTo(pc); + assertThat(su.getParticipantId()).isEqualTo(SUPER_USER); } @Test - void createNewUser_principalIsAdmin() { - var pc = ParticipantContext.Builder.newInstance() - .participantId(SUPER_USER) - .did("did:web:superuser") - .apiTokenAlias(SUPER_USER_ALIAS) - .roles(List.of("admin")) + void getUserById_notOwner_expect403() { + var user1 = "user1"; + var user1Context = ParticipantContext.Builder.newInstance() + .participantId(user1) + .did("did:web:" + user1) + .apiTokenAlias(user1 + "-alias") .build(); - var apiToken = storeParticipant(pc); + var apiToken1 = storeParticipant(user1Context); + + var user2 = "user2"; + var user2Context = ParticipantContext.Builder.newInstance() + .participantId(user2) + .did("did:web:" + user2) + .apiTokenAlias(user2 + "-alias") + .build(); + var apiToken2 = storeParticipant(user2Context); + + //user1 attempts to read user2 -> fail + RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() + .header(new Header("x-api-key", apiToken1)) + .contentType(ContentType.JSON) + .get("/v1/participants/" + user2) + .then() + .log().ifValidationFails() + .statusCode(403); + } + + @Test + void createNewUser_principalIsAdmin() { + var apikey = getSuperUserApiKey(); var manifest = createNewParticipant(); RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() - .header(new Header("x-api-key", apiToken)) + .header(new Header("x-api-key", apikey)) .contentType(ContentType.JSON) .body(manifest) .post("/v1/participants/") @@ -92,14 +91,13 @@ void createNewUser_principalIsAdmin() { @Test void createNewUser_principalIsNotAdmin_expect403() { - var pc = ParticipantContext.Builder.newInstance() - .participantId(SUPER_USER) - .did("did:web:superuser") - .apiTokenAlias(SUPER_USER_ALIAS) - .roles(List.of(/*admin role not assigned*/)) + var principal = "another-user"; + var anotherUser = ParticipantContext.Builder.newInstance() + .participantId(principal) + .did("did:web:" + principal) + .apiTokenAlias(principal + "-alias") .build(); - var apiToken = storeParticipant(pc); - + var apiToken = storeParticipant(anotherUser); var manifest = createNewParticipant(); RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() @@ -113,32 +111,45 @@ void createNewUser_principalIsNotAdmin_expect403() { .body(notNullValue()); } - private String createTokenFor(String userId) { - return new ApiTokenGenerator().generate(userId); - } + @Test + void createNewUser_principalIsKnown_expect401() { + var principal = "another-user"; - private String storeParticipant(ParticipantContext pc) { - var store = RUNTIME.getContext().getService(ParticipantContextStore.class); + var manifest = createNewParticipant(); - var vault = RUNTIME.getContext().getService(Vault.class); - var token = createTokenFor(pc.getParticipantId()); - vault.storeSecret(pc.getApiTokenAlias(), token); - store.create(pc).orElseThrow(f -> new RuntimeException(f.getFailureDetail())); - return token; + RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() + .header(new Header("x-api-key", createTokenFor(principal))) + .contentType(ContentType.JSON) + .body(manifest) + .post("/v1/participants/") + .then() + .log().ifError() + .statusCode(401) + .body(notNullValue()); } - private static ParticipantManifest createNewParticipant() { - var manifest = ParticipantManifest.Builder.newInstance() - .participantId("another-participant") - .active(false) - .did("did:web:another:participant") - .serviceEndpoint(new Service("test-service", "test-service-type", "https://test.com")) - .key(KeyDescriptor.Builder.newInstance() - .privateKeyAlias("another-alias") - .keyGeneratorParams(Map.of("algorithm", "EdDSA", "curve", "Ed25519")) - .keyId("another-keyid") - .build()) + @Test + void activateParticipant_principalIsAdmin() { + var participantId = "another-user"; + var anotherUser = ParticipantContext.Builder.newInstance() + .participantId(participantId) + .did("did:web:" + participantId) + .apiTokenAlias(participantId + "-alias") + .state(ParticipantContextState.CREATED) .build(); - return manifest; + storeParticipant(anotherUser); + + RUNTIME_CONFIGURATION.getManagementEndpoint().baseRequest() + .header(new Header("x-api-key", getSuperUserApiKey())) + .contentType(ContentType.JSON) + .post("/v1/participants/%s/state?isActive=true".formatted(participantId)) + .then() + .log().ifError() + .statusCode(204); + + var updatedParticipant = RUNTIME.getContext().getService(ParticipantContextService.class).getParticipantContext(participantId).orElseThrow(f -> new EdcException(f.getFailureDetail())); + assertThat(updatedParticipant.getState()).isEqualTo(ParticipantContextState.ACTIVATED.ordinal()); } + + } diff --git a/extensions/api/identityhub-api-auth/src/main/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImpl.java b/extensions/api/identityhub-api-auth/src/main/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImpl.java new file mode 100644 index 000000000..8e8e00ae4 --- /dev/null +++ b/extensions/api/identityhub-api-auth/src/main/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImpl.java @@ -0,0 +1,50 @@ +/* + * 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.authorization; + +import org.eclipse.edc.identityhub.spi.AuthorizationService; +import org.eclipse.edc.identityhub.spi.model.ParticipantResource; +import org.eclipse.edc.spi.result.ServiceResult; + +import java.security.Principal; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +import static java.util.Optional.ofNullable; + +public class AuthorizationServiceImpl implements AuthorizationService { + private final Map, Function> authorizationCheckFunctions = new HashMap<>(); + + public Map, Function> getAuthorizationCheckFunctions() { + return authorizationCheckFunctions; + } + + @Override + public ServiceResult isAuthorized(Principal user, String resourceId, Class resourceClass) { + var function = authorizationCheckFunctions.get(resourceClass); + if (function == null) { + return ServiceResult.success(); + } + + var isAuthorized = ofNullable(function.apply(resourceId)) + .map(pr -> Objects.equals(pr.getParticipantId(), user.getName())) + .orElse(false); + + return isAuthorized ? ServiceResult.success() : ServiceResult.unauthorized("User '%s' is not authorized to access resource of type %s with ID '%s'.".formatted(user.getName(), resourceClass, resourceId)); + + } +} diff --git a/extensions/api/identityhub-management-api-configuration/src/main/java/org/eclipse/edc/identityhub/api/configuration/ManagementApiConfigurationExtension.java b/extensions/api/identityhub-management-api-configuration/src/main/java/org/eclipse/edc/identityhub/api/configuration/ManagementApiConfigurationExtension.java index 97659b1d2..92de760c2 100644 --- a/extensions/api/identityhub-management-api-configuration/src/main/java/org/eclipse/edc/identityhub/api/configuration/ManagementApiConfigurationExtension.java +++ b/extensions/api/identityhub-management-api-configuration/src/main/java/org/eclipse/edc/identityhub/api/configuration/ManagementApiConfigurationExtension.java @@ -17,7 +17,11 @@ import org.eclipse.edc.identityhub.api.authentication.filter.RoleBasedAccessFeature; import org.eclipse.edc.identityhub.api.authentication.filter.UserAuthenticationFilter; import org.eclipse.edc.identityhub.api.authentication.spi.UserResolver; +import org.eclipse.edc.identityhub.api.authorization.AuthorizationServiceImpl; +import org.eclipse.edc.identityhub.spi.AuthorizationService; import org.eclipse.edc.identityhub.spi.ParticipantContextService; +import org.eclipse.edc.identityhub.spi.model.participant.KeyDescriptor; +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantManifest; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Provider; @@ -29,6 +33,9 @@ import org.eclipse.edc.web.spi.configuration.WebServiceConfigurer; import org.eclipse.edc.web.spi.configuration.WebServiceSettings; +import java.util.List; +import java.util.Map; + import static org.eclipse.edc.identityhub.api.configuration.ManagementApiConfigurationExtension.NAME; @Extension(value = NAME) @@ -70,6 +77,25 @@ public void initialize(ServiceExtensionContext context) { webService.registerResource(alias, new RoleBasedAccessFeature()); webService.registerResource(alias, new UserAuthenticationFilter(createUserService())); + + // create super-user + participantContextService.createParticipantContext(ParticipantManifest.Builder.newInstance() + .participantId("super-user") + .did("did:web:super-user") // doesn't matter, not intended for resolution + .active(true) + .key(KeyDescriptor.Builder.newInstance() + .keyGeneratorParams(Map.of("algorithm", "EdDSA", "curve", "Ed25519")) + .keyId("super-user-key") + .privateKeyAlias("super-user-alias") + .build()) + .roles(List.of("admin")) + .build()) + .onSuccess(apiKey -> context.getMonitor().info("Created user 'super-user'. Please take a note . API Key: %s".formatted(apiKey))); + } + + @Provider + public AuthorizationService createAuthService() { + return new AuthorizationServiceImpl(); } @Provider diff --git a/extensions/api/participant-context-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/ParticipantContextManagementApiExtension.java b/extensions/api/participant-context-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/ParticipantContextManagementApiExtension.java index d1f126089..92ba36023 100644 --- a/extensions/api/participant-context-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/ParticipantContextManagementApiExtension.java +++ b/extensions/api/participant-context-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/ParticipantContextManagementApiExtension.java @@ -17,7 +17,9 @@ import org.eclipse.edc.identityhub.api.configuration.ManagementApiConfiguration; import org.eclipse.edc.identityhub.api.participantcontext.v1.ParticipantContextApiController; import org.eclipse.edc.identityhub.api.participantcontext.v1.validation.ParticipantManifestValidator; +import org.eclipse.edc.identityhub.spi.AuthorizationService; import org.eclipse.edc.identityhub.spi.ParticipantContextService; +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.spi.system.ServiceExtension; @@ -25,6 +27,7 @@ import org.eclipse.edc.web.spi.WebService; import static org.eclipse.edc.identityhub.api.participantcontext.ParticipantContextManagementApiExtension.NAME; +import static org.eclipse.edc.identityhub.spi.AuthorizationResultHandler.exceptionMapper; @Extension(value = NAME) public class ParticipantContextManagementApiExtension implements ServiceExtension { @@ -36,6 +39,8 @@ public class ParticipantContextManagementApiExtension implements ServiceExtensio private ParticipantContextService participantContextService; @Inject private ManagementApiConfiguration webServiceConfiguration; + @Inject + private AuthorizationService authorizationService; @Override public String name() { @@ -44,7 +49,8 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { - var controller = new ParticipantContextApiController(new ParticipantManifestValidator(), participantContextService); + authorizationService.getAuthorizationCheckFunctions().put(ParticipantContext.class, s -> participantContextService.getParticipantContext(s).orElseThrow(exceptionMapper(ParticipantContext.class, s))); + var controller = new ParticipantContextApiController(new ParticipantManifestValidator(), participantContextService, authorizationService); webService.registerResource(webServiceConfiguration.getContextAlias(), controller); } } 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 8895e0a1d..2de020b40 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 @@ -24,6 +24,7 @@ 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 jakarta.ws.rs.core.SecurityContext; import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; import org.eclipse.edc.identityhub.spi.model.participant.ParticipantManifest; import org.eclipse.edc.web.spi.ApiErrorDetail; @@ -61,7 +62,7 @@ public interface ParticipantContextApi { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) } ) - ParticipantContext getParticipant(String participantId); + ParticipantContext getParticipant(String participantId, SecurityContext securityContext); @Tag(name = "ParticipantContext Management API") @Operation(description = "Regenerates the API token for a ParticipantContext and returns the new token.", @@ -76,7 +77,7 @@ public interface ParticipantContextApi { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) } ) - String regenerateToken(String participantId); + String regenerateToken(String participantId, SecurityContext securityContext); @Tag(name = "ParticipantContext Management API") @Operation(description = "Activates a ParticipantContext. This operation is idempotent, i.e. activating an already active ParticipantContext is a NOOP.", @@ -107,5 +108,5 @@ public interface ParticipantContextApi { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) } ) - void deleteParticipant(String participantId); + void deleteParticipant(String participantId, SecurityContext securityContext); } 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 da2f6b802..ccd779d27 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 @@ -23,14 +23,17 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.SecurityContext; import org.eclipse.edc.identityhub.api.participantcontext.v1.validation.ParticipantManifestValidator; +import org.eclipse.edc.identityhub.spi.AuthorizationService; import org.eclipse.edc.identityhub.spi.ParticipantContextService; import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; import org.eclipse.edc.identityhub.spi.model.participant.ParticipantManifest; import org.eclipse.edc.web.spi.exception.ValidationFailureException; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; -import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper; +import static org.eclipse.edc.identityhub.spi.AuthorizationResultHandler.exceptionMapper; @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) @@ -39,10 +42,12 @@ public class ParticipantContextApiController implements ParticipantContextApi { private final ParticipantManifestValidator participantManifestValidator; private final ParticipantContextService participantContextService; + private final AuthorizationService authorizationService; - public ParticipantContextApiController(ParticipantManifestValidator participantManifestValidator, ParticipantContextService participantContextService) { + public ParticipantContextApiController(ParticipantManifestValidator participantManifestValidator, ParticipantContextService participantContextService, AuthorizationService authorizationService) { this.participantManifestValidator = participantManifestValidator; this.participantContextService = participantContextService; + this.authorizationService = authorizationService; } @Override @@ -57,15 +62,19 @@ public String createParticipant(ParticipantManifest manifest) { @Override @GET @Path("/{participantId}") - public ParticipantContext getParticipant(@PathParam("participantId") String participantId) { - return participantContextService.getParticipantContext(participantId).orElseThrow(exceptionMapper(ParticipantContext.class)); + public ParticipantContext getParticipant(@PathParam("participantId") String participantId, @Context SecurityContext securityContext) { + return authorizationService.isAuthorized(securityContext.getUserPrincipal(), participantId, ParticipantContext.class) + .compose(u -> participantContextService.getParticipantContext(participantId)) + .orElseThrow(exceptionMapper(ParticipantContext.class, participantId)); } @Override @POST @Path("/{participantId}/token") - public String regenerateToken(@PathParam("participantId") String participantId) { - return participantContextService.regenerateApiToken(participantId).orElseThrow(exceptionMapper(ParticipantContext.class)); + public String regenerateToken(@PathParam("participantId") String participantId, @Context SecurityContext securityContext) { + return authorizationService.isAuthorized(securityContext.getUserPrincipal(), participantId, ParticipantContext.class) + .compose(u -> participantContextService.regenerateApiToken(participantId)) + .orElseThrow(exceptionMapper(ParticipantContext.class, participantId)); } @Override @@ -78,14 +87,17 @@ public void activateParticipant(@PathParam("participantId") String participantId } else { participantContextService.updateParticipant(participantId, ParticipantContext::deactivate); } + } @Override @DELETE @Path("/{participantId}") @RolesAllowed("admin") - public void deleteParticipant(@PathParam("participantId") String participantId) { - participantContextService.deleteParticipantContext(participantId).orElseThrow(exceptionMapper(ParticipantContext.class)); + public void deleteParticipant(@PathParam("participantId") String participantId, @Context SecurityContext securityContext) { + authorizationService.isAuthorized(securityContext.getUserPrincipal(), participantId, ParticipantContext.class) + .compose(u -> participantContextService.deleteParticipantContext(participantId)) + .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 4833455d2..4690f8f57 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 @@ -20,6 +20,7 @@ import io.restassured.http.ContentType; import io.restassured.specification.RequestSpecification; import org.eclipse.edc.identityhub.api.participantcontext.v1.validation.ParticipantManifestValidator; +import org.eclipse.edc.identityhub.spi.AuthorizationService; import org.eclipse.edc.identityhub.spi.ParticipantContextService; import org.eclipse.edc.identityhub.spi.model.participant.KeyDescriptor; import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; @@ -29,6 +30,7 @@ import org.eclipse.edc.spi.result.ServiceResult; import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.time.Instant; @@ -38,6 +40,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -48,6 +51,12 @@ class ParticipantContextApiControllerTest extends RestControllerTestBase { private final ParticipantContextService participantContextServiceMock = mock(); + private final AuthorizationService authService = mock(); + + @BeforeEach + void setUp() { + when(authService.isAuthorized(any(), anyString(), any())).thenReturn(ServiceResult.success()); + } @Test void getById() { @@ -193,7 +202,7 @@ void delete_notFound() { @Override protected Object controller() { - return new ParticipantContextApiController(new ParticipantManifestValidator(), participantContextServiceMock); + return new ParticipantContextApiController(new ParticipantManifestValidator(), participantContextServiceMock, authService); } private ParticipantContext.Builder createParticipantContext() { diff --git a/extensions/api/verifiable-credential-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/VerifiableCredentialApiExtension.java b/extensions/api/verifiable-credential-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/VerifiableCredentialApiExtension.java index 9b2d0434e..bbfb90821 100644 --- a/extensions/api/verifiable-credential-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/VerifiableCredentialApiExtension.java +++ b/extensions/api/verifiable-credential-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/VerifiableCredentialApiExtension.java @@ -16,9 +16,15 @@ import org.eclipse.edc.identityhub.api.configuration.ManagementApiConfiguration; import org.eclipse.edc.identityhub.api.verifiablecredentials.v1.VerifiableCredentialsApiController; +import org.eclipse.edc.identityhub.spi.AuthorizationService; +import org.eclipse.edc.identityhub.spi.model.ParticipantResource; import org.eclipse.edc.identityhub.spi.store.CredentialStore; +import org.eclipse.edc.identityhub.spi.store.model.VerifiableCredentialResource; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.eclipse.edc.web.spi.WebService; @@ -35,10 +41,19 @@ public class VerifiableCredentialApiExtension implements ServiceExtension { private WebService webService; @Inject private CredentialStore credentialStore; + @Inject + private AuthorizationService authorizationService; @Override public void initialize(ServiceExtensionContext context) { - var controller = new VerifiableCredentialsApiController(credentialStore); + authorizationService.getAuthorizationCheckFunctions().put(VerifiableCredentialResource.class, this::queryById); + var controller = new VerifiableCredentialsApiController(credentialStore, authorizationService); webService.registerResource(apiConfiguration.getContextAlias(), controller); } + + private ParticipantResource queryById(String credentialId) { + return credentialStore.query(QuerySpec.Builder.newInstance().filter(new Criterion("id", "=", credentialId)).build()) + .map(list -> list.iterator().next()) + .orElseThrow(f -> new EdcException(f.getFailureDetail())); + } } diff --git a/extensions/api/verifiable-credential-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/VerifiableCredentialsApi.java b/extensions/api/verifiable-credential-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/VerifiableCredentialsApi.java index 5fd157582..3044fb804 100644 --- a/extensions/api/verifiable-credential-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/VerifiableCredentialsApi.java +++ b/extensions/api/verifiable-credential-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/VerifiableCredentialsApi.java @@ -23,6 +23,7 @@ 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 jakarta.ws.rs.core.SecurityContext; import org.eclipse.edc.iam.did.spi.document.DidDocument; import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; import org.eclipse.edc.identityhub.spi.model.participant.ParticipantManifest; @@ -47,7 +48,7 @@ public interface VerifiableCredentialsApi { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) } ) - VerifiableCredentialResource findById(String id); + VerifiableCredentialResource findById(String id, SecurityContext securityContext); @Tag(name = "VerifiableCredentials Management API") @@ -61,7 +62,7 @@ public interface VerifiableCredentialsApi { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), } ) - Collection findByType(String type); + Collection findByType(String type, SecurityContext securityContext); @Tag(name = "VerifiableCredentials Management API") @Operation(description = "Delete a VerifiableCredential.", @@ -76,5 +77,5 @@ public interface VerifiableCredentialsApi { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) } ) - void deleteCredential(String id); + void deleteCredential(String id, SecurityContext securityContext); } diff --git a/extensions/api/verifiable-credential-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/VerifiableCredentialsApiController.java b/extensions/api/verifiable-credential-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/VerifiableCredentialsApiController.java index 6f3ea516c..1c00eb23f 100644 --- a/extensions/api/verifiable-credential-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/VerifiableCredentialsApiController.java +++ b/extensions/api/verifiable-credential-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/VerifiableCredentialsApiController.java @@ -21,6 +21,9 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.SecurityContext; +import org.eclipse.edc.identityhub.spi.AuthorizationService; import org.eclipse.edc.identityhub.spi.store.CredentialStore; import org.eclipse.edc.identityhub.spi.store.model.VerifiableCredentialResource; import org.eclipse.edc.spi.query.Criterion; @@ -32,7 +35,7 @@ import java.util.Collection; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; -import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper; +import static org.eclipse.edc.identityhub.spi.AuthorizationResultHandler.exceptionMapper; @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) @@ -40,15 +43,20 @@ public class VerifiableCredentialsApiController implements VerifiableCredentialsApi { private final CredentialStore credentialStore; + private final AuthorizationService authorizationService; - public VerifiableCredentialsApiController(CredentialStore credentialStore) { + public VerifiableCredentialsApiController(CredentialStore credentialStore, AuthorizationService authorizationService) { this.credentialStore = credentialStore; + this.authorizationService = authorizationService; } @GET @Path("/{credentialId}") @Override - public VerifiableCredentialResource findById(@PathParam("credentialId") String id) { + public VerifiableCredentialResource findById(@PathParam("credentialId") String id, @Context SecurityContext securityContext) { + authorizationService.isAuthorized(securityContext.getUserPrincipal(), id, VerifiableCredentialResource.class) + .orElseThrow(exceptionMapper(VerifiableCredentialResource.class, id)); + var result = credentialStore.query(QuerySpec.Builder.newInstance().filter(new Criterion("id", "=", id)).build()) .orElseThrow(InvalidRequestException::new); return result.stream().findFirst().orElseThrow(() -> new ObjectNotFoundException(VerifiableCredentialResource.class, id)); @@ -56,18 +64,23 @@ public VerifiableCredentialResource findById(@PathParam("credentialId") String i @GET @Override - public Collection findByType(@QueryParam("type") String type) { + public Collection findByType(@QueryParam("type") String type, @Context SecurityContext securityContext) { var query = QuerySpec.Builder.newInstance() .filter(new Criterion("verifiableCredential.credential.types", "contains", type)) .build(); - return credentialStore.query(query).orElseThrow(InvalidRequestException::new); + return credentialStore.query(query) + .orElseThrow(InvalidRequestException::new) + .stream().filter(vcr -> authorizationService.isAuthorized(securityContext.getUserPrincipal(), vcr.getId(), VerifiableCredentialResource.class).succeeded()) + .toList(); } @DELETE @Path("/{credentialId}") @Override - public void deleteCredential(@PathParam("credentialId") String id) { + public void deleteCredential(@PathParam("credentialId") String id, @Context SecurityContext securityContext) { + authorizationService.isAuthorized(securityContext.getUserPrincipal(), id, VerifiableCredentialResource.class) + .orElseThrow(exceptionMapper(VerifiableCredentialResource.class, id)); var res = credentialStore.deleteById(id); if (res.failed()) { throw exceptionMapper(VerifiableCredentialResource.class, id).apply(ServiceResult.fromFailure(res).getFailure()); diff --git a/extensions/api/verifiable-credential-mgmt-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/VerifiableCredentialsApiControllerTest.java b/extensions/api/verifiable-credential-mgmt-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/VerifiableCredentialsApiControllerTest.java index 4ad8d8aaa..3808b1c46 100644 --- a/extensions/api/verifiable-credential-mgmt-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/VerifiableCredentialsApiControllerTest.java +++ b/extensions/api/verifiable-credential-mgmt-api/src/test/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/VerifiableCredentialsApiControllerTest.java @@ -15,6 +15,7 @@ package org.eclipse.edc.identityhub.api.verifiablecredentials.v1; import io.restassured.specification.RequestSpecification; +import org.eclipse.edc.identityhub.spi.AuthorizationService; import org.eclipse.edc.identityhub.spi.store.CredentialStore; import org.eclipse.edc.identityhub.spi.store.model.VerifiableCredentialResource; import org.eclipse.edc.identitytrust.model.CredentialFormat; @@ -22,8 +23,10 @@ import org.eclipse.edc.identitytrust.model.Issuer; import org.eclipse.edc.identitytrust.model.VerifiableCredential; import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; +import org.eclipse.edc.spi.result.ServiceResult; import org.eclipse.edc.spi.result.StoreResult; import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.time.Instant; @@ -34,6 +37,7 @@ import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -44,6 +48,12 @@ class VerifiableCredentialsApiControllerTest extends RestControllerTestBase { private static final String CREDENTIAL_ID = "test-credential-id"; private final CredentialStore credentialStore = mock(); + private final AuthorizationService authorizationService = mock(); + + @BeforeEach + void setUp() { + when(authorizationService.isAuthorized(any(), anyString(), any())).thenReturn(ServiceResult.success()); + } @Test void findById() { @@ -62,6 +72,19 @@ void findById() { verifyNoMoreInteractions(credentialStore); } + + @Test + void findById_unauthorized403() { + when(authorizationService.isAuthorized(any(), anyString(), any())).thenReturn(ServiceResult.unauthorized("test-message")); + baseRequest() + .get("/%s".formatted(CREDENTIAL_ID)) + .then() + .log().ifValidationFails() + .statusCode(403); + verify(authorizationService).isAuthorized(any(), anyString(), eq(VerifiableCredentialResource.class)); + verifyNoMoreInteractions(credentialStore, authorizationService); + } + @Test void findById_whenNotExists_expect404() { when(credentialStore.query(any())).thenReturn(StoreResult.success(List.of())); @@ -94,6 +117,25 @@ void findByType() { verifyNoMoreInteractions(credentialStore); } + @Test + void findByType_unauthorized403() { + var credential1 = createCredential("test-type").build(); + var credential2 = createCredential("test-type").build(); + when(credentialStore.query(any())).thenReturn(StoreResult.success(List.of(credential1, credential2))); + when(authorizationService.isAuthorized(any(), eq(credential1.getId()), any())).thenReturn(ServiceResult.unauthorized("test-message")); + + var result = baseRequest() + .get("?type=test-type") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(VerifiableCredentialResource[].class); + + assertThat(result).usingRecursiveFieldByFieldElementComparatorIgnoringFields("clock").containsExactlyInAnyOrder(credential2); + verify(credentialStore).query(any()); + verifyNoMoreInteractions(credentialStore); + } + @Test void findByType_noResult() { when(credentialStore.query(any())).thenReturn(StoreResult.success(List.of())); @@ -124,6 +166,19 @@ void deleteCredential() { verifyNoMoreInteractions(credentialStore); } + @Test + void deleteCredential_unauthorized403() { + when(authorizationService.isAuthorized(any(), anyString(), any())).thenReturn(ServiceResult.unauthorized("test-message")); + + baseRequest() + .delete("/%s".formatted(CREDENTIAL_ID)) + .then() + .log().ifValidationFails() + .statusCode(403); + verify(authorizationService).isAuthorized(any(), anyString(), eq(VerifiableCredentialResource.class)); + verifyNoMoreInteractions(credentialStore, authorizationService); + } + @Test void deleteCredential_whenNotExists() { when(credentialStore.deleteById(CREDENTIAL_ID)).thenReturn(StoreResult.notFound("test-message")); @@ -140,7 +195,7 @@ void deleteCredential_whenNotExists() { @Override protected Object controller() { - return new VerifiableCredentialsApiController(credentialStore); + return new VerifiableCredentialsApiController(credentialStore, authorizationService); } private VerifiableCredentialResource.Builder createCredential(String... types) { diff --git a/extensions/did/did-management-api/src/main/java/org/eclipse/edc/identityhub/api/didmanagement/DidManagementApiExtension.java b/extensions/did/did-management-api/src/main/java/org/eclipse/edc/identityhub/api/didmanagement/DidManagementApiExtension.java index 836cc6be9..1496834e2 100644 --- a/extensions/did/did-management-api/src/main/java/org/eclipse/edc/identityhub/api/didmanagement/DidManagementApiExtension.java +++ b/extensions/did/did-management-api/src/main/java/org/eclipse/edc/identityhub/api/didmanagement/DidManagementApiExtension.java @@ -15,8 +15,10 @@ package org.eclipse.edc.identityhub.api.didmanagement; import org.eclipse.edc.identithub.did.spi.DidDocumentService; +import org.eclipse.edc.identithub.did.spi.model.DidResource; import org.eclipse.edc.identityhub.api.configuration.ManagementApiConfiguration; import org.eclipse.edc.identityhub.api.didmanagement.v1.DidManagementApiController; +import org.eclipse.edc.identityhub.spi.AuthorizationService; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.spi.system.ServiceExtension; @@ -36,7 +38,8 @@ public class DidManagementApiExtension implements ServiceExtension { private DidDocumentService didDocumentService; @Inject private ManagementApiConfiguration webServiceConfiguration; - + @Inject + private AuthorizationService authorizationService; @Override public String name() { @@ -45,8 +48,10 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { - var controller = new DidManagementApiController(didDocumentService); + authorizationService.getAuthorizationCheckFunctions().put(DidResource.class, s -> didDocumentService.findById(s)); + var controller = new DidManagementApiController(didDocumentService, authorizationService); webService.registerResource(webServiceConfiguration.getContextAlias(), controller); + } diff --git a/extensions/did/did-management-api/src/main/java/org/eclipse/edc/identityhub/api/didmanagement/v1/DidManagementApi.java b/extensions/did/did-management-api/src/main/java/org/eclipse/edc/identityhub/api/didmanagement/v1/DidManagementApi.java index d20432be7..f50082774 100644 --- a/extensions/did/did-management-api/src/main/java/org/eclipse/edc/identityhub/api/didmanagement/v1/DidManagementApi.java +++ b/extensions/did/did-management-api/src/main/java/org/eclipse/edc/identityhub/api/didmanagement/v1/DidManagementApi.java @@ -25,6 +25,7 @@ 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 jakarta.ws.rs.core.SecurityContext; import org.eclipse.edc.iam.did.spi.document.DidDocument; import org.eclipse.edc.iam.did.spi.document.Service; import org.eclipse.edc.spi.query.QuerySpec; @@ -47,7 +48,7 @@ public interface DidManagementApi { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) } ) - void publishDidFromBody(DidRequestPayload didRequestPayload); + void publishDidFromBody(DidRequestPayload didRequestPayload, SecurityContext securityContext); @Tag(name = "DID Management API") @Operation(description = "Un-Publish an (existing) DID document. The DID is expected to exist in the database.", @@ -62,7 +63,7 @@ public interface DidManagementApi { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) } ) - void unpublishDidFromBody(DidRequestPayload didRequestPayload); + void unpublishDidFromBody(DidRequestPayload didRequestPayload, SecurityContext securityContext); @Tag(name = "DID Management API") @Operation(description = "Query for DID documents..", @@ -76,7 +77,7 @@ public interface DidManagementApi { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), } ) - Collection queryDid(QuerySpec querySpec); + Collection queryDid(QuerySpec querySpec, SecurityContext securityContext); @Tag(name = "DID Management API") @Operation(description = "Get state of a DID document", @@ -89,7 +90,7 @@ public interface DidManagementApi { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")), } ) - String getState(DidRequestPayload request); + String getState(DidRequestPayload request, SecurityContext securityContext); @Tag(name = "DID Management API") @Operation(description = "Adds a service endpoint to a particular DID document.", @@ -105,7 +106,7 @@ public interface DidManagementApi { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) } ) - void addEndpoint(String did, Service service, boolean autoPublish); + void addEndpoint(String did, Service service, boolean autoPublish, SecurityContext securityContext); @Tag(name = "DID Management API") @Operation(description = "Replaces a service endpoint of a particular DID document.", @@ -121,13 +122,13 @@ public interface DidManagementApi { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) } ) - void replaceEndpoint(String did, Service service, boolean autoPublish); + void replaceEndpoint(String did, Service service, boolean autoPublish, SecurityContext securityContext); @Tag(name = "DID Management API") @Operation(description = "Removes a service endpoint from a particular DID document.", requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = Service.class), mediaType = "application/json")), parameters = {@Parameter(name = "serviceId", description = "The ID of the service that should get removed"), @Parameter(name = "autoPublish", description = "Whether the DID should " + - "get republished after the removal. Defaults to false.")}, + "get republished after the removal. Defaults to false.")}, responses = { @ApiResponse(responseCode = "200", description = "The DID document was successfully updated."), @ApiResponse(responseCode = "401", description = "The request could not be completed, because either the authentication was missing or was not valid.", @@ -138,5 +139,5 @@ public interface DidManagementApi { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")) } ) - void removeEndpoint(String did, String serviceId, boolean autoPublish); + void removeEndpoint(String did, String serviceId, boolean autoPublish, SecurityContext securityContext); } diff --git a/extensions/did/did-management-api/src/main/java/org/eclipse/edc/identityhub/api/didmanagement/v1/DidManagementApiController.java b/extensions/did/did-management-api/src/main/java/org/eclipse/edc/identityhub/api/didmanagement/v1/DidManagementApiController.java index 210b055b8..56ef50152 100644 --- a/extensions/did/did-management-api/src/main/java/org/eclipse/edc/identityhub/api/didmanagement/v1/DidManagementApiController.java +++ b/extensions/did/did-management-api/src/main/java/org/eclipse/edc/identityhub/api/didmanagement/v1/DidManagementApiController.java @@ -22,17 +22,21 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.SecurityContext; import org.eclipse.edc.iam.did.spi.document.DidDocument; import org.eclipse.edc.iam.did.spi.document.Service; import org.eclipse.edc.identithub.did.spi.DidDocumentService; +import org.eclipse.edc.identithub.did.spi.model.DidResource; import org.eclipse.edc.identithub.did.spi.model.DidState; +import org.eclipse.edc.identityhub.spi.AuthorizationService; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.ServiceResult; import java.util.Collection; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; -import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper; +import static org.eclipse.edc.identityhub.spi.AuthorizationResultHandler.exceptionMapper; @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) @@ -40,25 +44,29 @@ public class DidManagementApiController implements DidManagementApi { private final DidDocumentService documentService; + private final AuthorizationService authorizationService; - public DidManagementApiController(DidDocumentService documentService) { + public DidManagementApiController(DidDocumentService documentService, AuthorizationService authorizationService) { this.documentService = documentService; + this.authorizationService = authorizationService; } @Override @POST @Path("/publish") - public void publishDidFromBody(DidRequestPayload didRequestPayload) { - documentService.publish(didRequestPayload.did()) - .orElseThrow(exceptionMapper(DidDocument.class, didRequestPayload.did())); + public void publishDidFromBody(DidRequestPayload didRequestPayload, @Context SecurityContext securityContext) { + authorizationService.isAuthorized(securityContext.getUserPrincipal(), didRequestPayload.did(), DidResource.class) + .compose(u -> documentService.publish(didRequestPayload.did())) + .orElseThrow(exceptionMapper(DidResource.class, didRequestPayload.did())); } @Override @POST @Path("/unpublish") - public void unpublishDidFromBody(DidRequestPayload didRequestPayload) { - documentService.unpublish(didRequestPayload.did()) + public void unpublishDidFromBody(DidRequestPayload didRequestPayload, @Context SecurityContext securityContext) { + authorizationService.isAuthorized(securityContext.getUserPrincipal(), didRequestPayload.did(), DidResource.class) + .compose(u -> documentService.unpublish(didRequestPayload.did())) .orElseThrow(exceptionMapper(DidDocument.class, didRequestPayload.did())); } @@ -66,15 +74,19 @@ public void unpublishDidFromBody(DidRequestPayload didRequestPayload) { @POST @Path("/query") @Override - public Collection queryDid(QuerySpec querySpec) { + public Collection queryDid(QuerySpec querySpec, @Context SecurityContext securityContext) { return documentService.queryDocuments(querySpec) - .orElseThrow(exceptionMapper(DidDocument.class, null)); + .orElseThrow(exceptionMapper(DidDocument.class, null)) + .stream().filter(dd -> authorizationService.isAuthorized(securityContext.getUserPrincipal(), dd.getId(), DidResource.class).succeeded()) + .toList(); } @Override @POST @Path("/state") - public String getState(DidRequestPayload request) { + public String getState(DidRequestPayload request, @Context SecurityContext securityContext) { + authorizationService.isAuthorized(securityContext.getUserPrincipal(), request.did(), DidResource.class) + .orElseThrow(exceptionMapper(DidResource.class, request.did())); var byId = documentService.findById(request.did()); return byId != null ? DidState.from(byId.getState()).toString() : null; } @@ -82,8 +94,10 @@ public String getState(DidRequestPayload request) { @Override @POST @Path("/{did}/endpoints") - public void addEndpoint(@PathParam("did") String did, Service service, @QueryParam("autoPublish") boolean autoPublish) { - documentService.addService(did, service) + public void addEndpoint(@PathParam("did") String did, Service service, @QueryParam("autoPublish") boolean autoPublish, + @Context SecurityContext securityContext) { + authorizationService.isAuthorized(securityContext.getUserPrincipal(), did, DidResource.class) + .compose(u -> documentService.addService(did, service)) .compose(v -> autoPublish ? documentService.publish(did) : ServiceResult.success()) .orElseThrow(exceptionMapper(Service.class, did)); } @@ -91,8 +105,10 @@ public void addEndpoint(@PathParam("did") String did, Service service, @QueryPar @Override @PATCH @Path("/{did}/endpoints") - public void replaceEndpoint(@PathParam("did") String did, Service service, @QueryParam("autoPublish") boolean autoPublish) { - documentService.replaceService(did, service) + public void replaceEndpoint(@PathParam("did") String did, Service service, @QueryParam("autoPublish") boolean autoPublish, + @Context SecurityContext securityContext) { + authorizationService.isAuthorized(securityContext.getUserPrincipal(), did, DidResource.class) + .compose(u -> documentService.replaceService(did, service)) .compose(v -> autoPublish ? documentService.publish(did) : ServiceResult.success()) .orElseThrow(exceptionMapper(Service.class, did)); } @@ -100,8 +116,10 @@ public void replaceEndpoint(@PathParam("did") String did, Service service, @Quer @Override @DELETE @Path("/{did}/endpoints") - public void removeEndpoint(@PathParam("did") String did, @QueryParam("serviceId") String serviceId, @QueryParam("autoPublish") boolean autoPublish) { - documentService.removeService(did, serviceId) + public void removeEndpoint(@PathParam("did") String did, @QueryParam("serviceId") String serviceId, @QueryParam("autoPublish") boolean autoPublish, + @Context SecurityContext securityContext) { + authorizationService.isAuthorized(securityContext.getUserPrincipal(), did, DidResource.class) + .compose(u -> documentService.removeService(did, serviceId)) .compose(v -> autoPublish ? documentService.publish(did) : ServiceResult.success()) .orElseThrow(exceptionMapper(Service.class, did)); } diff --git a/extensions/did/did-management-api/src/test/java/org/eclipse/edc/identityhub/api/didmanagement/v1/DidManagementApiControllerTest.java b/extensions/did/did-management-api/src/test/java/org/eclipse/edc/identityhub/api/didmanagement/v1/DidManagementApiControllerTest.java index f33676084..a31115ca8 100644 --- a/extensions/did/did-management-api/src/test/java/org/eclipse/edc/identityhub/api/didmanagement/v1/DidManagementApiControllerTest.java +++ b/extensions/did/did-management-api/src/test/java/org/eclipse/edc/identityhub/api/didmanagement/v1/DidManagementApiControllerTest.java @@ -20,11 +20,15 @@ import org.eclipse.edc.iam.did.spi.document.Service; import org.eclipse.edc.iam.did.spi.document.VerificationMethod; import org.eclipse.edc.identithub.did.spi.DidDocumentService; +import org.eclipse.edc.identithub.did.spi.model.DidResource; +import org.eclipse.edc.identityhub.spi.AuthorizationService; import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.ServiceResult; import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.util.List; @@ -46,349 +50,16 @@ class DidManagementApiControllerTest extends RestControllerTestBase { public static final String TEST_DID = "did:web:host%3A1234:test-did"; private final DidDocumentService didDocumentServiceMock = mock(); + private final AuthorizationService authService = mock(); - - @Test - void publish_success() { - - when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.success()); - - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .post("/publish") - .then() - .log().ifValidationFails() - .statusCode(anyOf(equalTo(200), equalTo(204))); - verify(didDocumentServiceMock).publish(eq(TEST_DID)); - } - - @Test - void publish_whenNotExist_expect404() { - - when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.notFound("test-message")); - - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .post("/publish") - .then() - .log().ifValidationFails() - .statusCode(equalTo(404)); - verify(didDocumentServiceMock).publish(eq(TEST_DID)); - verifyNoMoreInteractions(didDocumentServiceMock); - } - - @Test - void publish_whenAlreadyPublished_expect200() { - - when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.success()); - - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .post("/publish") - .then() - .log().ifValidationFails() - .statusCode(anyOf(equalTo(200), equalTo(204))); - verify(didDocumentServiceMock).publish(eq(TEST_DID)); - verifyNoMoreInteractions(didDocumentServiceMock); - } - - @Test - void unpublish_success() { - - when(didDocumentServiceMock.unpublish(eq(TEST_DID))).thenReturn(ServiceResult.success()); - - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .post("/unpublish") - .then() - .log().ifValidationFails() - .statusCode(anyOf(equalTo(200), equalTo(204))); - verify(didDocumentServiceMock).unpublish(eq(TEST_DID)); - verifyNoMoreInteractions(didDocumentServiceMock); - } - - @Test - void unpublish_whenNotExist_expect404() { - when(didDocumentServiceMock.unpublish(eq(TEST_DID))).thenReturn(ServiceResult.notFound("test-message")); - - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .post("/unpublish") - .then() - .log().ifValidationFails() - .statusCode(404); - verify(didDocumentServiceMock).unpublish(eq(TEST_DID)); - verifyNoMoreInteractions(didDocumentServiceMock); - } - - @Test - void unpublish_whenNotPublished_expect200() { - // not needed - test setup is identical to publish_success - } - - @Test - void unpublish_whenAlreadyUnpublished_expect200() { - // not needed - test setup is identical to publish_success - } - - @Test - void unpublish_whenNotSupported_expect400() { - when(didDocumentServiceMock.unpublish(eq(TEST_DID))).thenReturn(ServiceResult.badRequest("test-message")); - - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .post("/unpublish") - .then() - .log().ifValidationFails() - .statusCode(400); - verify(didDocumentServiceMock).unpublish(eq(TEST_DID)); - verifyNoMoreInteractions(didDocumentServiceMock); - } - - @Test - void query_withSimpleField() { - var resultList = List.of(createDidDocument().build()); - when(didDocumentServiceMock.queryDocuments(any())).thenReturn(ServiceResult.success(resultList)); - var q = QuerySpec.Builder.newInstance().filter(new Criterion("id", "=", "foobar")).build(); - - var docListType = new TypeRef>() { - }; - var docList = baseRequest() - .body(q) - .post("/query") - .then() - .log().ifError() - .statusCode(200) - .extract().body().as(docListType); - - assertThat(docList).isNotEmpty().hasSize(1) - .usingRecursiveFieldByFieldElementComparator() - .isEqualTo(resultList); - verify(didDocumentServiceMock).queryDocuments(eq(q)); - } - - @Test - void query_invalidQuery_expect400() { - when(didDocumentServiceMock.queryDocuments(any())).thenReturn(ServiceResult.badRequest("test-message")); - var q = QuerySpec.Builder.newInstance().build(); - baseRequest() - .body(q) - .post("/query") - .then() - .log().ifValidationFails() - .statusCode(400); - - verify(didDocumentServiceMock).queryDocuments(eq(q)); - } - - @Test - void addEndpoint() { - when(didDocumentServiceMock.addService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.success()); - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .post("/%s/endpoints".formatted(TEST_DID)) - .then() - .log().ifValidationFails() - .statusCode(anyOf(equalTo(200), equalTo(204))); - verify(didDocumentServiceMock).addService(eq(TEST_DID), any(Service.class)); - verifyNoMoreInteractions(didDocumentServiceMock); - } - - @Test - void addEndpoint_withAutoPublish() { - when(didDocumentServiceMock.addService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.success()); - when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.success()); - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .post("/%s/endpoints?autoPublish=true".formatted(TEST_DID)) - .then() - .log().ifValidationFails() - .statusCode(anyOf(equalTo(200), equalTo(204))); - verify(didDocumentServiceMock).addService(eq(TEST_DID), any(Service.class)); - verify(didDocumentServiceMock).publish(eq(TEST_DID)); - verifyNoMoreInteractions(didDocumentServiceMock); - } - - @Test - void addEndpoint_whenAutoPublishFails_expect400() { - when(didDocumentServiceMock.addService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.success()); - when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.badRequest("publisher not working")); - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .post("/%s/endpoints?autoPublish=true".formatted(TEST_DID)) - .then() - .log().ifValidationFails() - .statusCode(400); - verify(didDocumentServiceMock).addService(eq(TEST_DID), any(Service.class)); - verify(didDocumentServiceMock).publish(eq(TEST_DID)); - verifyNoMoreInteractions(didDocumentServiceMock); - } - - @Test - void addEndpoint_alreadyExists() { - when(didDocumentServiceMock.addService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.conflict("exists")); - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .post("/%s/endpoints".formatted(TEST_DID)) - .then() - .log().ifValidationFails() - .statusCode(409); - verify(didDocumentServiceMock).addService(eq(TEST_DID), any(Service.class)); - } - - @Test - void addEndpoint_didNotFound() { - when(didDocumentServiceMock.addService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.notFound("did not found")); - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .post("/%s/endpoints".formatted(TEST_DID)) - .then() - .log().ifValidationFails() - .statusCode(404); - verify(didDocumentServiceMock).addService(eq(TEST_DID), any(Service.class)); - } - - @Test - void replaceEndpoint() { - when(didDocumentServiceMock.replaceService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.success()); - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .patch("/%s/endpoints".formatted(TEST_DID)) - .then() - .log().ifValidationFails() - .statusCode(anyOf(equalTo(200), equalTo(204))); - verify(didDocumentServiceMock).replaceService(eq(TEST_DID), any(Service.class)); - verifyNoMoreInteractions(didDocumentServiceMock); - } - - @Test - void replaceEndpoint_withAutoPublish() { - when(didDocumentServiceMock.replaceService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.success()); - when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.success()); - - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .patch("/%s/endpoints?autoPublish=true".formatted(TEST_DID)) - .then() - .log().ifValidationFails() - .statusCode(anyOf(equalTo(200), equalTo(204))); - verify(didDocumentServiceMock).replaceService(eq(TEST_DID), any(Service.class)); - verify(didDocumentServiceMock).publish(eq(TEST_DID)); - verifyNoMoreInteractions(didDocumentServiceMock); - } - - @Test - void replaceEndpoint_whenAutoPublishFails_expect400() { - when(didDocumentServiceMock.replaceService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.success()); - when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.badRequest("publisher not working")); - - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .patch("/%s/endpoints?autoPublish=true".formatted(TEST_DID)) - .then() - .log().ifValidationFails() - .statusCode(400); - verify(didDocumentServiceMock).replaceService(eq(TEST_DID), any(Service.class)); - verify(didDocumentServiceMock).publish(eq(TEST_DID)); - verifyNoMoreInteractions(didDocumentServiceMock); - } - - @Test - void replaceEndpoint_doesNotExist() { - when(didDocumentServiceMock.replaceService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.badRequest("service not found")); - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .patch("/%s/endpoints".formatted(TEST_DID)) - .then() - .log().ifValidationFails() - .statusCode(400); - verify(didDocumentServiceMock).replaceService(eq(TEST_DID), any(Service.class)); - } - - @Test - void replaceEndpoint_didNotFound() { - when(didDocumentServiceMock.replaceService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.notFound("did not found")); - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .patch("/%s/endpoints".formatted(TEST_DID)) - .then() - .log().ifValidationFails() - .statusCode(404); - verify(didDocumentServiceMock).replaceService(eq(TEST_DID), any(Service.class)); - } - - @Test - void removeEndpoint() { - when(didDocumentServiceMock.removeService(eq(TEST_DID), anyString())).thenReturn(ServiceResult.success()); - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .delete("/%s/endpoints?serviceId=test-service-id".formatted(TEST_DID)) - .then() - .log().ifValidationFails() - .statusCode(anyOf(equalTo(200), equalTo(204))); - verify(didDocumentServiceMock).removeService(eq(TEST_DID), eq("test-service-id")); - verifyNoMoreInteractions(didDocumentServiceMock); - } - - @Test - void removeEndpoint_withAutoPublish() { - when(didDocumentServiceMock.removeService(eq(TEST_DID), anyString())).thenReturn(ServiceResult.success()); - when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.success()); - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .delete("/%s/endpoints?serviceId=test-service-id&autoPublish=true".formatted(TEST_DID)) - .then() - .log().ifValidationFails() - .statusCode(anyOf(equalTo(200), equalTo(204))); - verify(didDocumentServiceMock).removeService(eq(TEST_DID), eq("test-service-id")); - verify(didDocumentServiceMock).publish(eq(TEST_DID)); - - verifyNoMoreInteractions(didDocumentServiceMock); - } - - @Test - void removeEndpoint_whenAutoPublishFails_expect400() { - when(didDocumentServiceMock.removeService(eq(TEST_DID), anyString())).thenReturn(ServiceResult.success()); - when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.badRequest("publisher not reachable")); - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .delete("/%s/endpoints?serviceId=test-service-id&autoPublish=true".formatted(TEST_DID)) - .then() - .log().ifValidationFails() - .statusCode(400); - verify(didDocumentServiceMock).removeService(eq(TEST_DID), eq("test-service-id")); - verify(didDocumentServiceMock).publish(eq(TEST_DID)); - - verifyNoMoreInteractions(didDocumentServiceMock); - } - - @Test - void removeEndpoint_doesNotExist() { - when(didDocumentServiceMock.removeService(eq(TEST_DID), anyString())).thenReturn(ServiceResult.badRequest("service not found")); - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .delete("/%s/endpoints?serviceId=test-service-id".formatted(TEST_DID)) - .then() - .log().ifValidationFails() - .statusCode(400); - verify(didDocumentServiceMock).removeService(eq(TEST_DID), eq("test-service-id")); - } - - @Test - void removeEndpoint_didNotFound() { - when(didDocumentServiceMock.removeService(eq(TEST_DID), anyString())).thenReturn(ServiceResult.notFound("did not found")); - baseRequest() - .body(new DidRequestPayload(TEST_DID)) - .delete("/%s/endpoints?serviceId=test-service-id".formatted(TEST_DID)) - .then() - .log().ifValidationFails() - .statusCode(404); - verify(didDocumentServiceMock).removeService(eq(TEST_DID), eq("test-service-id")); + @BeforeEach + void setUp() { + when(authService.isAuthorized(any(), anyString(), any())).thenReturn(ServiceResult.success()); } @Override protected DidManagementApiController controller() { - return new DidManagementApiController(didDocumentServiceMock); + return new DidManagementApiController(didDocumentServiceMock, authService); } private DidDocument.Builder createDidDocument() { @@ -407,4 +78,450 @@ private RequestSpecification baseRequest() { .baseUri("http://localhost:" + port + "/v1/dids") .when(); } + + @Nested + class RemoveEndpoint { + @Test + void removeEndpoint() { + when(didDocumentServiceMock.removeService(eq(TEST_DID), anyString())).thenReturn(ServiceResult.success()); + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .delete("/%s/endpoints?serviceId=test-service-id".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(anyOf(equalTo(200), equalTo(204))); + verify(didDocumentServiceMock).removeService(eq(TEST_DID), eq("test-service-id")); + verifyNoMoreInteractions(didDocumentServiceMock); + } + + @Test + void removeEndpoint_unauthorized403() { + when(authService.isAuthorized(any(), anyString(), any())).thenReturn(ServiceResult.unauthorized("test message")); + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .delete("/%s/endpoints?serviceId=test-service-id".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(403); + verify(authService).isAuthorized(any(), anyString(), eq(DidResource.class)); + verifyNoMoreInteractions(didDocumentServiceMock, authService); + } + + @Test + void removeEndpoint_withAutoPublish() { + when(didDocumentServiceMock.removeService(eq(TEST_DID), anyString())).thenReturn(ServiceResult.success()); + when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.success()); + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .delete("/%s/endpoints?serviceId=test-service-id&autoPublish=true".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(anyOf(equalTo(200), equalTo(204))); + verify(didDocumentServiceMock).removeService(eq(TEST_DID), eq("test-service-id")); + verify(didDocumentServiceMock).publish(eq(TEST_DID)); + + verifyNoMoreInteractions(didDocumentServiceMock); + } + + @Test + void removeEndpoint_whenAutoPublishFails_expect400() { + when(didDocumentServiceMock.removeService(eq(TEST_DID), anyString())).thenReturn(ServiceResult.success()); + when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.badRequest("publisher not reachable")); + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .delete("/%s/endpoints?serviceId=test-service-id&autoPublish=true".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(400); + verify(didDocumentServiceMock).removeService(eq(TEST_DID), eq("test-service-id")); + verify(didDocumentServiceMock).publish(eq(TEST_DID)); + + verifyNoMoreInteractions(didDocumentServiceMock); + } + + @Test + void removeEndpoint_doesNotExist() { + when(didDocumentServiceMock.removeService(eq(TEST_DID), anyString())).thenReturn(ServiceResult.badRequest("service not found")); + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .delete("/%s/endpoints?serviceId=test-service-id".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(400); + verify(didDocumentServiceMock).removeService(eq(TEST_DID), eq("test-service-id")); + } + + @Test + void removeEndpoint_didNotFound() { + when(didDocumentServiceMock.removeService(eq(TEST_DID), anyString())).thenReturn(ServiceResult.notFound("did not found")); + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .delete("/%s/endpoints?serviceId=test-service-id".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(404); + verify(didDocumentServiceMock).removeService(eq(TEST_DID), eq("test-service-id")); + } + } + + @Nested + class ReplaceEndpoint { + @Test + void replaceEndpoint() { + when(didDocumentServiceMock.replaceService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.success()); + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .patch("/%s/endpoints".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(anyOf(equalTo(200), equalTo(204))); + verify(didDocumentServiceMock).replaceService(eq(TEST_DID), any(Service.class)); + verifyNoMoreInteractions(didDocumentServiceMock); + } + + @Test + void replaceEndpoint_unauthorized403() { + when(authService.isAuthorized(any(), anyString(), any())).thenReturn(ServiceResult.unauthorized("test message")); + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .patch("/%s/endpoints".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(403); + verify(authService).isAuthorized(any(), anyString(), eq(DidResource.class)); + verifyNoMoreInteractions(didDocumentServiceMock, authService); + } + + @Test + void replaceEndpoint_withAutoPublish() { + when(didDocumentServiceMock.replaceService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.success()); + when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.success()); + + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .patch("/%s/endpoints?autoPublish=true".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(anyOf(equalTo(200), equalTo(204))); + verify(didDocumentServiceMock).replaceService(eq(TEST_DID), any(Service.class)); + verify(didDocumentServiceMock).publish(eq(TEST_DID)); + verifyNoMoreInteractions(didDocumentServiceMock); + } + + @Test + void replaceEndpoint_whenAutoPublishFails_expect400() { + when(didDocumentServiceMock.replaceService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.success()); + when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.badRequest("publisher not working")); + + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .patch("/%s/endpoints?autoPublish=true".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(400); + verify(didDocumentServiceMock).replaceService(eq(TEST_DID), any(Service.class)); + verify(didDocumentServiceMock).publish(eq(TEST_DID)); + verifyNoMoreInteractions(didDocumentServiceMock); + } + + @Test + void replaceEndpoint_doesNotExist() { + when(didDocumentServiceMock.replaceService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.badRequest("service not found")); + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .patch("/%s/endpoints".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(400); + verify(didDocumentServiceMock).replaceService(eq(TEST_DID), any(Service.class)); + } + + @Test + void replaceEndpoint_didNotFound() { + when(didDocumentServiceMock.replaceService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.notFound("did not found")); + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .patch("/%s/endpoints".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(404); + verify(didDocumentServiceMock).replaceService(eq(TEST_DID), any(Service.class)); + } + } + + @Nested + class AddEndpoint { + @Test + void addEndpoint() { + when(didDocumentServiceMock.addService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.success()); + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .post("/%s/endpoints".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(anyOf(equalTo(200), equalTo(204))); + verify(didDocumentServiceMock).addService(eq(TEST_DID), any(Service.class)); + verifyNoMoreInteractions(didDocumentServiceMock); + } + + @Test + void addEndpoint_unauthorized403() { + when(authService.isAuthorized(any(), anyString(), any())).thenReturn(ServiceResult.unauthorized("test message")); + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .post("/%s/endpoints".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(403); + verify(authService).isAuthorized(any(), anyString(), eq(DidResource.class)); + verifyNoMoreInteractions(didDocumentServiceMock, authService); + } + + @Test + void addEndpoint_withAutoPublish() { + when(didDocumentServiceMock.addService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.success()); + when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.success()); + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .post("/%s/endpoints?autoPublish=true".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(anyOf(equalTo(200), equalTo(204))); + verify(didDocumentServiceMock).addService(eq(TEST_DID), any(Service.class)); + verify(didDocumentServiceMock).publish(eq(TEST_DID)); + verifyNoMoreInteractions(didDocumentServiceMock); + } + + @Test + void addEndpoint_whenAutoPublishFails_expect400() { + when(didDocumentServiceMock.addService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.success()); + when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.badRequest("publisher not working")); + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .post("/%s/endpoints?autoPublish=true".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(400); + verify(didDocumentServiceMock).addService(eq(TEST_DID), any(Service.class)); + verify(didDocumentServiceMock).publish(eq(TEST_DID)); + verifyNoMoreInteractions(didDocumentServiceMock); + } + + @Test + void addEndpoint_alreadyExists() { + when(didDocumentServiceMock.addService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.conflict("exists")); + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .post("/%s/endpoints".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(409); + verify(didDocumentServiceMock).addService(eq(TEST_DID), any(Service.class)); + } + + @Test + void addEndpoint_didNotFound() { + when(didDocumentServiceMock.addService(eq(TEST_DID), any(Service.class))).thenReturn(ServiceResult.notFound("did not found")); + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .post("/%s/endpoints".formatted(TEST_DID)) + .then() + .log().ifValidationFails() + .statusCode(404); + verify(didDocumentServiceMock).addService(eq(TEST_DID), any(Service.class)); + } + } + + @Nested + class Query { + @Test + void query_withSimpleField() { + var resultList = List.of(createDidDocument().build()); + when(didDocumentServiceMock.queryDocuments(any())).thenReturn(ServiceResult.success(resultList)); + var q = QuerySpec.Builder.newInstance().filter(new Criterion("id", "=", "foobar")).build(); + + var docListType = new TypeRef>() { + }; + var docList = baseRequest() + .body(q) + .post("/query") + .then() + .log().ifError() + .statusCode(200) + .extract().body().as(docListType); + + assertThat(docList).isNotEmpty().hasSize(1) + .usingRecursiveFieldByFieldElementComparator() + .isEqualTo(resultList); + verify(didDocumentServiceMock).queryDocuments(eq(q)); + } + + @Test + void query_invalidQuery_expect400() { + when(didDocumentServiceMock.queryDocuments(any())).thenReturn(ServiceResult.badRequest("test-message")); + var q = QuerySpec.Builder.newInstance().build(); + baseRequest() + .body(q) + .post("/query") + .then() + .log().ifValidationFails() + .statusCode(400); + + verify(didDocumentServiceMock).queryDocuments(eq(q)); + } + + @Test + void query_unauthorized403() { + var resultList = List.of(createDidDocument().build()); + when(didDocumentServiceMock.queryDocuments(any())).thenReturn(ServiceResult.success(resultList)); + when(authService.isAuthorized(any(), anyString(), any())).thenReturn(ServiceResult.unauthorized("test-message")); + var q = QuerySpec.Builder.newInstance().build(); + + var result = baseRequest() + .body(q) + .post("/query") + .then() + .log().ifValidationFails() + .statusCode(200) + .extract().body().as(DidDocument[].class); + + assertThat(result).isEmpty(); + + verify(authService).isAuthorized(any(), anyString(), eq(DidResource.class)); + verify(didDocumentServiceMock).queryDocuments(eq(q)); + verifyNoMoreInteractions(didDocumentServiceMock, authService); + } + } + + @Nested + class Unpublish { + @Test + void unpublish_success() { + + when(didDocumentServiceMock.unpublish(eq(TEST_DID))).thenReturn(ServiceResult.success()); + + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .post("/unpublish") + .then() + .log().ifValidationFails() + .statusCode(anyOf(equalTo(200), equalTo(204))); + verify(didDocumentServiceMock).unpublish(eq(TEST_DID)); + verifyNoMoreInteractions(didDocumentServiceMock); + } + + @Test + void unpublish_unauthorized403() { + when(authService.isAuthorized(any(), anyString(), any())).thenReturn(ServiceResult.unauthorized("test message")); + + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .post("/unpublish") + .then() + .log().ifValidationFails() + .statusCode(403); + + verify(authService).isAuthorized(any(), anyString(), eq(DidResource.class)); + verifyNoMoreInteractions(didDocumentServiceMock, authService); + } + + @Test + void unpublish_whenNotExist_expect404() { + when(didDocumentServiceMock.unpublish(eq(TEST_DID))).thenReturn(ServiceResult.notFound("test-message")); + + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .post("/unpublish") + .then() + .log().ifValidationFails() + .statusCode(404); + verify(didDocumentServiceMock).unpublish(eq(TEST_DID)); + verifyNoMoreInteractions(didDocumentServiceMock); + } + + @Test + void unpublish_whenNotPublished_expect200() { + // not needed - test setup is identical to publish_success + } + + @Test + void unpublish_whenAlreadyUnpublished_expect200() { + // not needed - test setup is identical to publish_success + } + + @Test + void unpublish_whenNotSupported_expect400() { + when(didDocumentServiceMock.unpublish(eq(TEST_DID))).thenReturn(ServiceResult.badRequest("test-message")); + + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .post("/unpublish") + .then() + .log().ifValidationFails() + .statusCode(400); + verify(didDocumentServiceMock).unpublish(eq(TEST_DID)); + verifyNoMoreInteractions(didDocumentServiceMock); + } + } + + @Nested + class Publish { + @Test + void publish_success() { + + when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.success()); + + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .post("/publish") + .then() + .log().ifValidationFails() + .statusCode(anyOf(equalTo(200), equalTo(204))); + verify(didDocumentServiceMock).publish(eq(TEST_DID)); + } + + @Test + void publish_unauthorized403() { + when(authService.isAuthorized(any(), anyString(), any())).thenReturn(ServiceResult.unauthorized("test-msg")); + + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .post("/publish") + .then() + .log().ifValidationFails() + .statusCode(403); + verify(authService).isAuthorized(any(), anyString(), eq(DidResource.class)); + verifyNoMoreInteractions(didDocumentServiceMock, authService); + } + + @Test + void publish_whenNotExist_expect404() { + + when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.notFound("test-message")); + + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .post("/publish") + .then() + .log().ifValidationFails() + .statusCode(equalTo(404)); + verify(didDocumentServiceMock).publish(eq(TEST_DID)); + verifyNoMoreInteractions(didDocumentServiceMock); + } + + @Test + void publish_whenAlreadyPublished_expect200() { + + when(didDocumentServiceMock.publish(eq(TEST_DID))).thenReturn(ServiceResult.success()); + + baseRequest() + .body(new DidRequestPayload(TEST_DID)) + .post("/publish") + .then() + .log().ifValidationFails() + .statusCode(anyOf(equalTo(200), equalTo(204))); + verify(didDocumentServiceMock).publish(eq(TEST_DID)); + verifyNoMoreInteractions(didDocumentServiceMock); + } + } } \ No newline at end of file diff --git a/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/DidDocumentService.java b/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/DidDocumentService.java index d4529318f..f14d03170 100644 --- a/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/DidDocumentService.java +++ b/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/DidDocumentService.java @@ -32,7 +32,7 @@ public interface DidDocumentService { * * @return a {@link ServiceResult} to indicate success or failure. */ - ServiceResult store(DidDocument document); + ServiceResult store(DidDocument document, String participantId); /** * Deletes a DID document if found. * * @param did The ID of the DID document to delete. diff --git a/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidResource.java b/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidResource.java index 90b62d731..770c1257d 100644 --- a/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidResource.java +++ b/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidResource.java @@ -16,7 +16,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import org.eclipse.edc.iam.did.spi.document.DidDocument; -import org.eclipse.edc.identityhub.spi.store.model.ParticipantResource; +import org.eclipse.edc.identityhub.spi.model.ParticipantResource; import java.time.Clock; import java.util.Objects; diff --git a/spi/identity-hub-spi/build.gradle.kts b/spi/identity-hub-spi/build.gradle.kts index a9663e4ae..4fd5dcbce 100644 --- a/spi/identity-hub-spi/build.gradle.kts +++ b/spi/identity-hub-spi/build.gradle.kts @@ -23,6 +23,7 @@ val swagger: String by project dependencies { api(libs.edc.spi.identitytrust) + api(libs.edc.spi.web) implementation(libs.jackson.databind) implementation(libs.nimbus.jwt) implementation(libs.edc.spi.identity.did) diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/AuthorizationResultHandler.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/AuthorizationResultHandler.java new file mode 100644 index 000000000..cdc3743d1 --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/AuthorizationResultHandler.java @@ -0,0 +1,34 @@ +/* + * 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.spi; + +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.result.ServiceFailure; +import org.eclipse.edc.web.spi.exception.NotAuthorizedException; +import org.eclipse.edc.web.spi.exception.ServiceResultHandler; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Function; + +public class AuthorizationResultHandler { + public static Function exceptionMapper(@NotNull Class clazz, String id) { + return failure -> { + if (failure.getReason() == ServiceFailure.Reason.UNAUTHORIZED) { + return new NotAuthorizedException(failure.getFailureDetail()); + } + return ServiceResultHandler.exceptionMapper(clazz, id).apply(failure); + }; + } +} diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/AuthorizationService.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/AuthorizationService.java new file mode 100644 index 000000000..954e479d2 --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/AuthorizationService.java @@ -0,0 +1,28 @@ +/* + * 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.spi; + +import org.eclipse.edc.identityhub.spi.model.ParticipantResource; +import org.eclipse.edc.spi.result.ServiceResult; + +import java.security.Principal; +import java.util.Map; +import java.util.function.Function; + +public interface AuthorizationService { + ServiceResult isAuthorized(Principal user, String resourceId, Class resourceClass); + + Map, Function> getAuthorizationCheckFunctions(); +} diff --git a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/ParticipantResource.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/ParticipantResource.java similarity index 96% rename from spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/ParticipantResource.java rename to spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/ParticipantResource.java index dd455a685..2a9271fa1 100644 --- a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/ParticipantResource.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/ParticipantResource.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.identityhub.spi.store.model; +package org.eclipse.edc.identityhub.spi.model; import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantContext.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantContext.java index bc9d114f4..fb17e040a 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantContext.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantContext.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import org.eclipse.edc.identityhub.spi.model.ParticipantResource; import java.time.Instant; import java.util.ArrayList; @@ -28,9 +29,8 @@ * Representation of a participant in Identity Hub. */ @JsonDeserialize(builder = ParticipantContext.Builder.class) -public class ParticipantContext { +public class ParticipantContext extends ParticipantResource { private List roles = new ArrayList<>(); - private String participantId; private String did; private long createdAt; private long lastModified; @@ -40,13 +40,6 @@ public class ParticipantContext { private ParticipantContext() { } - /** - * Participant IDs must be stable and globally unique (i.e. per dataspace). They will be visible in contracts, negotiations, etc. - */ - public String getParticipantId() { - return participantId; - } - /** * The POSIX timestamp in ms when this entry was created. Immutable */ @@ -111,57 +104,61 @@ public List getRoles() { } @JsonPOJOBuilder(withPrefix = "") - public static final class Builder { - private final ParticipantContext participantContext; + public static final class Builder extends ParticipantResource.Builder { private Builder() { - participantContext = new ParticipantContext(); - participantContext.createdAt = Instant.now().toEpochMilli(); + super(new ParticipantContext()); + entity.createdAt = Instant.now().toEpochMilli(); } public Builder createdAt(long createdAt) { - this.participantContext.createdAt = createdAt; + this.entity.createdAt = createdAt; return this; } public Builder lastModified(long lastModified) { - this.participantContext.lastModified = lastModified; + this.entity.lastModified = lastModified; + return this; + } + + @Override + public Builder self() { return this; } public Builder participantId(String participantId) { - this.participantContext.participantId = participantId; + this.entity.participantId = participantId; return this; } public Builder state(ParticipantContextState state) { - this.participantContext.state = state.ordinal(); + this.entity.state = state.ordinal(); return this; } public Builder roles(List roles) { - this.participantContext.roles = roles; + this.entity.roles = roles; return this; } public Builder apiTokenAlias(String apiToken) { - this.participantContext.apiTokenAlias = apiToken; + this.entity.apiTokenAlias = apiToken; return this; } public Builder did(String did) { - this.participantContext.did = did; + this.entity.did = did; return this; } public ParticipantContext build() { - Objects.requireNonNull(participantContext.participantId, "Participant ID cannot be null"); - Objects.requireNonNull(participantContext.apiTokenAlias, "API Token Alias cannot be null"); + Objects.requireNonNull(entity.participantId, "Participant ID cannot be null"); + Objects.requireNonNull(entity.apiTokenAlias, "API Token Alias cannot be null"); - if (participantContext.getLastModified() == 0L) { - participantContext.lastModified = participantContext.getCreatedAt(); + if (entity.getLastModified() == 0L) { + entity.lastModified = entity.getCreatedAt(); } - return participantContext; + return super.build(); } @JsonCreator diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantManifest.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantManifest.java index 3019164d6..6c9583d78 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantManifest.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/participant/ParticipantManifest.java @@ -19,7 +19,9 @@ import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import org.eclipse.edc.iam.did.spi.document.Service; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; /** @@ -27,6 +29,7 @@ */ @JsonDeserialize(builder = ParticipantManifest.Builder.class) public class ParticipantManifest { + private List roles = new ArrayList<>(); private Set serviceEndpoints = new HashSet<>(); private boolean isActive; private String participantId; @@ -73,6 +76,10 @@ public String getDid() { return did; } + public List getRoles() { + return roles; + } + @JsonPOJOBuilder(withPrefix = "") public static final class Builder { @@ -107,6 +114,11 @@ public Builder key(KeyDescriptor key) { return this; } + public Builder roles(List roles) { + manifest.roles = roles; + return this; + } + public Builder did(String did) { manifest.did = did; return this; diff --git a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/IdentityResource.java b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/IdentityResource.java index b0f223120..d2a70a944 100644 --- a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/IdentityResource.java +++ b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/IdentityResource.java @@ -15,6 +15,7 @@ package org.eclipse.edc.identityhub.spi.store.model; import com.fasterxml.jackson.annotation.JsonIgnore; +import org.eclipse.edc.identityhub.spi.model.ParticipantResource; import java.time.Clock; import java.util.Objects; diff --git a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/KeyPairResource.java b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/KeyPairResource.java index d9d2a1ec1..9b34bd282 100644 --- a/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/KeyPairResource.java +++ b/spi/identity-hub-store-spi/src/main/java/org/eclipse/edc/identityhub/spi/store/model/KeyPairResource.java @@ -15,6 +15,7 @@ package org.eclipse.edc.identityhub.spi.store.model; import com.fasterxml.jackson.annotation.JsonIgnore; +import org.eclipse.edc.identityhub.spi.model.ParticipantResource; import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; import org.eclipse.edc.spi.security.KeyParserRegistry; import org.eclipse.edc.spi.security.Vault; From 00db93d512caf818030b7c9fdffcc2d727170a3f Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Fri, 26 Jan 2024 15:23:46 +0100 Subject: [PATCH 2/4] update signature of AuthorizationService --- .../AuthorizationServiceImpl.java | 9 +++++---- ...ticipantContextManagementApiExtension.java | 2 +- .../VerifiableCredentialApiExtension.java | 2 +- .../DidManagementApiExtension.java | 2 +- .../spi/AuthorizationResultHandler.java | 4 ++++ .../identityhub/spi/AuthorizationService.java | 20 +++++++++++++++++-- 6 files changed, 30 insertions(+), 9 deletions(-) diff --git a/extensions/api/identityhub-api-auth/src/main/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImpl.java b/extensions/api/identityhub-api-auth/src/main/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImpl.java index 8e8e00ae4..496c8ed18 100644 --- a/extensions/api/identityhub-api-auth/src/main/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImpl.java +++ b/extensions/api/identityhub-api-auth/src/main/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImpl.java @@ -29,10 +29,6 @@ public class AuthorizationServiceImpl implements AuthorizationService { private final Map, Function> authorizationCheckFunctions = new HashMap<>(); - public Map, Function> getAuthorizationCheckFunctions() { - return authorizationCheckFunctions; - } - @Override public ServiceResult isAuthorized(Principal user, String resourceId, Class resourceClass) { var function = authorizationCheckFunctions.get(resourceClass); @@ -47,4 +43,9 @@ public ServiceResult isAuthorized(Principal user, String resourceId, Class return isAuthorized ? ServiceResult.success() : ServiceResult.unauthorized("User '%s' is not authorized to access resource of type %s with ID '%s'.".formatted(user.getName(), resourceClass, resourceId)); } + + @Override + public void addLoookupFunction(Class resourceClass, Function lookupFunction) { + authorizationCheckFunctions.put(resourceClass, lookupFunction); + } } diff --git a/extensions/api/participant-context-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/ParticipantContextManagementApiExtension.java b/extensions/api/participant-context-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/ParticipantContextManagementApiExtension.java index 92ba36023..9f9ea64c7 100644 --- a/extensions/api/participant-context-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/ParticipantContextManagementApiExtension.java +++ b/extensions/api/participant-context-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/participantcontext/ParticipantContextManagementApiExtension.java @@ -49,7 +49,7 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { - authorizationService.getAuthorizationCheckFunctions().put(ParticipantContext.class, s -> participantContextService.getParticipantContext(s).orElseThrow(exceptionMapper(ParticipantContext.class, s))); + authorizationService.addLoookupFunction(ParticipantContext.class, s -> participantContextService.getParticipantContext(s).orElseThrow(exceptionMapper(ParticipantContext.class, s))); var controller = new ParticipantContextApiController(new ParticipantManifestValidator(), participantContextService, authorizationService); webService.registerResource(webServiceConfiguration.getContextAlias(), controller); } diff --git a/extensions/api/verifiable-credential-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/VerifiableCredentialApiExtension.java b/extensions/api/verifiable-credential-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/VerifiableCredentialApiExtension.java index bbfb90821..a8d9a4965 100644 --- a/extensions/api/verifiable-credential-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/VerifiableCredentialApiExtension.java +++ b/extensions/api/verifiable-credential-mgmt-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/VerifiableCredentialApiExtension.java @@ -46,7 +46,7 @@ public class VerifiableCredentialApiExtension implements ServiceExtension { @Override public void initialize(ServiceExtensionContext context) { - authorizationService.getAuthorizationCheckFunctions().put(VerifiableCredentialResource.class, this::queryById); + authorizationService.addLoookupFunction(VerifiableCredentialResource.class, this::queryById); var controller = new VerifiableCredentialsApiController(credentialStore, authorizationService); webService.registerResource(apiConfiguration.getContextAlias(), controller); } diff --git a/extensions/did/did-management-api/src/main/java/org/eclipse/edc/identityhub/api/didmanagement/DidManagementApiExtension.java b/extensions/did/did-management-api/src/main/java/org/eclipse/edc/identityhub/api/didmanagement/DidManagementApiExtension.java index 1496834e2..873e917ee 100644 --- a/extensions/did/did-management-api/src/main/java/org/eclipse/edc/identityhub/api/didmanagement/DidManagementApiExtension.java +++ b/extensions/did/did-management-api/src/main/java/org/eclipse/edc/identityhub/api/didmanagement/DidManagementApiExtension.java @@ -48,7 +48,7 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { - authorizationService.getAuthorizationCheckFunctions().put(DidResource.class, s -> didDocumentService.findById(s)); + authorizationService.addLoookupFunction(DidResource.class, s -> didDocumentService.findById(s)); var controller = new DidManagementApiController(didDocumentService, authorizationService); webService.registerResource(webServiceConfiguration.getContextAlias(), controller); diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/AuthorizationResultHandler.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/AuthorizationResultHandler.java index cdc3743d1..5456ae998 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/AuthorizationResultHandler.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/AuthorizationResultHandler.java @@ -22,6 +22,10 @@ import java.util.function.Function; +/** + * Extension of the {@link ServiceResultHandler} which also can handle {@link ServiceFailure.Reason#UNAUTHORIZED} failures. All other + * failures are delegated back to the {@link ServiceResultHandler}. + */ public class AuthorizationResultHandler { public static Function exceptionMapper(@NotNull Class clazz, String id) { return failure -> { diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/AuthorizationService.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/AuthorizationService.java index 954e479d2..8cc1c2d62 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/AuthorizationService.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/AuthorizationService.java @@ -14,15 +14,31 @@ package org.eclipse.edc.identityhub.spi; +import jakarta.ws.rs.core.SecurityContext; import org.eclipse.edc.identityhub.spi.model.ParticipantResource; import org.eclipse.edc.spi.result.ServiceResult; import java.security.Principal; -import java.util.Map; import java.util.function.Function; +/** + * This service takes a {@link Principal}, that is typically obtained from the {@link jakarta.ws.rs.core.SecurityContext} of an incoming + * HTTP request, and checks whether this principal is authorized to access a particular resource, identified by ID and by object class. + */ public interface AuthorizationService { + /** + * Checks whether the principal is authorized to access a particular resource. + * + * @param user The {@link Principal}, typically obtained via {@link SecurityContext#getUserPrincipal()}. + * @param resourceId The database ID of the resource. The resource must be of type {@link ParticipantResource}. + * @param resourceClass The concrete type of the resource. + * @return success if authorized, {@link ServiceResult#unauthorized(String)} if not authorized + */ ServiceResult isAuthorized(Principal user, String resourceId, Class resourceClass); - Map, Function> getAuthorizationCheckFunctions(); + /** + * Register a function, that can lookup a particular resource type by ID. Typically, every resource that should be protected with + * authorization, registers a lookup function for the type of resource. + */ + void addLoookupFunction(Class resourceClass, Function checkFunction); } From 0ff36357d1a3bd5b8638fc25431f1993d5e98177 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Sat, 27 Jan 2024 16:50:39 +0100 Subject: [PATCH 3/4] added tests --- .../api/identityhub-api-auth/build.gradle.kts | 1 + .../AuthorizationServiceImpl.java | 2 +- .../AuthorizationServiceImplTest.java | 52 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 extensions/api/identityhub-api-auth/src/test/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImplTest.java diff --git a/extensions/api/identityhub-api-auth/build.gradle.kts b/extensions/api/identityhub-api-auth/build.gradle.kts index ae5969059..69ce5953d 100644 --- a/extensions/api/identityhub-api-auth/build.gradle.kts +++ b/extensions/api/identityhub-api-auth/build.gradle.kts @@ -28,5 +28,6 @@ dependencies { implementation(libs.edc.core.jerseyproviders) implementation(libs.jakarta.rsApi) + testImplementation(libs.edc.junit) testRuntimeOnly(libs.jersey.common) // needs the RuntimeDelegate } diff --git a/extensions/api/identityhub-api-auth/src/main/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImpl.java b/extensions/api/identityhub-api-auth/src/main/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImpl.java index 496c8ed18..0953a5988 100644 --- a/extensions/api/identityhub-api-auth/src/main/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImpl.java +++ b/extensions/api/identityhub-api-auth/src/main/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImpl.java @@ -33,7 +33,7 @@ public class AuthorizationServiceImpl implements AuthorizationService { public ServiceResult isAuthorized(Principal user, String resourceId, Class resourceClass) { var function = authorizationCheckFunctions.get(resourceClass); if (function == null) { - return ServiceResult.success(); + return ServiceResult.unauthorized("User access for '%s' to resource ID '%s' of type '%s' cannot be verified".formatted(user.getName(), resourceClass, resourceClass)); } var isAuthorized = ofNullable(function.apply(resourceId)) diff --git a/extensions/api/identityhub-api-auth/src/test/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImplTest.java b/extensions/api/identityhub-api-auth/src/test/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImplTest.java new file mode 100644 index 000000000..cbd06477c --- /dev/null +++ b/extensions/api/identityhub-api-auth/src/test/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImplTest.java @@ -0,0 +1,52 @@ +package org.eclipse.edc.identityhub.api.authorization; + +import org.eclipse.edc.identityhub.spi.model.ParticipantResource; +import org.junit.jupiter.api.Test; + +import java.security.Principal; + +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AuthorizationServiceImplTest { + + private final AuthorizationServiceImpl authorizationService = new AuthorizationServiceImpl(); + + @Test + void isAuthorized_whenAuthorized() { + authorizationService.addLoookupFunction(String.class, s -> new ParticipantResource() { + @Override + public String getParticipantId() { + return "test-id"; + } + }); + + var principal = mock(Principal.class); + when(principal.getName()).thenReturn("test-id"); + assertThat(authorizationService.isAuthorized(principal, "test-resource-id", String.class)) + .isSucceeded(); + } + + @Test + void isAuthorized_whenNoLookupFunction(){ + var principal = mock(Principal.class); + when(principal.getName()).thenReturn("test-id"); + assertThat(authorizationService.isAuthorized(principal, "test-resource-id", String.class)) + .isFailed(); + } + + @Test + void isAuthorized_whenNotAuthorized(){ + authorizationService.addLoookupFunction(String.class, s -> new ParticipantResource() { + @Override + public String getParticipantId() { + return "another-test-id"; + } + }); + var principal = mock(Principal.class); + when(principal.getName()).thenReturn("test-id"); + assertThat(authorizationService.isAuthorized(principal, "test-resource-id", String.class)) + .isFailed(); + } +} \ No newline at end of file From 83a6253c7beebf8e775f2a31d19e423a8d31f831 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Sat, 27 Jan 2024 16:56:07 +0100 Subject: [PATCH 4/4] checkstyle --- .../AuthorizationServiceImplTest.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/extensions/api/identityhub-api-auth/src/test/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImplTest.java b/extensions/api/identityhub-api-auth/src/test/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImplTest.java index cbd06477c..ed4985851 100644 --- a/extensions/api/identityhub-api-auth/src/test/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImplTest.java +++ b/extensions/api/identityhub-api-auth/src/test/java/org/eclipse/edc/identityhub/api/authorization/AuthorizationServiceImplTest.java @@ -1,3 +1,17 @@ +/* + * 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.authorization; import org.eclipse.edc.identityhub.spi.model.ParticipantResource; @@ -29,7 +43,7 @@ public String getParticipantId() { } @Test - void isAuthorized_whenNoLookupFunction(){ + void isAuthorized_whenNoLookupFunction() { var principal = mock(Principal.class); when(principal.getName()).thenReturn("test-id"); assertThat(authorizationService.isAuthorized(principal, "test-resource-id", String.class)) @@ -37,7 +51,7 @@ void isAuthorized_whenNoLookupFunction(){ } @Test - void isAuthorized_whenNotAuthorized(){ + void isAuthorized_whenNotAuthorized() { authorizationService.addLoookupFunction(String.class, s -> new ParticipantResource() { @Override public String getParticipantId() {