From 5b4321d71c37cc514ddc59f4a06da47f9d85a798 Mon Sep 17 00:00:00 2001 From: Sebastian Becker Date: Wed, 13 Nov 2024 12:16:38 +0100 Subject: [PATCH 1/8] feat(amphora-service): add opa client and service Signed-off-by: Sebastian Becker --- .../amphora/service/config/OpaConfig.java | 31 +++ .../amphora/service/config/OpaProperties.java | 22 +++ .../service/exceptions/CsOpaException.java | 14 ++ .../amphora/service/opa/OpaClient.java | 64 ++++++ .../amphora/service/opa/OpaClientRequest.java | 45 +++++ .../amphora/service/opa/OpaRequest.java | 20 ++ .../amphora/service/opa/OpaRequestBody.java | 34 ++++ .../amphora/service/opa/OpaResult.java | 19 ++ .../amphora/service/opa/OpaService.java | 132 +++++++++++++ .../amphora/service/opa/OpaClientTest.java | 168 ++++++++++++++++ .../amphora/service/opa/OpaServiceTest.java | 184 ++++++++++++++++++ 11 files changed, 733 insertions(+) create mode 100644 amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaConfig.java create mode 100644 amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaProperties.java create mode 100644 amphora-service/src/main/java/io/carbynestack/amphora/service/exceptions/CsOpaException.java create mode 100644 amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClient.java create mode 100644 amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClientRequest.java create mode 100644 amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequest.java create mode 100644 amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequestBody.java create mode 100644 amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaResult.java create mode 100644 amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaService.java create mode 100644 amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaClientTest.java create mode 100644 amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaServiceTest.java diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaConfig.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaConfig.java new file mode 100644 index 0000000..1fb21c2 --- /dev/null +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaConfig.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 - for information on the respective copyright owner + * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.carbynestack.amphora.service.config; + +import io.carbynestack.amphora.service.opa.OpaClient; +import io.carbynestack.castor.client.download.CastorIntraVcpClient; +import io.carbynestack.castor.client.download.DefaultCastorIntraVcpClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +import java.io.File; +import java.net.URI; + +@Configuration +public class OpaConfig { + + @Bean + OpaClient opaClient(OpaProperties opaProperties) { + return OpaClient.builder() + .opaServiceUri(URI.create(opaProperties.getEndpoint())) + .defaultPolicyPackage(opaProperties.getDefaultPolicyPackage()) + .build(); + } +} diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaProperties.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaProperties.java new file mode 100644 index 0000000..2a50e85 --- /dev/null +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaProperties.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 - for information on the respective copyright owner + * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package io.carbynestack.amphora.service.config; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@ConfigurationProperties(prefix = "carbynestack.amphora.opa") +@Component +@Data +@Accessors(chain = true) +public class OpaProperties { + + private String endpoint; + private String defaultPolicyPackage; +} diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/exceptions/CsOpaException.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/exceptions/CsOpaException.java new file mode 100644 index 0000000..1513f40 --- /dev/null +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/exceptions/CsOpaException.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2024 - for information on the respective copyright owner + * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.carbynestack.amphora.service.exceptions; + +public class CsOpaException extends Exception { + public CsOpaException(String message) { + super(message); + } +} diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClient.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClient.java new file mode 100644 index 0000000..54aacb6 --- /dev/null +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClient.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 - for information on the respective copyright owner + * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.carbynestack.amphora.service.opa; + +import io.carbynestack.amphora.common.Tag; +import io.carbynestack.httpclient.CsHttpClient; +import io.carbynestack.httpclient.CsHttpClientException; +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; + +import java.net.URI; +import java.util.List; + +@Slf4j +public class OpaClient { + private final CsHttpClient csHttpClient; + private final URI opaServiceUri; + private final String defaultPolicyPackage; + + @Builder + public OpaClient(URI opaServiceUri, String defaultPolicyPackage) { + this(CsHttpClient.createDefault(), opaServiceUri, defaultPolicyPackage); + } + + OpaClient(CsHttpClient httpClient, URI opaServiceUri, String defaultPolicyPackage) { + this.csHttpClient = httpClient; + this.opaServiceUri = opaServiceUri; + this.defaultPolicyPackage = defaultPolicyPackage; + } + + /** + * Evaluate the OPA policy package with the given action, subject and tags. + * + * @param policyPackage The OPA policy package to evaluate. + * @param action The action to evaluate. + * @param subject The subject attempting to perform the action. + * @param tags The tags describing the accessed object. + * @return True if the subject can perform the action, false otherwise (or if an error occurred). + */ + boolean isAllowed(String policyPackage, String action, String subject, List tags) { + OpaRequestBody body = OpaRequestBody.builder() + .subject(subject) + .tags(tags) + .build(); + try { + return csHttpClient.postForObject( + opaServiceUri.resolve(String.format("/v1/data/%s/%s", policyPackage, action)), + new OpaRequest(body), + OpaResult.class).isAllowed(); + } catch (CsHttpClientException e) { + log.error("Error occurred while evaluating OPA policy package: {}", e.getMessage()); + } + return false; + } + + public OpaClientRequest newRequest() { + return new OpaClientRequest(this, defaultPolicyPackage); + } +} \ No newline at end of file diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClientRequest.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClientRequest.java new file mode 100644 index 0000000..a2f2c98 --- /dev/null +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClientRequest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 - for information on the respective copyright owner + * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.carbynestack.amphora.service.opa; + +import io.carbynestack.amphora.common.Tag; +import io.carbynestack.amphora.service.exceptions.CsOpaException; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.apache.logging.log4j.util.Strings; + +import java.util.List; +import java.util.Objects; + +@Setter() +@Accessors(chain = true, fluent = true) +public class OpaClientRequest { + private String withPolicyPackage; + private String withSubject; + private List withTags; + private String withAction; + + private final OpaClient opaClient; + + public OpaClientRequest(OpaClient opaClient, String defaultPolicyPackage) { + this.withPolicyPackage = defaultPolicyPackage; + this.opaClient = opaClient; + } + + public boolean evaluate() throws CsOpaException { + if(Strings.isEmpty(withSubject)) { + throw new CsOpaException("Subject is required to evaluate the policy"); + } + if(Strings.isEmpty(withAction)) { + throw new CsOpaException("Action is required to evaluate the policy"); + } + withTags.removeIf(Objects::isNull); + return opaClient.isAllowed(withPolicyPackage, withAction, withSubject, withTags); + } +} + diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequest.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequest.java new file mode 100644 index 0000000..6f276b5 --- /dev/null +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequest.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 - for information on the respective copyright owner + * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.carbynestack.amphora.service.opa; + +import lombok.Getter; + +@Getter +class OpaRequest { + OpaRequestBody input; + + public OpaRequest(OpaRequestBody input) { + this.input = input; + } + +} diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequestBody.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequestBody.java new file mode 100644 index 0000000..9992d96 --- /dev/null +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequestBody.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 - for information on the respective copyright owner + * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.carbynestack.amphora.service.opa; + +import io.carbynestack.amphora.common.Tag; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Value +public class OpaRequestBody { + String subject; + long currentTime; + List tags; + + @Builder + public OpaRequestBody(String subject, List tags) { + this.subject = subject; + this.currentTime = System.currentTimeMillis(); + this.tags = tags; + } + + OpaRequestBody(String subject, long currentTime, List tags) { + this.subject = subject; + this.currentTime = currentTime; + this.tags = tags; + } +} diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaResult.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaResult.java new file mode 100644 index 0000000..da0d2e0 --- /dev/null +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaResult.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024 - for information on the respective copyright owner + * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.carbynestack.amphora.service.opa; + +import lombok.Setter; + +public class OpaResult { + @Setter + private boolean result; + + boolean isAllowed() { + return result; + } +} diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaService.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaService.java new file mode 100644 index 0000000..d154726 --- /dev/null +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaService.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2024 - for information on the respective copyright owner + * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.carbynestack.amphora.service.opa; + +import io.carbynestack.amphora.common.Tag; +import io.carbynestack.amphora.service.exceptions.CsOpaException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class OpaService { + public static final String POLICY_PACKAGE_TAG_KEY = "accessPolicy"; + static final String READ_SECRET_ACTION_NAME = "read"; + static final String DELETE_SECRET_ACTION_NAME = "delete"; + static final String CREATE_TAG_ACTION_NAME = "tag/create"; + static final String READ_TAG_ACTION_NAME = "tag/read"; + static final String UPDATE_TAG_ACTION_NAME = "tag/update"; + static final String DELETE_TAG_ACTION_NAME = "tag/delete"; + + private final OpaClient opaClient; + + @Autowired + public OpaService(OpaClient opaClient) { + this.opaClient = opaClient; + } + + /** + * Check if the subject can read the secret described by the given tags evaluating the OPA policy package. + * The policy package is extracted from the tags if present. If not present, the default policy package is used. + * + * @param subject The subject attempting to read the secret. + * @param tags The tags describing the referenced secret. + * @return True if the subject can read the secret, false otherwise. + * @throws CsOpaException If an error occurred while evaluating the policy. + */ + public boolean canReadSecret(String subject, List tags) throws CsOpaException { + return isAllowed(subject, READ_SECRET_ACTION_NAME, tags); + } + + /** + * Check if the subject can delete the secret described by the given tags evaluating the OPA policy package. + * The policy package is extracted from the tags if present. If not present, the default policy package is used. + * + * @param subject The subject attempting to delete the secret. + * @param tags The tags describing the referenced secret. + * @return True if the subject can delete the secret, false otherwise. + * @throws CsOpaException If an error occurred while evaluating the policy. + */ + public boolean canDeleteSecret(String subject, List tags) throws CsOpaException { + return isAllowed(subject, DELETE_SECRET_ACTION_NAME, tags); + } + + /** + * Check if the subject can create the given tags evaluating the OPA policy package. + * The policy package is extracted from the tags if present. If not present, the default policy package is used. + * + * @param subject The subject attempting to create the tags. + * @param tags The tags to create. + * @return True if the subject can create the tags, false otherwise. + * @throws CsOpaException If an error occurred while evaluating the policy. + */ + public boolean canCreateTags(String subject, List tags) throws CsOpaException { + return isAllowed(subject, CREATE_TAG_ACTION_NAME, tags); + } + + /** + * Check if the subject can read the given tags evaluating the OPA policy package. + * The policy package is extracted from the tags if present. If not present, the default policy package is used. + * + * @param subject The subject attempting to read the tags. + * @param tags The tags to read. + * @return True if the subject can read the tags, false otherwise. + * @throws CsOpaException If an error occurred while evaluating the policy. + */ + public boolean canReadTags(String subject, List tags) throws CsOpaException { + return isAllowed(subject, READ_TAG_ACTION_NAME, tags); + } + + /** + * Check if the subject can update the given tags evaluating the OPA policy package. + * The policy package is extracted from the tags if present. If not present, the default policy package is used. + * + * @param subject The subject attempting to update the tags. + * @param tags The tags to update. + * @return True if the subject can update the tags, false otherwise. + * @throws CsOpaException If an error occurred while evaluating the policy. + */ + public boolean canUpdateTags(String subject, List tags) throws CsOpaException { + return isAllowed(subject, UPDATE_TAG_ACTION_NAME, tags); + } + + /** + * Check if the subject can delete the given tags evaluating the OPA policy package. + * The policy package is extracted from the tags if present. If not present, the default policy package is used. + * + * @param subject The subject attempting to delete the tags. + * @param tags The tags to delete. + * @return True if the subject can delete the tags, false otherwise. + * @throws CsOpaException If an error occurred while evaluating the policy. + */ + public boolean canDeleteTags(String subject, List tags) throws CsOpaException { + return isAllowed(subject, DELETE_TAG_ACTION_NAME, tags); + } + + /** + * Check if the subject can perform the given action evaluating the OPA policy package. + * The policy package is extracted from the tags if present. If not present, the default policy package is used. + * + * @param action The action to evaluate. + * @param subject The subject attempting to perform the action. + * @param tags The tags describing the accessed object. + * @return True if the subject can perform the action, false otherwise. + * @throws CsOpaException If an error occurred while evaluating the policy. + */ + boolean isAllowed(String subject, String action, List tags) throws CsOpaException { + OpaClientRequest request = opaClient.newRequest() + .withSubject(subject) + .withAction(action) + .withTags(tags); + tags.stream().filter(tag -> tag.getKey().equals(POLICY_PACKAGE_TAG_KEY)) + .findFirst() + .ifPresent(tag -> request.withPolicyPackage(tag.getValue())); + return request.evaluate(); + } +} diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaClientTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaClientTest.java new file mode 100644 index 0000000..e6b2d19 --- /dev/null +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaClientTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2024 - for information on the respective copyright owner + * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.carbynestack.amphora.service.opa; + +import com.google.common.collect.Lists; +import io.carbynestack.amphora.common.Tag; +import io.carbynestack.amphora.service.exceptions.CsOpaException; +import io.carbynestack.httpclient.CsHttpClient; +import io.carbynestack.httpclient.CsHttpClientException; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.net.URI; +import java.util.List; + +import static io.carbynestack.amphora.service.opa.OpaService.READ_SECRET_ACTION_NAME; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class OpaClientTest { + + private static final URI OPA_SERVICE_URI = URI.create("http://localhost:8081"); + private static final String POLICY_PACKAGE = "play"; + private static final String DEFAULT_POLICY_PACKAGE = "default"; + private static final String SUBJECT = "me"; + private static final List TAGS = Lists.newArrayList( + Tag.builder().key("created").value("yesterday").build(), + Tag.builder().key("owner").value("me").build() + ); + private static final OpaResult POSITIVE_RESULT; + static { + POSITIVE_RESULT = new OpaResult(); + POSITIVE_RESULT.setResult(true); + } + private static final OpaResult NEGATIVE_RESULT = new OpaResult(); + + @Mock + private CsHttpClient csHttpClientMock = mock(CsHttpClient.class); + + @Before + public void setUp() { + reset(csHttpClientMock); + } + + @Test + public void givenValidRequest_whenEvaluate_thenReturnTrue() throws CsOpaException, CsHttpClientException { + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(OpaRequest.class); + when(csHttpClientMock.postForObject(uriCaptor.capture(), requestCaptor.capture(), eq(OpaResult.class))) + .thenReturn(NEGATIVE_RESULT); + + OpaClient opaClient = new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); + opaClient.newRequest() + .withPolicyPackage(POLICY_PACKAGE) + .withAction(READ_SECRET_ACTION_NAME) + .withSubject(SUBJECT) + .withTags(TAGS) + .evaluate(); + + URI actualUri = uriCaptor.getValue(); + MatcherAssert.assertThat(actualUri.toString(), + Matchers.startsWith(OPA_SERVICE_URI.toString())); + MatcherAssert.assertThat( + actualUri.toString(), + Matchers.endsWith(String.format("/v1/data/%s/%s", POLICY_PACKAGE, READ_SECRET_ACTION_NAME))); + OpaRequestBody actualRequestBody = requestCaptor.getValue().getInput(); + assertEquals(SUBJECT, actualRequestBody.getSubject()); + assertEquals(TAGS, actualRequestBody.getTags()); + } + + @Test + public void givenNoPolicyPackageDefined_whenEvaluate_thenReturnUseDefaultPackage() throws CsOpaException, CsHttpClientException { + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(OpaRequest.class); + when(csHttpClientMock.postForObject(uriCaptor.capture(), requestCaptor.capture(), eq(OpaResult.class))) + .thenReturn(NEGATIVE_RESULT); + + OpaClient opaClient = new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); + opaClient.newRequest() + .withAction(READ_SECRET_ACTION_NAME) + .withSubject(SUBJECT) + .withTags(TAGS) + .evaluate(); + + URI actualUri = uriCaptor.getValue(); + MatcherAssert.assertThat(actualUri.toString(), + Matchers.startsWith(OPA_SERVICE_URI.toString())); + MatcherAssert.assertThat( + actualUri.toString(), + Matchers.endsWith(String.format("/v1/data/%s/%s", DEFAULT_POLICY_PACKAGE, READ_SECRET_ACTION_NAME))); + OpaRequestBody actualRequestBody = requestCaptor.getValue().getInput(); + assertEquals(SUBJECT, actualRequestBody.getSubject()); + assertEquals(TAGS, actualRequestBody.getTags()); + } + + @Test + public void givenOpaReturnsFalse_whenEvaluate_thenReturnFalse() throws CsHttpClientException, CsOpaException { + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(OpaRequest.class); + when(csHttpClientMock.postForObject(uriCaptor.capture(), requestCaptor.capture(), eq(OpaResult.class))) + .thenReturn(NEGATIVE_RESULT); + + OpaClient opaClient =new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); + boolean result = opaClient.newRequest() + .withAction(READ_SECRET_ACTION_NAME) + .withSubject(SUBJECT) + .withTags(TAGS) + .evaluate(); + + assertFalse("must not be allowed", result); + } + + @Test + public void givenNoSubjectDefined_whenEvaluate_thenThrowException() { + OpaClient opaClient = new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); + try { + opaClient.newRequest() + .withAction(READ_SECRET_ACTION_NAME) + .withTags(TAGS) + .evaluate(); + fail("must throw exception"); + } catch (CsOpaException e) { + assertEquals("Subject is required to evaluate the policy", e.getMessage()); + } + } + + @Test + public void givenNoActionDefined_whenEvaluate_thenThrowException() { + OpaClient opaClient = new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); + try { + opaClient.newRequest() + .withSubject(SUBJECT) + .withTags(TAGS) + .evaluate(); + fail("must throw exception"); + } catch (CsOpaException e) { + assertEquals("Action is required to evaluate the policy", e.getMessage()); + } + } + + @Test + public void givenClientThrows_whenEvaluate_thenReturnFalse() throws CsHttpClientException, CsOpaException { + when(csHttpClientMock.postForObject(any(), any(), eq(OpaResult.class))) + .thenThrow(new CsHttpClientException("")); + + OpaClient opaClient =new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); + boolean result = opaClient.newRequest() + .withAction(READ_SECRET_ACTION_NAME) + .withSubject(SUBJECT) + .withTags(TAGS) + .evaluate(); + assertFalse("must not be allowed", result); + } +} \ No newline at end of file diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaServiceTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaServiceTest.java new file mode 100644 index 0000000..bd62aa6 --- /dev/null +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaServiceTest.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2024 - for information on the respective copyright owner + * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.carbynestack.amphora.service.opa; + +import com.google.common.collect.Lists; +import io.carbynestack.amphora.common.Tag; +import io.carbynestack.amphora.service.exceptions.CsOpaException; +import org.junit.Before; +import org.junit.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static io.carbynestack.amphora.service.opa.OpaService.READ_SECRET_ACTION_NAME; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class OpaServiceTest { + + private static final String POLICY_PACKAGE = "play"; + private static final String DEFAULT_POLICY_PACKAGE = "default"; + private static final String SUBJECT = "me"; + private static final List TAGS = Lists.newArrayList( + Tag.builder().key("created").value("yesterday").build(), + Tag.builder().key("owner").value("me").build() + ); + private static final Tag POLICY_TAG = Tag.builder() + .key(OpaService.POLICY_PACKAGE_TAG_KEY) + .value(POLICY_PACKAGE).build(); + private static final OpaResult POSITIVE_RESULT; + static { + POSITIVE_RESULT = new OpaResult(); + POSITIVE_RESULT.setResult(true); + } + + @Mock + private OpaClient opaClientMock = mock(OpaClient.class); + + @Before + public void setUp() { + reset(opaClientMock); + when(opaClientMock.newRequest()).thenReturn(new OpaClientRequest(opaClientMock, DEFAULT_POLICY_PACKAGE)); + } + + @Test + public void givenPolicyDefinedInTag_whenIsAllowed_thenUsePolicyPackageProvided() throws CsOpaException { + ArgumentCaptor packageCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor subjectCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor> tagsCaptor = ArgumentCaptor.forClass(List.class); + when(opaClientMock.isAllowed( + packageCaptor.capture(), actionCaptor.capture(), subjectCaptor.capture(), tagsCaptor.capture())) + .thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + List testTags = Lists.newArrayList(TAGS); + testTags.add(POLICY_TAG); + boolean result = service.isAllowed(SUBJECT, READ_SECRET_ACTION_NAME, testTags); + + assertTrue("must be allowed", result); + String actualPackage = packageCaptor.getValue(); + assertEquals(POLICY_TAG.getValue(), actualPackage); + String actualAction = actionCaptor.getValue(); + assertEquals(READ_SECRET_ACTION_NAME, actualAction); + String actualSubject = subjectCaptor.getValue(); + assertEquals(SUBJECT, actualSubject); + List actualTags = tagsCaptor.getValue(); + assertEquals(testTags, actualTags); + } + + @Test + public void givenNoPolicyDefinedInTag_whenIsAllowed_thenUseDefaultPolicyPackage() throws CsOpaException { + ArgumentCaptor packageCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor> tagsCaptor = ArgumentCaptor.forClass(List.class); + when(opaClientMock.isAllowed( + packageCaptor.capture(), any(), any(), tagsCaptor.capture())) + .thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + boolean result = service.isAllowed(READ_SECRET_ACTION_NAME, SUBJECT, TAGS); + + assertTrue("must be allowed", result); + String actualPackage = packageCaptor.getValue(); + assertEquals(DEFAULT_POLICY_PACKAGE, actualPackage); + List actualTags = tagsCaptor.getValue(); + assertEquals(TAGS, actualTags); + } + + @Test + public void whenCanReadSecret_thenUseProperAction() throws CsOpaException { + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); + when(opaClientMock.isAllowed( + any(), actionCaptor.capture(), any(), any())) + .thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + service.canReadSecret(SUBJECT, TAGS); + + String actualAction = actionCaptor.getValue(); + assertEquals(READ_SECRET_ACTION_NAME, actualAction); + + } + + @Test + public void whenCanDeleteSecret_thenUseProperAction() throws CsOpaException { + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); + when(opaClientMock.isAllowed( + any(), actionCaptor.capture(), any(), any())) + .thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + service.canDeleteSecret(SUBJECT, TAGS); + + String actualAction = actionCaptor.getValue(); + assertEquals(OpaService.DELETE_SECRET_ACTION_NAME, actualAction); + } + + @Test + public void whenCanCreateTags_thenUseProperAction() throws CsOpaException { + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); + when(opaClientMock.isAllowed( + any(), actionCaptor.capture(), any(), any())) + .thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + service.canCreateTags(SUBJECT, TAGS); + + String actualAction = actionCaptor.getValue(); + assertEquals(OpaService.CREATE_TAG_ACTION_NAME, actualAction); + } + + @Test + public void whenCanReadTags_thenUseProperAction() throws CsOpaException { + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); + when(opaClientMock.isAllowed( + any(), actionCaptor.capture(), any(), any())) + .thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + service.canReadTags(SUBJECT, TAGS); + + String actualAction = actionCaptor.getValue(); + assertEquals(OpaService.READ_TAG_ACTION_NAME, actualAction); + } + + @Test + public void whenCanUpdateTags_thenUseProperAction() throws CsOpaException { + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); + when(opaClientMock.isAllowed( + any(), actionCaptor.capture(), any(), any())) + .thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + service.canUpdateTags(SUBJECT, TAGS); + + String actualAction = actionCaptor.getValue(); + assertEquals(OpaService.UPDATE_TAG_ACTION_NAME, actualAction); + } + + @Test + public void whenCanDeleteTags_thenUseProperAction() throws CsOpaException { + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); + when(opaClientMock.isAllowed( + any(), actionCaptor.capture(), any(), any())) + .thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + service.canDeleteTags(SUBJECT, TAGS); + + String actualAction = actionCaptor.getValue(); + assertEquals(OpaService.DELETE_TAG_ACTION_NAME, actualAction); + } +} \ No newline at end of file From a3c65689ed9a488a3a37b643327c661087823488 Mon Sep 17 00:00:00 2001 From: Sebastian Becker Date: Thu, 14 Nov 2024 15:43:42 +0100 Subject: [PATCH 2/8] feat(amphora-service): protect public endpoints Signed-off-by: Sebastian Becker --- .../service/calculation/SecretShareUtil.java | 3 +- .../service/config/AuthProperties.java | 22 + .../amphora/service/config/OpaConfig.java | 11 +- .../amphora/service/config/OpaProperties.java | 2 +- .../exceptions/UnauthorizedException.java | 14 + .../amphora/service/opa/JwtReader.java | 69 ++++ .../amphora/service/opa/OpaClient.java | 2 +- .../amphora/service/opa/OpaService.java | 2 + .../persistence/metadata/StorageService.java | 382 ++++++++++++------ .../service/rest/IntraVcpController.java | 13 +- .../service/rest/MaskedInputController.java | 19 +- .../rest/RestControllerExceptionHandler.java | 11 +- .../service/rest/SecretShareController.java | 22 +- .../amphora/service/rest/TagsController.java | 70 +++- .../src/main/resources/application.properties | 5 +- .../service/AmphoraServiceSystemTest.java | 32 +- .../calculation/SecretShareUtilTest.java | 8 +- .../amphora/service/opa/JwtReaderTest.java | 44 ++ .../amphora/service/opa/OpaClientTest.java | 28 +- .../amphora/service/opa/OpaServiceTest.java | 6 +- .../persistence/metadata/StorageIT.java | 66 ++- .../metadata/StorageServiceTest.java | 212 +++++----- .../service/rest/IntraVcpControllerTest.java | 4 +- .../rest/MaskedInputControllerTest.java | 55 ++- .../rest/SecretShareControllerTest.java | 62 +-- .../service/rest/TagsControllerTest.java | 88 ++-- .../resources/application-test.properties | 7 +- 27 files changed, 862 insertions(+), 397 deletions(-) create mode 100644 amphora-service/src/main/java/io/carbynestack/amphora/service/config/AuthProperties.java create mode 100644 amphora-service/src/main/java/io/carbynestack/amphora/service/exceptions/UnauthorizedException.java create mode 100644 amphora-service/src/main/java/io/carbynestack/amphora/service/opa/JwtReader.java create mode 100644 amphora-service/src/test/java/io/carbynestack/amphora/service/opa/JwtReaderTest.java diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/calculation/SecretShareUtil.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/calculation/SecretShareUtil.java index 0536909..713a375 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/calculation/SecretShareUtil.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/calculation/SecretShareUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 - for information on the respective copyright owner + * Copyright (c) 2021-2024 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 @@ -13,6 +13,7 @@ import io.carbynestack.amphora.common.MaskedInput; import io.carbynestack.amphora.common.MaskedInputData; import io.carbynestack.amphora.common.SecretShare; +import io.carbynestack.amphora.common.Tag; import io.carbynestack.castor.common.entities.Field; import io.carbynestack.castor.common.entities.InputMask; import io.carbynestack.castor.common.entities.Share; diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/config/AuthProperties.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/config/AuthProperties.java new file mode 100644 index 0000000..8e448da --- /dev/null +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/config/AuthProperties.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 - for information on the respective copyright owner + * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.carbynestack.amphora.service.config; + + +import lombok.Data; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@ConfigurationProperties(prefix = "carbynestack.auth") +@Component +@Data +@Accessors(chain = true) +public class AuthProperties { + private String userIdFieldName; +} diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaConfig.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaConfig.java index 1fb21c2..de8cedd 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaConfig.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaConfig.java @@ -7,20 +7,21 @@ package io.carbynestack.amphora.service.config; +import io.carbynestack.amphora.service.opa.JwtReader; import io.carbynestack.amphora.service.opa.OpaClient; -import io.carbynestack.castor.client.download.CastorIntraVcpClient; -import io.carbynestack.castor.client.download.DefaultCastorIntraVcpClient; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import org.springframework.web.client.RestTemplate; -import java.io.File; import java.net.URI; @Configuration public class OpaConfig { + @Bean + JwtReader jwtReader(AuthProperties authProperties) { + return new JwtReader(authProperties.getUserIdFieldName()); + } + @Bean OpaClient opaClient(OpaProperties opaProperties) { return OpaClient.builder() diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaProperties.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaProperties.java index 2a50e85..12c73b2 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaProperties.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaProperties.java @@ -11,7 +11,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; -@ConfigurationProperties(prefix = "carbynestack.amphora.opa") +@ConfigurationProperties(prefix = "carbynestack.opa") @Component @Data @Accessors(chain = true) diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/exceptions/UnauthorizedException.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/exceptions/UnauthorizedException.java new file mode 100644 index 0000000..9828698 --- /dev/null +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/exceptions/UnauthorizedException.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2024 - for information on the respective copyright owner + * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.carbynestack.amphora.service.exceptions; + +public class UnauthorizedException extends Exception { + public UnauthorizedException(String message) { + super(message); + } +} diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/JwtReader.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/JwtReader.java new file mode 100644 index 0000000..fba703a --- /dev/null +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/JwtReader.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 - for information on the respective copyright owner + * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.carbynestack.amphora.service.opa; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.carbynestack.amphora.service.exceptions.UnauthorizedException; +import io.vavr.control.Option; + +import java.util.Base64; + +public class JwtReader { + static ObjectMapper mapper = new ObjectMapper(); + private final String userIdField; + + public JwtReader(String userIdField) { + this.userIdField = userIdField; + } + + public String extractUserIdFromAuthHeader(String header) throws UnauthorizedException { + return extractFieldFromAuthHeader(header, userIdField); + } + + private static String extractFieldFromAuthHeader(String header, String field) throws UnauthorizedException { + return tokenFromHeader(header) + .flatMap(JwtReader::dataNodeFromToken) + .flatMap(node -> fieldFromNode(node, field)) + .getOrElseThrow(() -> new UnauthorizedException("No token provided")); + } + + private static Option dataNodeFromToken(String token) { + String[] parts = token.split("\\."); + if (parts.length != 3) { + return Option.none(); + } + try { + String jwt = new String(Base64.getDecoder().decode(parts[1])); + return Option.of(mapper.reader().readTree(jwt)); + } catch (JsonProcessingException e) { + return Option.none(); + } + } + + private static Option tokenFromHeader(String header) { + if (header != null && header.startsWith("Bearer ")) { + return Option.of(header.substring(7)); + } + return Option.none(); + } + + private static Option fieldFromNode(JsonNode node, String fieldName) { + JsonNode field = node; + try { + for(String f : fieldName.split("\\.")) { + field = field.get(f); + } + return Option.of(field.asText()); + } catch (NullPointerException e) { + return Option.none(); + } + } +} + diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClient.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClient.java index 54aacb6..1fb544f 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClient.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClient.java @@ -42,7 +42,7 @@ public OpaClient(URI opaServiceUri, String defaultPolicyPackage) { * @param tags The tags describing the accessed object. * @return True if the subject can perform the action, false otherwise (or if an error occurred). */ - boolean isAllowed(String policyPackage, String action, String subject, List tags) { + public boolean isAllowed(String policyPackage, String action, String subject, List tags) { OpaRequestBody body = OpaRequestBody.builder() .subject(subject) .tags(tags) diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaService.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaService.java index d154726..2ee6770 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaService.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaService.java @@ -17,6 +17,8 @@ @Service public class OpaService { public static final String POLICY_PACKAGE_TAG_KEY = "accessPolicy"; + public static final String OWNER_TAG_KEY = "owner"; + static final String READ_SECRET_ACTION_NAME = "read"; static final String DELETE_SECRET_ACTION_NAME = "delete"; static final String CREATE_TAG_ACTION_NAME = "tag/create"; diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java index b271f3f..cfa0445 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023 - for information on the respective copyright owner + * Copyright (c) 2021-2024 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 @@ -7,24 +7,23 @@ package io.carbynestack.amphora.service.persistence.metadata; -import static io.carbynestack.amphora.service.persistence.metadata.TagEntity.setFromTagList; -import static io.carbynestack.amphora.service.persistence.metadata.TagEntity.setToTagList; - +import com.google.common.collect.Lists; import io.carbynestack.amphora.common.*; import io.carbynestack.amphora.common.exceptions.AmphoraServiceException; import io.carbynestack.amphora.service.calculation.SecretShareUtil; import io.carbynestack.amphora.service.config.AmphoraServiceProperties; import io.carbynestack.amphora.service.config.SpdzProperties; import io.carbynestack.amphora.service.exceptions.AlreadyExistsException; +import io.carbynestack.amphora.service.exceptions.CsOpaException; import io.carbynestack.amphora.service.exceptions.NotFoundException; +import io.carbynestack.amphora.service.exceptions.UnauthorizedException; +import io.carbynestack.amphora.service.opa.OpaService; import io.carbynestack.amphora.service.persistence.cache.InputMaskCachingService; import io.carbynestack.amphora.service.persistence.datastore.SecretShareDataStore; import io.carbynestack.castor.common.entities.Field; import io.carbynestack.castor.common.entities.InputMask; import io.carbynestack.castor.common.entities.TupleList; import io.vavr.control.Option; -import java.util.*; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -33,9 +32,18 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; +import java.util.*; +import java.util.stream.Collectors; + +import static io.carbynestack.amphora.service.opa.OpaService.OWNER_TAG_KEY; +import static io.carbynestack.amphora.service.persistence.metadata.TagEntity.setFromTagList; +import static io.carbynestack.amphora.service.persistence.metadata.TagEntity.setToTagList; + /** * A service to persist and manipulate {@link SecretEntity SecretEntities} and related data like * {@link TagEntity TagEntities} and {@link SecretShare SecretShares}. @@ -45,18 +53,20 @@ @RequiredArgsConstructor(onConstructor_ = @Autowired) public class StorageService { public static final String CREATION_DATE_KEY = "creation-date"; - public static final List RESERVED_TAG_KEYS = Collections.singletonList(CREATION_DATE_KEY); + public static final List RESERVED_TAG_KEYS = Lists.newArrayList( + CREATION_DATE_KEY, + OWNER_TAG_KEY); public static final String TAGS_WITH_THE_SAME_KEY_DEFINED_EXCEPTION_MSG = - "Two or more tags with the same key defined."; + "Two or more tags with the same key defined."; public static final String SECRET_WITH_ID_EXISTS_EXCEPTION_MSG = - "A secret with the given id already exists."; + "A secret with the given id already exists."; public static final String IS_RESERVED_KEY_EXCEPTION_MSG = "\"%s\" is a reserved key."; public static final String NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG = - "No secret with the given id #%s exists."; + "No secret with the given id #%s exists."; public static final String NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG = - "No tag with key \"%s\" exists for secret with id #%s."; + "No tag with key \"%s\" exists for secret with id #%s."; public static final String TAG_WITH_KEY_EXISTS_FOR_SECRET_EXCEPTION_MSG = - "A tag with key \"%s\" already exists for secret #%s"; + "A tag with key \"%s\" already exists for secret #%s"; private final SecretEntityRepository secretEntityRepository; private final InputMaskCachingService inputMaskStore; @@ -65,6 +75,7 @@ public class StorageService { private final SpdzProperties spdzProperties; private final AmphoraServiceProperties amphoraServiceProperties; private final SecretShareDataStore secretShareDataStore; + private final OpaService opaService; /** * Takes a {@link MaskedInput}, converts it into an individual {@link SecretShare} and persits the @@ -74,6 +85,7 @@ public class StorageService { * persisting the secret without further notice. * * @param maskedInput the {@link MaskedInput} to persist + * @param authorizedUserId the id of the authenticated user becoming the owner of the persisted {@link SecretShare} * @return the id of the new {@link SecretShare} as {@link String} * @throws AlreadyExistsException if an {@link SecretShare} with the given id already exists. * @throws IllegalArgumentException if one or more {@link Tag}s with the same {@link Tag#getKey() @@ -83,7 +95,7 @@ public class StorageService { * fails */ @Transactional - public String createSecret(MaskedInput maskedInput) { + public String createSecret(MaskedInput maskedInput, String authorizedUserId) { if (secretEntityRepository.existsById(maskedInput.getSecretId().toString())) { throw new AlreadyExistsException(SECRET_WITH_ID_EXISTS_EXCEPTION_MSG); } @@ -91,14 +103,15 @@ public String createSecret(MaskedInput maskedInput) { throw new IllegalArgumentException(TAGS_WITH_THE_SAME_KEY_DEFINED_EXCEPTION_MSG); } TupleList, Field.Gfp> inputMasks = - inputMaskStore.getCachedInputMasks(maskedInput.getSecretId()); + inputMaskStore.getCachedInputMasks(maskedInput.getSecretId()); SecretShare secretShare = - secretShareUtil.convertToSecretShare( - maskedInput, - spdzProperties.getMacKey(), - inputMasks, - amphoraServiceProperties.getPlayerId() != 0); - String secretId = persistSecretShare(secretShare); + secretShareUtil.convertToSecretShare( + maskedInput, + spdzProperties.getMacKey(), + inputMasks, + amphoraServiceProperties.getPlayerId() != 0); + String secretId = persistSecretShare(secretShare, + Collections.singletonList(Tag.builder().key(OWNER_TAG_KEY).value(authorizedUserId).build())); inputMaskStore.removeInputMasks(secretShare.getSecretId()); return secretId; } @@ -124,27 +137,28 @@ public String storeSecretShare(SecretShare secretShare) { if (hasDuplicateKey(secretShare.getTags())) { throw new IllegalArgumentException(TAGS_WITH_THE_SAME_KEY_DEFINED_EXCEPTION_MSG); } - return persistSecretShare(secretShare); + return persistSecretShare(secretShare, Collections.emptyList()); } /** * @return the id used to reference the persisted data * @throws AmphoraServiceException if storing the {@link SecretShare#getData() data} fails. */ - private String persistSecretShare(SecretShare secretShare) { + private String persistSecretShare(SecretShare secretShare, List reservedTags) { List tags = dropReservedTags(new ArrayList<>(secretShare.getTags())); tags.add( - Tag.builder() - .key(StorageService.CREATION_DATE_KEY) - .value(Long.toString(System.currentTimeMillis())) - .build()); + Tag.builder() + .key(StorageService.CREATION_DATE_KEY) + .value(Long.toString(System.currentTimeMillis())) + .build()); + tags.addAll(reservedTags); Set tagEntities = setFromTagList(tags); String persistedSecretId = - secretEntityRepository - .save(new SecretEntity(secretShare.getSecretId().toString(), tagEntities)) - .getSecretId(); + secretEntityRepository + .save(new SecretEntity(secretShare.getSecretId().toString(), tagEntities)) + .getSecretId(); secretShareDataStore.storeSecretShareData( - UUID.fromString(persistedSecretId), secretShare.getData()); + UUID.fromString(persistedSecretId), secretShare.getData()); return persistedSecretId; } @@ -164,12 +178,12 @@ private static boolean hasDuplicateKey(List tags) { private static List dropReservedTags(List tags) { if (!CollectionUtils.isEmpty(tags)) { List itemsToDrop = - tags.stream().filter(StorageService::tagIsReserved).collect(Collectors.toList()); + tags.stream().filter(StorageService::tagIsReserved).collect(Collectors.toList()); itemsToDrop.forEach( - t -> { - log.debug("Dropped tag {} for using reserved key.", t.toString()); - tags.remove(t); - }); + t -> { + log.debug("Dropped tag {} for using reserved key.", t.toString()); + tags.remove(t); + }); } return tags; } @@ -191,8 +205,8 @@ public Page getSecretList(Sort sort) { @Transactional(readOnly = true) public Page getSecretList(List tagFilters, Pageable pageable) { return secretEntityRepository - .findAll(SecretEntitySpecification.with(tagFilters), pageable) - .map(SecretEntity::toMetadata); + .findAll(SecretEntitySpecification.with(tagFilters), pageable) + .map(SecretEntity::toMetadata); } @Transactional(readOnly = true) @@ -210,44 +224,94 @@ public Page getSecretList(List tagFilters, Sort sort) { * @throws NotFoundException if no {@link SecretShare} with the given id exists */ @Transactional(readOnly = true) - public SecretShare getSecretShare(UUID secretId) { + public SecretShare getSecretShareAuthorized(UUID secretId) { return secretEntityRepository - .findById(secretId.toString()) - .map( - entity -> - SecretShare.builder() - .secretId(secretId) - .data(secretShareDataStore.getSecretShareData(secretId)) - .tags(setToTagList(entity.getTags())) - .build()) - .orElseThrow( - () -> - new NotFoundException( - String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + .findById(secretId.toString()) + .map(this::getSecretShareForEntity) + .orElseThrow( + () -> + new NotFoundException( + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + } + + /** + * Retrieves an {@link SecretShare} with a given id. + *

+ * This method requires the authenticated user to be authorized to perform the operation. The authorization is + * checked by the {@link OpaService#canReadSecret(String, List)} method using the {@link Tag}s of + * the related {@link SecretEntity}. + * + * @param secretId id of the {@link SecretShare} to retrieve + * @param authorizedUserId the id of the authenticated user requesting the operation + * @return an {@link Option} containing the requested {@link SecretShare} of {@link Option#none()} + * if no secret with the given id exists. + * @throws AmphoraServiceException if an {@link SecretShare} exists but could not be retrieved. + * @throws NotFoundException if no {@link SecretShare} with the given id exists + * @throws UnauthorizedException if the user not authorized to read the secret + * @throws CsOpaException if an error occurred while evaluating the policy + */ + @Transactional(readOnly = true) + public SecretShare getSecretShare(UUID secretId, String authorizedUserId) throws CsOpaException, UnauthorizedException { + SecretEntity secretEntity = secretEntityRepository + .findById(secretId.toString()) + .orElseThrow( + () -> + new NotFoundException( + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + + if(!opaService.canReadSecret(authorizedUserId, setToTagList(secretEntity.getTags()))) { + throw new UnauthorizedException("User is not authorized to read this secret"); + } + return getSecretShareForEntity(secretEntity); + } + + private SecretShare getSecretShareForEntity(SecretEntity entity) { + return SecretShare.builder() + .secretId(UUID.fromString(entity.getSecretId())) + .data(secretShareDataStore.getSecretShareData(UUID.fromString(entity.getSecretId()))) + .tags(setToTagList(entity.getTags())) + .build(); } /** * Removes an {@link SecretEntity} and all related information ({@link TagEntity tags} and {@link * SecretShare data}) from the storage. + *

+ * This method requires the authenticated user to be authorized to perform the operation. The authorization is + * checked by the {@link OpaService#canDeleteSecret(String, List)} method using the {@link Tag}s of + * the related {@link SecretEntity}. * * @param secretId the id of the secret to be removed. + * @param authorizedUserId the id of the authenticated user requesting the deletion * @throws NotFoundException if no {@link SecretEntity} with the given id exists. * @throws AmphoraServiceException if the {@link SecretEntity}'s data could not be deleted. + * @throws UnauthorizedException if the user is not authorized to delete the secret + * @throws CsOpaException if an error occurred while evaluating the policy */ @Transactional - public void deleteSecret(UUID secretId) { + public void deleteSecret(UUID secretId, String authorizedUserId) throws CsOpaException, UnauthorizedException { // Better to accept String as input once - instead of repeatedly converting it. - if (secretEntityRepository.deleteBySecretId(secretId.toString()) == 0) { - throw new NotFoundException(String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId)); + SecretEntity secretEntity = secretEntityRepository.findById(secretId.toString()) + .orElseThrow(() -> + new NotFoundException( + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + if(!opaService.canDeleteSecret(authorizedUserId, setToTagList(secretEntity.getTags()))) { + throw new UnauthorizedException("User is not authorized to delete this secret"); } + secretEntityRepository.deleteBySecretId(secretId.toString()); secretShareDataStore.deleteSecretShareData(secretId); } /** * Persists a {@link Tag} related to a specified {@link SecretShare} + *

+ * This method requires the authenticated user to be authorized to perform the operation. The authorization is + * checked by the {@link OpaService#canCreateTags(String, List)} method using the {@link Tag}s of + * the related {@link SecretEntity}. * * @param secretId id of the secret this {@link Tag} belongs to * @param tag the tag to persist + * @param authorizedUserId the ID of the authenticated user requesting the operation * @return the {@link Tag#getKey() key} of the stored {@link Tag} * @throws IllegalArgumentException if tag uses a reserved key (see {@link #RESERVED_TAG_KEYS}). * @throws NotFoundException if no {@link SecretEntity} with the given id exists. @@ -255,53 +319,69 @@ public void deleteSecret(UUID secretId) { * exists. */ @Transactional - public String storeTag(UUID secretId, Tag tag) { + public String storeTag(UUID secretId, Tag tag, String authorizedUserId) throws CsOpaException, UnauthorizedException { if (tagIsReserved(tag)) { throw new IllegalArgumentException( - String.format(IS_RESERVED_KEY_EXCEPTION_MSG, tag.getKey())); + String.format(IS_RESERVED_KEY_EXCEPTION_MSG, tag.getKey())); } - if (!secretEntityRepository.existsById(secretId.toString())) { - throw new NotFoundException(String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId)); + SecretEntity secretEntity = + secretEntityRepository + .findById(secretId.toString()) + .orElseThrow( + () -> + new NotFoundException( + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + if (!opaService.canCreateTags(authorizedUserId, setToTagList(secretEntity.getTags()))) { + throw new UnauthorizedException("User is not authorized to create tags for this secret"); } - SecretEntity secretEntityReference = secretEntityRepository.getById(secretId.toString()); tagRepository - .findBySecretAndKey(secretEntityReference, tag.getKey()) - .ifPresent( - t -> { - throw new AlreadyExistsException( - String.format( - TAG_WITH_KEY_EXISTS_FOR_SECRET_EXCEPTION_MSG, tag.getKey(), secretId)); - }); - return tagRepository.save(TagEntity.fromTag(tag).setSecret(secretEntityReference)).getKey(); + .findBySecretAndKey(secretEntity, tag.getKey()) + .ifPresent( + t -> { + throw new AlreadyExistsException( + String.format( + TAG_WITH_KEY_EXISTS_FOR_SECRET_EXCEPTION_MSG, tag.getKey(), secretId)); + }); + return tagRepository.save(TagEntity.fromTag(tag).setSecret(secretEntity)).getKey(); } /** * Replaces the {@link Tag}s for a {@link SecretEntity} with the given id. - * - *

{@link Tag}s that use a reserved tag {@link #RESERVED_TAG_KEYS} will be removed before + *

+ * This method requires the authenticated user to be authorized to perform the operation. The authorization is + * checked by the {@link OpaService#canUpdateTags(String, List)} method using the {@link Tag}s of + * the related {@link SecretEntity}. + *

+ * {@link Tag}s that use a reserved tag {@link #RESERVED_TAG_KEYS} will be removed before * persisting the secret without further notice. * * @param secretId the id of the {@link SecretEntity} whose tags should be replaced. * @param tags the new set of {@link Tag}s. + * @param authorizedUserId the id of the authenticated user requesting the operation * @throws IllegalArgumentException if the set of {@link Tag}s contains duplicate {@link * Tag#getKey() keys}. * @throws NotFoundException if no {@link SecretEntity} with the given id exists. + * @throws UnauthorizedException if the user is not authorized to update the tags + * @throws CsOpaException if an error occurred while evaluating the policy */ @Transactional - public void replaceTags(UUID secretId, List tags) { + public void replaceTags(UUID secretId, List tags, String authorizedUserId) throws CsOpaException, UnauthorizedException { if (hasDuplicateKey(tags)) { throw new IllegalArgumentException(TAGS_WITH_THE_SAME_KEY_DEFINED_EXCEPTION_MSG); } - if (!secretEntityRepository.existsById(secretId.toString())) { - throw new NotFoundException(String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId)); + SecretEntity secretEntityReference = secretEntityRepository.findById(secretId.toString()) + .orElseThrow(() -> + new NotFoundException( + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + if (!opaService.canUpdateTags(authorizedUserId, setToTagList(secretEntityReference.getTags()))) { + throw new UnauthorizedException("User is not authorized to update tags for this secret"); } - SecretEntity secretEntityReference = secretEntityRepository.getById(secretId.toString()); List existingReservedTags = - RESERVED_TAG_KEYS.stream() - .map(key -> tagRepository.findBySecretAndKey(secretEntityReference, key)) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toList()); + RESERVED_TAG_KEYS.stream() + .map(key -> tagRepository.findBySecretAndKey(secretEntityReference, key)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); List newTags = dropReservedTags(new ArrayList<>(tags)); tagRepository.deleteBySecret(secretEntityReference); Set newTagList = setFromTagList(newTags); @@ -312,109 +392,163 @@ public void replaceTags(UUID secretId, List tags) { /** * Returns all {@link Tag}s associated to an {@link SecretEntity} with the given id. + *

+ * This method requires the authenticated user to be authorized to perform the operation. The authorization is + * checked by the {@link OpaService#canReadTags(String, List)} method using the {@link Tag}s of + * the related {@link SecretEntity}. * * @param secretId the id of the {@link SecretEntity} whose tags should be retrieved. + * @param authorizedUserId the id of the authenticated user requesting the operation * @return a list of {@link Tag}s * @throws NotFoundException if no {@link SecretEntity} with the given id exists. + * @throws UnauthorizedException if the user is not authorized to read the tags + * @throws CsOpaException if an error occurred while evaluating the policy */ @Transactional(readOnly = true) - public List retrieveTags(UUID secretId) { + public List retrieveTags(UUID secretId, String authorizedUserId) throws CsOpaException, UnauthorizedException { SecretEntity secretEntity = - secretEntityRepository - .findById(secretId.toString()) - .orElseThrow( - () -> - new NotFoundException( - String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + secretEntityRepository + .findById(secretId.toString()) + .orElseThrow( + () -> + new NotFoundException( + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + if (!opaService.canReadTags(authorizedUserId, setToTagList(secretEntity.getTags()))) { + throw new UnauthorizedException("User is not authorized to read tags for this secret"); + } return setToTagList(secretEntity.getTags()); } /** * Returns a single {@link Tag}s associated to an {@link SecretEntity} with the given id, and a * specified {@link Tag#getKey() key}. + *

+ * This method requires the authenticated user to be authorized to perform the operation. The authorization is + * checked by the {@link OpaService#canReadTags(String, List)} method using the {@link Tag}s of + * the related {@link SecretEntity}. * * @param secretId the id of the {@link SecretShare} whose {@link Tag} to be retrieved. * @param key the {@link Tag#getKey() key} of the {@link Tag} to be retrieved. + * @param authorizedUserId the id of the authenticated user requesting the operation * @return the {@link Tag} * @throws NotFoundException if no {@link SecretShare} with the given id exists. * @throws NotFoundException if no {@link Tag} with the given {@link Tag#getKey() key} exists. + * @throws UnauthorizedException if the user is not authorized to read the tag + * @throws CsOpaException if an error occurred while evaluating the policy */ @Transactional(readOnly = true) - public Tag retrieveTag(UUID secretId, String key) { - if (!secretEntityRepository.existsById(secretId.toString())) { - throw new NotFoundException(String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId)); + public Tag retrieveTag(UUID secretId, String key, String authorizedUserId) throws CsOpaException, UnauthorizedException { + SecretEntity secretEntityReference = secretEntityRepository.findById(secretId.toString()) + .orElseThrow(() -> + new NotFoundException( + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + if (!opaService.canReadTags(authorizedUserId, setToTagList(secretEntityReference.getTags()))) { + throw new UnauthorizedException("User is not authorized to read tags for this secret"); } - SecretEntity secretEntityReference = secretEntityRepository.getById(secretId.toString()); return tagRepository - .findBySecretAndKey(secretEntityReference, key) - .map(TagEntity::toTag) - .orElseThrow( - () -> - new NotFoundException( - String.format( - NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, key, secretId))); + .findBySecretAndKey(secretEntityReference, key) + .map(TagEntity::toTag) + .orElseThrow( + () -> + new NotFoundException( + String.format( + NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, key, secretId))); } /** * Updates an existing {@link Tag} linked to an {@link SecretEntity} with the given id. + *

+ * This method requires the authenticated user to be authorized to perform the operation. The authorization is + * checked by the {@link OpaService#canUpdateTags(String, List)} method using the {@link Tag}s of + * the related {@link SecretEntity}. * * @param secretId the id of the {@link SecretShare} whose {@link Tag} to be updated. * @param tag the new tag + * @param authorizedUserId the id of the authenticated user requesting the operation * @throws IllegalArgumentException if tag uses a reserved key (see {@link #RESERVED_TAG_KEYS}). * @throws NotFoundException if no {@link SecretShare} with the given id exists. * @throws NotFoundException if no {@link Tag} with the given {@link Tag#getKey() key} exists. + * @throws UnauthorizedException if the user is not authorized to update the tag + * @throws CsOpaException if an error occurred while evaluating the policy */ @Transactional - public void updateTag(UUID secretId, Tag tag) { + public void updateTag(UUID secretId, Tag tag, String authorizedUserId) throws CsOpaException, UnauthorizedException { if (tagIsReserved(tag)) { throw new IllegalArgumentException( - String.format(IS_RESERVED_KEY_EXCEPTION_MSG, tag.getKey())); + String.format(IS_RESERVED_KEY_EXCEPTION_MSG, tag.getKey())); } - if (!secretEntityRepository.existsById(secretId.toString())) { - throw new NotFoundException(String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId)); + SecretEntity secretEntityReference = secretEntityRepository.findById(secretId.toString()) + .orElseThrow(() -> + new NotFoundException( + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + if (!opaService.canUpdateTags(authorizedUserId, setToTagList(secretEntityReference.getTags()))) { + throw new UnauthorizedException("User is not authorized to update tags for this secret"); } - SecretEntity secretEntityReference = secretEntityRepository.getById(secretId.toString()); TagEntity existingTag = - tagRepository - .findBySecretAndKey(secretEntityReference, tag.getKey()) - .orElseThrow( - () -> - new NotFoundException( - String.format( - NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, - tag.getKey(), - secretId))); + tagRepository + .findBySecretAndKey(secretEntityReference, tag.getKey()) + .orElseThrow( + () -> + new NotFoundException( + String.format( + NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, + tag.getKey(), + secretId))); tagRepository.save( - existingTag.setValue(tag.getValue()).setValueType(tag.getValueType().toString())); + existingTag.setValue(tag.getValue()).setValueType(tag.getValueType().toString())); } /** * Deletes an existing {@link Tag} linked to an {@link SecretEntity} with the given id. + *

+ * This method requires the authenticated user to be authorized to perform the operation. The authorization is + * checked by the {@link OpaService#canDeleteTags(String, List)} method using the {@link Tag}s of + * the related {@link SecretEntity}. * * @param secretId the id of the {@link SecretShare} whose {@link Tag} to be deleted. * @param key the {@link Tag#getKey() key} of the {@link Tag} to be deleted. + * @param authorizedUserId the id of the authenticated user requesting the operation * @throws IllegalArgumentException if tag uses a reserved key (see {@link #RESERVED_TAG_KEYS}). * @throws NotFoundException if no {@link SecretShare} with the given id exists. * @throws NotFoundException if no {@link Tag} with the given {@link Tag#getKey() key} exists. + * @throws UnauthorizedException if the user is not authorized to delete the tag + * @throws CsOpaException if an error occurred while evaluating the policy */ @Transactional - public void deleteTag(UUID secretId, String key) { + public void deleteTag(UUID secretId, String key, String authorizedUserId) throws CsOpaException, UnauthorizedException { if (RESERVED_TAG_KEYS.contains(key)) { throw new IllegalArgumentException(String.format(IS_RESERVED_KEY_EXCEPTION_MSG, key)); } - if (!secretEntityRepository.existsById(secretId.toString())) { - throw new NotFoundException(String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId)); - } - SecretEntity secretEntityReference = secretEntityRepository.getById(secretId.toString()); - tagRepository.delete( - tagRepository - .findBySecretAndKey(secretEntityReference, key) - .orElseThrow( - () -> + SecretEntity secretEntityReference = secretEntityRepository.findById(secretId.toString()) + .orElseThrow(() -> new NotFoundException( - String.format( - NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, - key, - secretId)))); + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + + if (!opaService.canDeleteTags(authorizedUserId, setToTagList(secretEntityReference.getTags()))) { + throw new UnauthorizedException("User is not authorized to delete tags for this secret"); + } + secretEntityReference.getTags().remove(tagRepository + .findBySecretAndKey(secretEntityReference, key).orElseThrow( + () -> + new NotFoundException( + String.format( + NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, + key, + secretId)))); +// SecretEntity secretEntityReference = secretEntityRepository.findById(secretId.toString()) +// .orElseThrow(() -> +// new NotFoundException( +// String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); +// if (!opaService.canDeleteTags(authorizedUserId, setToTagList(secretEntityReference.getTags()))) { +// throw new UnauthorizedException("User is not authorized to delete tags for this secret"); +// } +// TagEntity tagEntity = tagRepository.findBySecretAndKey(secretEntityReference, key) +// .orElseThrow(() -> +// new NotFoundException( +// String.format( +// NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, +// key, +// secretId))); +// tagRepository.delete(tagEntity); } } diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/IntraVcpController.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/IntraVcpController.java index 9b86049..f8e6f8e 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/IntraVcpController.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/IntraVcpController.java @@ -1,21 +1,17 @@ /* - * Copyright (c) 2021 - for information on the respective copyright owner + * Copyright (c) 2021-2024 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 */ package io.carbynestack.amphora.service.rest; -import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.*; - import io.carbynestack.amphora.common.SecretShare; import io.carbynestack.amphora.common.Tag; import io.carbynestack.amphora.common.exceptions.AmphoraServiceException; import io.carbynestack.amphora.service.exceptions.AlreadyExistsException; import io.carbynestack.amphora.service.exceptions.NotFoundException; import io.carbynestack.amphora.service.persistence.metadata.StorageService; -import java.net.URI; -import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -26,6 +22,11 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import java.net.URI; +import java.util.UUID; + +import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.*; + @Slf4j @RestController @RequiredArgsConstructor(onConstructor_ = @Autowired) @@ -67,6 +68,6 @@ public ResponseEntity uploadSecretShare(@RequestBody SecretShare secretShar */ @GetMapping(path = "/{" + SECRET_ID_PARAMETER + "}") public ResponseEntity downloadSecretShare(@PathVariable UUID secretId) { - return new ResponseEntity<>(storageService.getSecretShare(secretId), HttpStatus.OK); + return new ResponseEntity<>(storageService.getSecretShareAuthorized(secretId), HttpStatus.OK); } } diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/MaskedInputController.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/MaskedInputController.java index 2e63424..919ef54 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/MaskedInputController.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/MaskedInputController.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 - for information on the respective copyright owner + * Copyright (c) 2021-2024 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 @@ -15,17 +15,17 @@ import io.carbynestack.amphora.common.SecretShare; import io.carbynestack.amphora.common.Tag; import io.carbynestack.amphora.service.exceptions.AlreadyExistsException; +import io.carbynestack.amphora.service.exceptions.UnauthorizedException; import io.carbynestack.amphora.service.persistence.metadata.StorageService; import java.net.URI; + +import io.carbynestack.amphora.service.opa.JwtReader; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; @Slf4j @@ -34,6 +34,7 @@ @RequestMapping(path = UPLOAD_MASKED_INPUTS_ENDPOINT) public class MaskedInputController { private final StorageService storageService; + private final JwtReader jwtReader; /** * Takes a {@link MaskedInput}, converts it into an individual {@link SecretShare} and persits the @@ -51,12 +52,16 @@ public class MaskedInputController { * @throws AlreadyExistsException if an {@link SecretShare} with the given id already exists. */ @PostMapping - public ResponseEntity upload(@RequestBody MaskedInput maskedInput) { + public ResponseEntity upload(@RequestHeader("Authorization") String authorizationHeader, + @RequestBody MaskedInput maskedInput) throws UnauthorizedException { notNull(maskedInput, "MaskedInput must not be null"); notEmpty(maskedInput.getData(), "MaskedInput data must not be empty"); return new ResponseEntity<>( ServletUriComponentsBuilder.fromCurrentRequestUri() - .pathSegment(storageService.createSecret(maskedInput)) + .pathSegment( + storageService.createSecret( + maskedInput, + jwtReader.extractUserIdFromAuthHeader(authorizationHeader))) .build() .toUri(), CREATED); diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/RestControllerExceptionHandler.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/RestControllerExceptionHandler.java index cc8364a..71fb037 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/RestControllerExceptionHandler.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/RestControllerExceptionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 - for information on the respective copyright owner + * Copyright (c) 2021-2024 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 @@ -13,6 +13,7 @@ import io.carbynestack.amphora.common.exceptions.AmphoraServiceException; import io.carbynestack.amphora.service.exceptions.AlreadyExistsException; import io.carbynestack.amphora.service.exceptions.NotFoundException; +import io.carbynestack.amphora.service.exceptions.UnauthorizedException; import io.carbynestack.castor.common.exceptions.CastorServiceException; import java.io.UnsupportedEncodingException; import org.springframework.http.HttpStatus; @@ -49,6 +50,14 @@ protected ResponseEntity handleBadRequestException(Exception e) OBJECT_WRITER.writeValueAsString(e.getMessage()), HttpStatus.BAD_REQUEST); } + @ExceptionHandler({UnauthorizedException.class}) + protected ResponseEntity handleUnauthorizedException(UnauthorizedException e) + throws JsonProcessingException { + logger.debug("Handling Unauthorized Error", e); + return new ResponseEntity<>( + OBJECT_WRITER.writeValueAsString(e.getMessage()), HttpStatus.UNAUTHORIZED); + } + @ExceptionHandler({AmphoraServiceException.class, CastorServiceException.class, Exception.class}) protected ResponseEntity handleInternalException(Exception e) throws JsonProcessingException { diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/SecretShareController.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/SecretShareController.java index bb95d83..d8719fe 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/SecretShareController.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/SecretShareController.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023 - for information on the respective copyright owner + * Copyright (c) 2021-2024 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 @@ -12,14 +12,18 @@ import io.carbynestack.amphora.common.*; import io.carbynestack.amphora.common.exceptions.AmphoraServiceException; import io.carbynestack.amphora.service.calculation.OutputDeliveryService; +import io.carbynestack.amphora.service.exceptions.CsOpaException; import io.carbynestack.amphora.service.exceptions.NotFoundException; +import io.carbynestack.amphora.service.exceptions.UnauthorizedException; import io.carbynestack.amphora.service.persistence.metadata.StorageService; +import io.carbynestack.amphora.service.opa.JwtReader; import io.vavr.control.Try; import java.io.UnsupportedEncodingException; import java.util.*; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; @@ -32,11 +36,12 @@ @RestController @RequestMapping(path = SECRET_SHARES_ENDPOINT) -@RequiredArgsConstructor +@RequiredArgsConstructor(onConstructor_ = @Autowired) @Slf4j public class SecretShareController { private final StorageService storageService; private final OutputDeliveryService outputDeliveryService; + private final JwtReader jwtReader; /** * Retrieves a page of all {@link Metadata} matching the given filters and criteria. @@ -104,10 +109,12 @@ public ResponseEntity getObjectList( */ @GetMapping(path = "/{" + SECRET_ID_PARAMETER + "}") public ResponseEntity getSecretShare( + @RequestHeader("Authorization") String authorizationHeader, @PathVariable final UUID secretId, - @RequestParam(value = REQUEST_ID_PARAMETER) final UUID requestId) { + @RequestParam(value = REQUEST_ID_PARAMETER) final UUID requestId) throws UnauthorizedException, CsOpaException { Assert.notNull(requestId, "Request identifier must not be omitted"); - SecretShare secretShare = storageService.getSecretShare(secretId); + SecretShare secretShare = storageService.getSecretShare(secretId, + jwtReader.extractUserIdFromAuthHeader(authorizationHeader)); OutputDeliveryObject outputDeliveryObject = outputDeliveryService.computeOutputDeliveryObject(secretShare, requestId); return new ResponseEntity<>( @@ -123,8 +130,11 @@ public ResponseEntity getSecretShare( * @throws AmphoraServiceException if the SecretEntity's data could not be deleted. */ @DeleteMapping(path = "/{" + SECRET_ID_PARAMETER + "}") - public ResponseEntity deleteSecretShare(@PathVariable UUID secretId) { - storageService.deleteSecret(secretId); + public ResponseEntity deleteSecretShare( + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable UUID secretId) throws UnauthorizedException, CsOpaException { + storageService.deleteSecret(secretId, + jwtReader.extractUserIdFromAuthHeader(authorizationHeader)); return new ResponseEntity<>(HttpStatus.OK); } diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/TagsController.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/TagsController.java index 3796994..cc4d0b9 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/TagsController.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/TagsController.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 - for information on the respective copyright owner + * Copyright (c) 2021-2024 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 @@ -11,11 +11,16 @@ import io.carbynestack.amphora.common.SecretShare; import io.carbynestack.amphora.common.Tag; import io.carbynestack.amphora.service.exceptions.AlreadyExistsException; +import io.carbynestack.amphora.service.exceptions.CsOpaException; import io.carbynestack.amphora.service.exceptions.NotFoundException; +import io.carbynestack.amphora.service.exceptions.UnauthorizedException; import io.carbynestack.amphora.service.persistence.metadata.StorageService; import java.net.URI; import java.util.List; import java.util.UUID; + +import io.carbynestack.amphora.service.opa.JwtReader; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -27,14 +32,11 @@ @Slf4j @RestController +@RequiredArgsConstructor(onConstructor_ = @Autowired) @RequestMapping(path = SECRET_SHARES_ENDPOINT + "/{" + SECRET_ID_PARAMETER + "}" + TAGS_ENDPOINT) public class TagsController { private final StorageService storageService; - - @Autowired - public TagsController(StorageService storageService) { - this.storageService = storageService; - } + private final JwtReader jwtReader; /** * Retrieves all {@link Tag}s for an {@link SecretShare} with the given id. @@ -44,8 +46,14 @@ public TagsController(StorageService storageService) { * @throws NotFoundException if no {@link SecretShare} with the given id exists. */ @GetMapping - public ResponseEntity> getTags(@PathVariable UUID secretId) { - return new ResponseEntity<>(storageService.retrieveTags(secretId), HttpStatus.OK); + public ResponseEntity> getTags( + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable UUID secretId) throws UnauthorizedException, CsOpaException { + return new ResponseEntity<>( + storageService.retrieveTags( + secretId, + jwtReader.extractUserIdFromAuthHeader(authorizationHeader)), + HttpStatus.OK); } /** @@ -62,11 +70,17 @@ public ResponseEntity> getTags(@PathVariable UUID secretId) { */ @Transactional @PostMapping - public ResponseEntity createTag(@PathVariable UUID secretId, @RequestBody Tag tag) { + public ResponseEntity createTag( + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable UUID secretId, @RequestBody Tag tag) throws UnauthorizedException, CsOpaException { Assert.notNull(tag, "Tag must not be empty"); return new ResponseEntity<>( ServletUriComponentsBuilder.fromCurrentRequestUri() - .pathSegment(storageService.storeTag(secretId, tag)) + .pathSegment( + storageService.storeTag( + secretId, + tag, + jwtReader.extractUserIdFromAuthHeader(authorizationHeader))) .build() .toUri(), HttpStatus.CREATED); @@ -88,9 +102,14 @@ public ResponseEntity createTag(@PathVariable UUID secretId, @RequestBody T */ @Transactional @PutMapping - public ResponseEntity updateTags(@PathVariable UUID secretId, @RequestBody List tags) { + public ResponseEntity updateTags( + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable UUID secretId, @RequestBody List tags) throws UnauthorizedException, CsOpaException { Assert.notEmpty(tags, "At least one tag must be given."); - storageService.replaceTags(secretId, tags); + storageService.replaceTags( + secretId, + tags, + jwtReader.extractUserIdFromAuthHeader(authorizationHeader)); return new ResponseEntity<>(HttpStatus.OK); } @@ -104,8 +123,15 @@ public ResponseEntity updateTags(@PathVariable UUID secretId, @RequestBody * @throws NotFoundException if no {@link Tag} with the given {@link Tag#getKey() key} exists. */ @GetMapping(path = "/{" + TAG_KEY_PARAMETER + ":.+}") - public ResponseEntity getTag(@PathVariable UUID secretId, @PathVariable String tagKey) { - return new ResponseEntity<>(storageService.retrieveTag(secretId, tagKey), HttpStatus.OK); + public ResponseEntity getTag( + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable UUID secretId, @PathVariable String tagKey) throws UnauthorizedException, CsOpaException { + return new ResponseEntity<>( + storageService.retrieveTag( + secretId, + tagKey, + jwtReader.extractUserIdFromAuthHeader(authorizationHeader)), + HttpStatus.OK); } /** @@ -126,13 +152,17 @@ public ResponseEntity getTag(@PathVariable UUID secretId, @PathVariable Str @Transactional @PutMapping(path = "/{" + TAG_KEY_PARAMETER + ":.+}") public ResponseEntity putTag( - @PathVariable UUID secretId, @PathVariable String tagKey, @RequestBody Tag tag) { + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable UUID secretId, @PathVariable String tagKey, @RequestBody Tag tag) throws UnauthorizedException, CsOpaException { Assert.notNull(tag, "Tag must not be empty"); if (!tagKey.equals(tag.getKey())) { throw new IllegalArgumentException( String.format("The defined key and tag data do not match.%n%s <> %s", tagKey, tag)); } - storageService.updateTag(secretId, tag); + storageService.updateTag( + secretId, + tag, + jwtReader.extractUserIdFromAuthHeader(authorizationHeader)); return new ResponseEntity<>(HttpStatus.OK); } @@ -149,8 +179,12 @@ public ResponseEntity putTag( */ @Transactional @DeleteMapping(path = "/{" + TAG_KEY_PARAMETER + ":.+}") - public ResponseEntity deleteTag(@PathVariable UUID secretId, @PathVariable String tagKey) { - storageService.deleteTag(secretId, tagKey); + public ResponseEntity deleteTag( + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable UUID secretId, @PathVariable String tagKey) throws UnauthorizedException, CsOpaException { + storageService.deleteTag(secretId, + tagKey, + jwtReader.extractUserIdFromAuthHeader(authorizationHeader)); return new ResponseEntity<>(HttpStatus.OK); } } diff --git a/amphora-service/src/main/resources/application.properties b/amphora-service/src/main/resources/application.properties index aa7c134..cc9b74e 100644 --- a/amphora-service/src/main/resources/application.properties +++ b/amphora-service/src/main/resources/application.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 - for information on the respective copyright owner +# Copyright (c) 2023-2024 - for information on the respective copyright owner # see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. # # SPDX-License-Identifier: Apache-2.0 @@ -21,6 +21,9 @@ server.servlet.context-path=/ management.endpoints.web.exposure.include=info,health,prometheus +carbynestack.auth.user-id-field-name=${AMPHORA_USER_ID_FIELD_NAME:sub} +carbynestack.opa.default-policy-package=${AMPHORA_OPA_DEFAULT_POLICY_PACKAGE:default} +carbynestack.opa.endpoint=${AMPHORA_OPA_URL:http://opa.carbynestack.io} carbynestack.spdz.mac-key=${MAC_KEY} carbynestack.spdz.prime=${SPDZ_PRIME} carbynestack.spdz.r=${SPDZ_R} diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/AmphoraServiceSystemTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/AmphoraServiceSystemTest.java index 5ff6536..299d5f0 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/AmphoraServiceSystemTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/AmphoraServiceSystemTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 - for information on the respective copyright owner + * Copyright (c) 2023-2024 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 @@ -11,13 +11,20 @@ import static java.util.Arrays.asList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasItems; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; import static org.springframework.test.util.AssertionErrors.assertEquals; +import static org.springframework.test.util.AssertionErrors.assertTrue; import io.carbynestack.amphora.common.Metadata; import io.carbynestack.amphora.common.MetadataPage; import io.carbynestack.amphora.common.SecretShare; import io.carbynestack.amphora.common.Tag; +import io.carbynestack.amphora.service.exceptions.UnauthorizedException; +import io.carbynestack.amphora.service.opa.OpaClient; +import io.carbynestack.amphora.service.persistence.metadata.StorageService; import io.carbynestack.amphora.service.testconfig.PersistenceTestEnvironment; import io.carbynestack.amphora.service.testconfig.ReusableMinioContainer; import io.carbynestack.amphora.service.testconfig.ReusablePostgreSQLContainer; @@ -33,9 +40,15 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.testcontainers.junit.jupiter.Container; @@ -53,6 +66,8 @@ public class AmphoraServiceSystemTest { private final UUID testSecretId2 = UUID.fromString("82a73814-321c-4261-abcd-27c6c0ebfb27"); private final UUID testSecretId3 = UUID.fromString("82a73814-321c-4261-abcd-27c6c0ebfb28"); private final UUID testSecretId4 = UUID.fromString("82a73814-321c-4261-abcd-27c6c0ebfb29"); + private final String bearerToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5Njk0OTgyLWQzMTAtNDBkOC04ZDk4LTczOWI1ZGZjNWUyNiIsInR5cCI6IkpXVCJ9.eyJhbXIiOlsicGFzc3dvcmQiXSwiYXRfaGFzaCI6InowbjhudTNJQ19HaXN3bmFTWjgwZ2ciLCJhdWQiOlsiOGExYTIwNzUtMzY3Yi00ZGU1LTgyODgtMGMyNzQ1OTMzMmI3Il0sImF1dGhfdGltZSI6MTczMTUwMDQ0OSwiZXhwIjoxNzMxNTA0NDIyLCJpYXQiOjE3MzE1MDA4MjIsImlzcyI6Imh0dHA6Ly8xNzIuMTguMS4xMjguc3NsaXAuaW8vaWFtL29hdXRoIiwianRpIjoiZTlhMmQxYzQtZGViNy00MTgwLWE0M2YtN2QwNTZhYjNlNTk3Iiwibm9uY2UiOiJnV1JVZjhxTERaeDZpOFNhMXpMdm9IX2tSQ01OWll2WTE0WTFsLWNBU0tVIiwicmF0IjoxNzMxNTAwODIyLCJzaWQiOiJlNGVkOTc2Mi0yMmNlLTQyYzEtOTU3NC01MDVkYjAyMThhNDYiLCJzdWIiOiJhZmMwMTE3Zi1jOWNkLTRkOGMtYWNlZS1mYTE0MzNjYTBmZGQifQ.OACqa6WjpAeZbHR54b3p7saUk9plTdXlZsou41E-gfC7WxCG7ZEKfDPKXUky-r20oeIt1Ov3S2QL6Kefe5dTXEC6nhKGxeClg8ys56_FPcx_neI-p09_pSWOkMx7DHP65giaP7UubyyInVpE-2Eu1o6TpoheahNQfCahKDsJmJ-4Vvts3wA79UMfOI0WHO4vLaaG6DRAZQK_dv7ltw3p_WlncpaQAtHwY9iVhtdB3LtAI39EjplCoAF0c9uQO6W7KHWUlj24l2kc564bsJgZSrYvezw6b2-FY7YisVnicSyAORpeqhWEpLltH3D8I1NtHlSYMJhWuVZbBhAm7Iz6q1-W-Q9ttvdPchdwPSASFRkrjrdIyQf6MsFrItKzUxYck57EYL4GkyN9MWvMNxL1UTtkzGsFEczUVsJFm8OQpulYXIFZksmnPTBB0KuUUvEZ-xih8V1HsMRoHvbiCLaDJwjOFKzPevVggsSMysPKR52UAZJDZLTeHBnVCtQ3rro6T0RxNg94lXypz0AmfsGnoTF34u4FmMxzoeFZ9N5zmEpOnMRqLs7Sb3FnLL-IMitc9_2bsHuFbBJl8KbiGHBQihK5v5SIa292L7P9ChsxomWVhM29qHNFuXQMwFUr57hmveNh2Fz9mduZ5h2hLUuDf5xc6u9rSxy3_e3t_xBuUT4"; + private final String authorizedUserId ="afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; @Container public static ReusableRedisContainer reusableRedisContainer = @@ -66,6 +81,9 @@ public class AmphoraServiceSystemTest { public static ReusablePostgreSQLContainer reusablePostgreSQLContainer = ReusablePostgreSQLContainer.getInstance(); + @MockBean + private OpaClient opaClientMock; + @Autowired private TestRestTemplate restTemplate; @Autowired private PersistenceTestEnvironment testEnvironment; @@ -112,8 +130,10 @@ public class AmphoraServiceSystemTest { .build(); @BeforeEach - public void setUp() { + public void setUp() throws UnauthorizedException { testEnvironment.clearAllData(); + when(opaClientMock.newRequest()).thenCallRealMethod(); + when(opaClientMock.isAllowed(any(), any(), eq(authorizedUserId), any())).thenReturn(true); } @Test @@ -204,8 +224,12 @@ void givenSuccessfulRequest_whenDeleteSecret_thenReturnRemoveAndDoNoLongerReturn "Response contains wrong total number of results prior to deletion!", 4L, responsePreDel.getTotalElements()); - restTemplate.delete( - new URI(String.format("/secret-shares/%s", testSecretShare1.getSecretId()))); + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(bearerToken); + HttpEntity entity = new HttpEntity<>(headers); + ResponseEntity response = restTemplate.exchange(new URI(String.format("/secret-shares/%s", testSecretShare1.getSecretId())), + HttpMethod.DELETE, entity, Void.class); + assertTrue("Request must be successful.", response.getStatusCode().is2xxSuccessful()); MetadataPage responsePostDel = restTemplate.getForObject(SECRET_SHARES_ENDPOINT, MetadataPage.class); assertEquals( diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/calculation/SecretShareUtilTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/calculation/SecretShareUtilTest.java index 44512b5..d469288 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/calculation/SecretShareUtilTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/calculation/SecretShareUtilTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 - for information on the respective copyright owner + * Copyright (c) 2023-2024 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 @@ -19,10 +19,8 @@ import io.carbynestack.mpspdz.integration.MpSpdzIntegrationUtils; import java.math.BigInteger; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; +import java.util.*; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/JwtReaderTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/JwtReaderTest.java new file mode 100644 index 0000000..fe9ddbb --- /dev/null +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/JwtReaderTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 - for information on the respective copyright owner + * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.carbynestack.amphora.service.opa; + +import io.carbynestack.amphora.service.exceptions.UnauthorizedException; +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class JwtReaderTest { + + @Test + void givenTokenProvided_whenExtractSubject_thenReturnSubject() throws UnauthorizedException { + JwtReader jwtReader = new JwtReader("sub"); + String header = "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5Njk0OTgyLWQzMTAtNDBkOC04ZDk4LTczOWI1ZGZjNWUyNiIsInR5cCI6IkpXVCJ9.eyJhbXIiOlsicGFzc3dvcmQiXSwiYXRfaGFzaCI6InowbjhudTNJQ19HaXN3bmFTWjgwZ2ciLCJhdWQiOlsiOGExYTIwNzUtMzY3Yi00ZGU1LTgyODgtMGMyNzQ1OTMzMmI3Il0sImF1dGhfdGltZSI6MTczMTUwMDQ0OSwiZXhwIjoxNzMxNTA0NDIyLCJpYXQiOjE3MzE1MDA4MjIsImlzcyI6Imh0dHA6Ly8xNzIuMTguMS4xMjguc3NsaXAuaW8vaWFtL29hdXRoIiwianRpIjoiZTlhMmQxYzQtZGViNy00MTgwLWE0M2YtN2QwNTZhYjNlNTk3Iiwibm9uY2UiOiJnV1JVZjhxTERaeDZpOFNhMXpMdm9IX2tSQ01OWll2WTE0WTFsLWNBU0tVIiwicmF0IjoxNzMxNTAwODIyLCJzaWQiOiJlNGVkOTc2Mi0yMmNlLTQyYzEtOTU3NC01MDVkYjAyMThhNDYiLCJzdWIiOiJhZmMwMTE3Zi1jOWNkLTRkOGMtYWNlZS1mYTE0MzNjYTBmZGQifQ.OACqa6WjpAeZbHR54b3p7saUk9plTdXlZsou41E-gfC7WxCG7ZEKfDPKXUky-r20oeIt1Ov3S2QL6Kefe5dTXEC6nhKGxeClg8ys56_FPcx_neI-p09_pSWOkMx7DHP65giaP7UubyyInVpE-2Eu1o6TpoheahNQfCahKDsJmJ-4Vvts3wA79UMfOI0WHO4vLaaG6DRAZQK_dv7ltw3p_WlncpaQAtHwY9iVhtdB3LtAI39EjplCoAF0c9uQO6W7KHWUlj24l2kc564bsJgZSrYvezw6b2-FY7YisVnicSyAORpeqhWEpLltH3D8I1NtHlSYMJhWuVZbBhAm7Iz6q1-W-Q9ttvdPchdwPSASFRkrjrdIyQf6MsFrItKzUxYck57EYL4GkyN9MWvMNxL1UTtkzGsFEczUVsJFm8OQpulYXIFZksmnPTBB0KuUUvEZ-xih8V1HsMRoHvbiCLaDJwjOFKzPevVggsSMysPKR52UAZJDZLTeHBnVCtQ3rro6T0RxNg94lXypz0AmfsGnoTF34u4FmMxzoeFZ9N5zmEpOnMRqLs7Sb3FnLL-IMitc9_2bsHuFbBJl8KbiGHBQihK5v5SIa292L7P9ChsxomWVhM29qHNFuXQMwFUr57hmveNh2Fz9mduZ5h2hLUuDf5xc6u9rSxy3_e3t_xBuUT4"; + String result = jwtReader.extractUserIdFromAuthHeader(header); + String expectedSubject = "afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; + assertEquals(expectedSubject, result); + } + + @Test + void givenNoTokenProvided_whenExtractSubject_thenThrowUnauthorizedException() throws UnauthorizedException { + JwtReader jwtReader = new JwtReader("sub"); + String invalidToken = "{\"auth_time\": 1731500449,\n" + + " \"exp\": 1731504422,\n" + + " \"iat\": 1731500822,\n" + + " \"something\": {\n" + + " \"what\": \"is this\"\n"+ + " }}"; + assertThrows(UnauthorizedException.class, () -> jwtReader.extractUserIdFromAuthHeader( + String.format( + "Bearer header.%s.signature", + Base64.toBase64String(invalidToken.getBytes(StandardCharsets.UTF_8))))); + } +} \ No newline at end of file diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaClientTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaClientTest.java index e6b2d19..72b76ba 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaClientTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaClientTest.java @@ -14,8 +14,8 @@ import io.carbynestack.httpclient.CsHttpClientException; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -25,13 +25,15 @@ import java.util.List; import static io.carbynestack.amphora.service.opa.OpaService.READ_SECRET_ACTION_NAME; -import static org.junit.Assert.*; +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) -public class OpaClientTest { +class OpaClientTest { private static final URI OPA_SERVICE_URI = URI.create("http://localhost:8081"); private static final String POLICY_PACKAGE = "play"; @@ -51,13 +53,13 @@ public class OpaClientTest { @Mock private CsHttpClient csHttpClientMock = mock(CsHttpClient.class); - @Before + @BeforeEach public void setUp() { reset(csHttpClientMock); } @Test - public void givenValidRequest_whenEvaluate_thenReturnTrue() throws CsOpaException, CsHttpClientException { + void givenValidRequest_whenEvaluate_thenReturnTrue() throws CsOpaException, CsHttpClientException { ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(OpaRequest.class); when(csHttpClientMock.postForObject(uriCaptor.capture(), requestCaptor.capture(), eq(OpaResult.class))) @@ -83,7 +85,7 @@ public void givenValidRequest_whenEvaluate_thenReturnTrue() throws CsOpaExceptio } @Test - public void givenNoPolicyPackageDefined_whenEvaluate_thenReturnUseDefaultPackage() throws CsOpaException, CsHttpClientException { + void givenNoPolicyPackageDefined_whenEvaluate_thenReturnUseDefaultPackage() throws CsOpaException, CsHttpClientException { ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(OpaRequest.class); when(csHttpClientMock.postForObject(uriCaptor.capture(), requestCaptor.capture(), eq(OpaResult.class))) @@ -108,7 +110,7 @@ public void givenNoPolicyPackageDefined_whenEvaluate_thenReturnUseDefaultPackage } @Test - public void givenOpaReturnsFalse_whenEvaluate_thenReturnFalse() throws CsHttpClientException, CsOpaException { + void givenOpaReturnsFalse_whenEvaluate_thenReturnFalse() throws CsHttpClientException, CsOpaException { ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(OpaRequest.class); when(csHttpClientMock.postForObject(uriCaptor.capture(), requestCaptor.capture(), eq(OpaResult.class))) @@ -121,11 +123,11 @@ public void givenOpaReturnsFalse_whenEvaluate_thenReturnFalse() throws CsHttpCli .withTags(TAGS) .evaluate(); - assertFalse("must not be allowed", result); + assertFalse(result, "must not be allowed"); } @Test - public void givenNoSubjectDefined_whenEvaluate_thenThrowException() { + void givenNoSubjectDefined_whenEvaluate_thenThrowException() { OpaClient opaClient = new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); try { opaClient.newRequest() @@ -139,7 +141,7 @@ public void givenNoSubjectDefined_whenEvaluate_thenThrowException() { } @Test - public void givenNoActionDefined_whenEvaluate_thenThrowException() { + void givenNoActionDefined_whenEvaluate_thenThrowException() { OpaClient opaClient = new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); try { opaClient.newRequest() @@ -153,7 +155,7 @@ public void givenNoActionDefined_whenEvaluate_thenThrowException() { } @Test - public void givenClientThrows_whenEvaluate_thenReturnFalse() throws CsHttpClientException, CsOpaException { + void givenClientThrows_whenEvaluate_thenReturnFalse() throws CsHttpClientException, CsOpaException { when(csHttpClientMock.postForObject(any(), any(), eq(OpaResult.class))) .thenThrow(new CsHttpClientException("")); @@ -163,6 +165,6 @@ public void givenClientThrows_whenEvaluate_thenReturnFalse() throws CsHttpClient .withSubject(SUBJECT) .withTags(TAGS) .evaluate(); - assertFalse("must not be allowed", result); + assertFalse(result, "must not be allowed"); } } \ No newline at end of file diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaServiceTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaServiceTest.java index bd62aa6..49f536b 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaServiceTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaServiceTest.java @@ -10,8 +10,8 @@ import com.google.common.collect.Lists; import io.carbynestack.amphora.common.Tag; import io.carbynestack.amphora.service.exceptions.CsOpaException; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -47,7 +47,7 @@ public class OpaServiceTest { @Mock private OpaClient opaClientMock = mock(OpaClient.class); - @Before + @BeforeEach public void setUp() { reset(opaClientMock); when(opaClientMock.newRequest()).thenReturn(new OpaClientRequest(opaClientMock, DEFAULT_POLICY_PACKAGE)); diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageIT.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageIT.java index fd9ad5a..f9b7be6 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageIT.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageIT.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 - for information on the respective copyright owner + * Copyright (c) 2023-2024 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 @@ -13,6 +13,9 @@ import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; import static org.springframework.util.CollectionUtils.isEmpty; @@ -23,7 +26,10 @@ import io.carbynestack.amphora.common.exceptions.AmphoraServiceException; import io.carbynestack.amphora.service.AmphoraServiceApplication; import io.carbynestack.amphora.service.config.MinioProperties; +import io.carbynestack.amphora.service.exceptions.CsOpaException; import io.carbynestack.amphora.service.exceptions.NotFoundException; +import io.carbynestack.amphora.service.exceptions.UnauthorizedException; +import io.carbynestack.amphora.service.opa.OpaClient; import io.carbynestack.amphora.service.testconfig.PersistenceTestEnvironment; import io.carbynestack.amphora.service.testconfig.ReusableMinioContainer; import io.carbynestack.amphora.service.testconfig.ReusablePostgreSQLContainer; @@ -40,10 +46,13 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.Page; import org.springframework.data.domain.Sort; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -58,6 +67,7 @@ public class StorageIT { private final UUID testSecretId = UUID.fromString("3bcf8308-8f50-4d24-a37b-b0075bb5e779"); private final UUID testSecretId2 = UUID.fromString("0e7cd962-d98e-4eea-82ae-4641399c9ad7"); private final Tag testTag = Tag.builder().key("TEST_KEY").value("TEST_VALUE").build(); + private final String authorizedUserId ="afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; @Container public static ReusableRedisContainer reusableRedisContainer = @@ -82,9 +92,14 @@ public class StorageIT { @Autowired private MinioClient minioClient; @Autowired private MinioProperties minioProperties; + @MockBean + private OpaClient opaClientMock; + @BeforeEach public void setUp() { testEnvironment.clearAllData(); + when(opaClientMock.newRequest()).thenCallRealMethod(); + when(opaClientMock.isAllowed(any(), any(), eq(authorizedUserId), any())).thenReturn(true); } private SecretEntity persistObjectWithIdAndTags(UUID id, Tag... tags) { @@ -93,12 +108,14 @@ private SecretEntity persistObjectWithIdAndTags(UUID id, Tag... tags) { } @Test - void givenSuccessfulRequest_whenStoreTag_thenPersist() { + void givenSuccessfulRequest_whenStoreTag_thenPersist() throws CsOpaException, UnauthorizedException { persistObjectWithIdAndTags(testSecretId, testTag); storageService.storeTag( - testSecretId, Tag.builder().key("ANOTHER_KEY").value(testTag.getValue()).build()); + testSecretId, Tag.builder().key("ANOTHER_KEY").value(testTag.getValue()).build(), + authorizedUserId); - assertEquals(testTag, storageService.retrieveTag(testSecretId, testTag.getKey())); + assertEquals(testTag, + storageService.retrieveTag(testSecretId, testTag.getKey(),authorizedUserId)); } @Test @@ -113,7 +130,8 @@ void givenSuccessfulRequest_whenGetObjectList_thenReturnExpectedResult() { @Test void givenNoObjectWithReferencedIdDefined_whenRetrieveTags_thenThrowNotFoundException() { NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.retrieveTags(testSecretId)); + assertThrows(NotFoundException.class, () -> + storageService.retrieveTags(testSecretId, authorizedUserId)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @@ -121,24 +139,25 @@ void givenNoObjectWithReferencedIdDefined_whenRetrieveTags_thenThrowNotFoundExce @Test void givenSecretIdUnknown_whenStoreTag_thenThrowNotFoundException() { NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.storeTag(unknownId, testTag)); + assertThrows(NotFoundException.class, () -> + storageService.storeTag(unknownId, testTag, authorizedUserId)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, unknownId), nfe.getMessage()); } @Test - void givenSuccessfulRequests_whenStoreAndRetrieveTag_thenPersistAndReturnExpectedContent() { + void givenSuccessfulRequests_whenStoreAndRetrieveTag_thenPersistAndReturnExpectedContent() throws CsOpaException, UnauthorizedException { persistObjectWithIdAndTags(testSecretId); - storageService.storeTag(testSecretId, testTag); - List tags = storageService.retrieveTags(testSecretId); + storageService.storeTag(testSecretId, testTag, authorizedUserId); + List tags = storageService.retrieveTags(testSecretId, authorizedUserId); assertEquals(testTag, tags.get(0)); assertEquals(1, tags.size()); } @Test - void givenObjectWithoutTagsDefined_whenRetrieveTags_thenReturnEmptyList() { + void givenObjectWithoutTagsDefined_whenRetrieveTags_thenReturnEmptyList() throws CsOpaException, UnauthorizedException { persistObjectWithIdAndTags(testSecretId); - assertEquals(Collections.emptyList(), storageService.retrieveTags(testSecretId)); + assertEquals(Collections.emptyList(), storageService.retrieveTags(testSecretId, authorizedUserId)); } @SneakyThrows @@ -151,7 +170,8 @@ void givenObjectWithoutTagsDefined_whenRetrieveSecretShare_thenReturnEmptyListFo .object(testSecretId.toString()) .stream(new ByteArrayInputStream(new byte[0]), 0, -1) .build()); - assertEquals(Collections.emptyList(), storageService.getSecretShare(testSecretId).getTags()); + assertEquals(Collections.emptyList(), + storageService.getSecretShare(testSecretId, authorizedUserId).getTags()); } @Test @@ -159,7 +179,8 @@ void givenObjectHasNoDataPersisted_whenGetSecretShare_thenThrowAmphoraServiceExc persistObjectWithIdAndTags(testSecretId); AmphoraServiceException ase = assertThrows( - AmphoraServiceException.class, () -> storageService.getSecretShare(testSecretId)); + AmphoraServiceException.class, () -> + storageService.getSecretShare(testSecretId, authorizedUserId)); assertEquals( String.format( GET_DATA_FOR_SECRET_EXCEPTION_MSG, testSecretId, "The specified key does not exist."), @@ -173,23 +194,23 @@ void givenSecretIdUnknown_whenReplaceTags_thenThrowNotFoundException() { assertThrows( NotFoundException.class, () -> { - storageService.replaceTags(unknownId, tags); + storageService.replaceTags(unknownId, tags, authorizedUserId); }); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, unknownId), nfe.getMessage()); } @Test - void givenSuccessfulRequest_whenDeleteTag_thenDoNoLongerReturn() { + void givenSuccessfulRequest_whenDeleteTag_thenDoNoLongerReturn() throws CsOpaException, UnauthorizedException { SecretEntity secretEntity = persistObjectWithIdAndTags(testSecretId, testTag); Tag expectedTag = Tag.builder().key(testTag.getKey() + "new").value(testTag.getValue()).build(); TagEntity expectedTagEntity = TagEntity.fromTag(expectedTag).setSecret(secretEntity); tagRepository.save(expectedTagEntity); persistObjectWithIdAndTags(testSecretId2, testTag); - storageService.deleteTag(testSecretId, testTag.getKey()); + storageService.deleteTag(testSecretId, testTag.getKey(), authorizedUserId); - List actualTags = storageService.retrieveTags(testSecretId); + List actualTags = storageService.retrieveTags(testSecretId, authorizedUserId); assertEquals(1, actualTags.size()); assertEquals(expectedTag, actualTags.get(0)); assertEquals( @@ -211,7 +232,8 @@ void givenObjectHasNoTagWithRequestedKey_whenDeleteKey_thenThrowNotFoundExceptio NotFoundException nfe = assertThrows( - NotFoundException.class, () -> storageService.deleteTag(testSecretId, unknownKey)); + NotFoundException.class, () -> + storageService.deleteTag(testSecretId, unknownKey, authorizedUserId)); assertEquals( String.format( NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, unknownKey, testSecretId), @@ -219,12 +241,12 @@ void givenObjectHasNoTagWithRequestedKey_whenDeleteKey_thenThrowNotFoundExceptio } @Test - void givenMultipleObjectsWIthIdenticalTag_whenDeleteTagOnOneObject_thenKeepTagForOtherObjects() { + void givenMultipleObjectsWIthIdenticalTag_whenDeleteTagOnOneObject_thenKeepTagForOtherObjects() throws CsOpaException, UnauthorizedException { persistObjectWithIdAndTags(testSecretId, testTag); persistObjectWithIdAndTags(testSecretId2, testTag); - storageService.deleteTag(testSecretId, testTag.getKey()); + storageService.deleteTag(testSecretId, testTag.getKey(), authorizedUserId); - assertTrue(isEmpty(storageService.retrieveTags(testSecretId))); + assertTrue(isEmpty(storageService.retrieveTags(testSecretId, authorizedUserId))); assertEquals( 1, storageService @@ -239,7 +261,7 @@ void givenMultipleObjectsWIthIdenticalTag_whenDeleteTagOnOneObject_thenKeepTagFo @Test void givenAnUnknownId_whenStoringATag_thenThrow() { NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.storeTag(testSecretId, testTag)); + assertThrows(NotFoundException.class, () -> storageService.storeTag(testSecretId, testTag, authorizedUserId)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageServiceTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageServiceTest.java index 6adac2b..a5617b2 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageServiceTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageServiceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 - for information on the respective copyright owner + * Copyright (c) 2023-2024 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 @@ -7,33 +7,25 @@ package io.carbynestack.amphora.service.persistence.metadata; -import static io.carbynestack.amphora.service.persistence.metadata.StorageService.*; -import static io.carbynestack.amphora.service.util.CreationDateTagMatchers.containsCreationDateTag; -import static io.carbynestack.castor.common.entities.Field.GFP; -import static java.util.Arrays.asList; -import static java.util.Collections.*; -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; -import static org.mockito.Mockito.any; - import io.carbynestack.amphora.common.*; import io.carbynestack.amphora.common.exceptions.AmphoraServiceException; import io.carbynestack.amphora.service.calculation.SecretShareUtil; import io.carbynestack.amphora.service.config.AmphoraServiceProperties; import io.carbynestack.amphora.service.config.SpdzProperties; import io.carbynestack.amphora.service.exceptions.AlreadyExistsException; +import io.carbynestack.amphora.service.exceptions.CsOpaException; import io.carbynestack.amphora.service.exceptions.NotFoundException; +import io.carbynestack.amphora.service.exceptions.UnauthorizedException; +import io.carbynestack.amphora.service.opa.OpaClient; +import io.carbynestack.amphora.service.opa.OpaService; import io.carbynestack.amphora.service.persistence.cache.InputMaskCachingService; import io.carbynestack.amphora.service.persistence.datastore.SecretShareDataStore; import io.carbynestack.castor.common.entities.InputMask; import io.carbynestack.castor.common.entities.TupleList; import io.carbynestack.mpspdz.integration.MpSpdzIntegrationUtils; -import java.util.*; -import java.util.stream.Collectors; import org.apache.commons.lang3.RandomUtils; import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -42,6 +34,22 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.*; +import java.util.*; +import java.util.stream.Collectors; + +import static io.carbynestack.amphora.service.opa.OpaService.OWNER_TAG_KEY; +import static io.carbynestack.amphora.service.persistence.metadata.StorageService.*; +import static io.carbynestack.amphora.service.persistence.metadata.TagEntity.setToTagList; +import static io.carbynestack.amphora.service.util.CreationDateTagMatchers.containsCreationDateTag; +import static io.carbynestack.castor.common.entities.Field.GFP; +import static java.util.Arrays.asList; +import static java.util.Collections.*; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.*; + @ExtendWith(MockitoExtension.class) class StorageServiceTest { private final UUID testSecretId = UUID.fromString("3bcf8308-8f50-4d24-a37b-b0075bb5e779"); @@ -50,6 +58,7 @@ class StorageServiceTest { private final Tag testTag2 = Tag.builder().key("SUPER_KEY").value("MY#SUPER,VALUE").build(); private final Tag testTagReservedCreationDateKey = Tag.builder().key(StorageService.RESERVED_TAG_KEYS.get(0)).value("MY#SUPER,VALUE").build(); + private final String authorizedUserId ="afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; @Mock private SecretEntityRepository secretEntityRepository; @Mock private InputMaskCachingService inputMaskStore; @@ -58,6 +67,7 @@ class StorageServiceTest { @Mock private SpdzProperties spdzProperties; @Mock private AmphoraServiceProperties amphoraServiceProperties; @Mock private SecretShareDataStore secretShareDataStore; + @Mock private OpaService opaService; @InjectMocks private StorageService storageService; @@ -75,7 +85,8 @@ void givenIdIsAlreadyInUse_whenCreateSecret_thenThrowAlreadyExistsException() { AlreadyExistsException aee = assertThrows( - AlreadyExistsException.class, () -> storageService.createSecret(testMaskedInput)); + AlreadyExistsException.class, () -> + storageService.createSecret(testMaskedInput, authorizedUserId )); assertEquals(SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, aee.getMessage()); } @@ -97,7 +108,7 @@ void givenMaskedInputHasTagsWithSameKey_whenCreateObject_thenThrowIllegalArgumen IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, - () -> storageService.createSecret(maskedInputDuplicateTagKeys)); + () -> storageService.createSecret(maskedInputDuplicateTagKeys, authorizedUserId)); assertEquals(TAGS_WITH_THE_SAME_KEY_DEFINED_EXCEPTION_MSG, iae.getMessage()); } @@ -129,11 +140,11 @@ void givenMaskedInputWithReservedKey_whenCreateObject_thenReplaceReservedKey() { assertThrows( RuntimeException.class, - () -> storageService.createSecret(maskedInput), + () -> storageService.createSecret(maskedInput, authorizedUserId), expectedAbortTestException.getMessage()); SecretEntity capturedSecretEntity = secretEntityArgumentCaptor.getValue(); List actualTags = TagEntity.setToTagList(capturedSecretEntity.getTags()); - assertEquals(2, actualTags.size()); + assertEquals(3, actualTags.size()); // creation date, owner and test tag MatcherAssert.assertThat(actualTags, allOf(hasItem(testTag), containsCreationDateTag())); Tag actualTagWithReservedKey = actualTags.stream() @@ -172,7 +183,7 @@ void givenSuccessfulRequest_whenCreateObject_thenReturnSecretId() { when(secretEntityRepository.save(secretEntityArgumentCaptor.capture())) .thenReturn(persistedSecretEntity); - assertEquals(maskedInput.getSecretId().toString(), storageService.createSecret(maskedInput)); + assertEquals(maskedInput.getSecretId().toString(), storageService.createSecret(maskedInput, authorizedUserId)); verify(secretEntityRepository, times(1)).existsById(maskedInput.getSecretId().toString()); verify(inputMaskStore, times(1)).getCachedInputMasks(maskedInput.getSecretId()); verify(secretEntityRepository, times(1)).save(secretEntityArgumentCaptor.capture()); @@ -182,7 +193,7 @@ void givenSuccessfulRequest_whenCreateObject_thenReturnSecretId() { SecretEntity actualSecretEntity = secretEntityArgumentCaptor.getValue(); assertEquals(maskedInput.getSecretId().toString(), actualSecretEntity.getSecretId()); List actualTags = TagEntity.setToTagList(actualSecretEntity.getTags()); - assertEquals(2, actualTags.size()); + assertEquals(3, actualTags.size()); // creation date, owner and test tag MatcherAssert.assertThat( actualTags, allOf(containsCreationDateTag(), hasItems(maskedInput.getTags().toArray(new Tag[0])))); @@ -382,27 +393,29 @@ void givenNoSecretShareWithGivenIdInDatabase_whenGetSecretShare_thenThrowNotFoun when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.empty()); NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.getSecretShare(testSecretId)); + assertThrows(NotFoundException.class, () -> storageService.getSecretShare(testSecretId, authorizedUserId)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @Test - void givenDataCannotBeRetrieved_whenGetSecretShare_thenThrowAmphoraServiceException() { + void givenDataCannotBeRetrieved_whenGetSecretShare_thenThrowAmphoraServiceException() throws CsOpaException { AmphoraServiceException expectedAse = new AmphoraServiceException("Expected this one"); - SecretEntity secretEntity = new SecretEntity(); + TagEntity tagEntity = TagEntity.fromTag(testTag); + SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), singleton(tagEntity)); + when(opaService.canReadSecret(authorizedUserId, setToTagList(secretEntity.getTags()))).thenReturn(true); when(secretEntityRepository.findById(testSecretId.toString())) .thenReturn(Optional.of(secretEntity)); when(secretShareDataStore.getSecretShareData(testSecretId)).thenThrow(expectedAse); assertThrows( AmphoraServiceException.class, - () -> storageService.getSecretShare(testSecretId), + () -> storageService.getSecretShare(testSecretId, authorizedUserId), expectedAse.getMessage()); } @Test - void givenSuccessfulRequest_whenGetSecretShare_thenReturnContent() { + void givenSuccessfulRequest_whenGetSecretShare_thenReturnContent() throws CsOpaException, UnauthorizedException { List expectedTags = singletonList(testTag); byte[] expectedData = RandomUtils.nextBytes(MpSpdzIntegrationUtils.SHARE_WIDTH); SecretEntity existingSecretEntity = @@ -413,9 +426,11 @@ void givenSuccessfulRequest_whenGetSecretShare_thenReturnContent() { when(secretShareDataStore.getSecretShareData( UUID.fromString(existingSecretEntity.getSecretId()))) .thenReturn(expectedData); + when(opaService.canReadSecret(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + .thenReturn(true); SecretShare actualSecretShare = - storageService.getSecretShare(UUID.fromString(existingSecretEntity.getSecretId())); + storageService.getSecretShare(UUID.fromString(existingSecretEntity.getSecretId()), authorizedUserId); assertEquals( UUID.fromString(existingSecretEntity.getSecretId()), actualSecretShare.getSecretId()); assertEquals(expectedTags, actualSecretShare.getTags()); @@ -423,32 +438,42 @@ void givenSuccessfulRequest_whenGetSecretShare_thenReturnContent() { } @Test - void givenNoSecretShareWithGivenIdInDatabase_whenDeleteObject_thenThrowNotFoundException() { - when(secretEntityRepository.deleteBySecretId(testSecretId.toString())).thenReturn(0L); - - NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.deleteSecret(testSecretId)); + void givenNoSecretShareWithGivenIdInDatabase_whenDeleteObject_thenThrowNotFoundException() { NotFoundException nfe = + assertThrows(NotFoundException.class, () -> storageService.deleteSecret(testSecretId, authorizedUserId)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @Test - void givenDeleteObjectDataFails_whenDeleteObject_thenThrowGivenException() { + void givenDeleteObjectDataFails_whenDeleteObject_thenThrowGivenException() throws CsOpaException { AmphoraServiceException expectedAse = new AmphoraServiceException("Expected this one"); + TagEntity tagEntity = TagEntity.fromTag(testTag); + SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), singleton(tagEntity)); + when(opaService.canDeleteSecret(authorizedUserId, setToTagList(secretEntity.getTags()))).thenReturn(true); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(secretEntity)); when(secretEntityRepository.deleteBySecretId(testSecretId.toString())).thenReturn(1L); when(secretShareDataStore.deleteSecretShareData(testSecretId)).thenThrow(expectedAse); assertThrows( AmphoraServiceException.class, - () -> storageService.deleteSecret(testSecretId), + () -> storageService.deleteSecret(testSecretId, authorizedUserId), expectedAse.getMessage()); } @Test - void givenSuccessfulRequest_whenDeleteObject_thenDeleteObjectAndData() { + void givenSuccessfulRequest_whenDeleteObject_thenDeleteObjectAndData() throws CsOpaException, UnauthorizedException { + TagEntity existingTagEntity = TagEntity.fromTag(testTag); + SecretEntity existingSecretEntity = + new SecretEntity(testSecretId.toString(), singleton(existingTagEntity)); + + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(existingSecretEntity)); when(secretEntityRepository.deleteBySecretId(testSecretId.toString())).thenReturn(1L); + when(opaService.canDeleteSecret(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + .thenReturn(true); - storageService.deleteSecret(testSecretId); + storageService.deleteSecret(testSecretId, authorizedUserId); verify(secretShareDataStore, times(1)).deleteSecretShareData(testSecretId); } @@ -457,7 +482,7 @@ void givenTagHasReservedKey_whenStoreTag_thenThrowIllegalArgumentException() { IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, - () -> storageService.storeTag(testSecretId, testTagReservedCreationDateKey)); + () -> storageService.storeTag(testSecretId, testTagReservedCreationDateKey, authorizedUserId)); assertEquals( String.format(IS_RESERVED_KEY_EXCEPTION_MSG, testTagReservedCreationDateKey.getKey()), iae.getMessage()); @@ -465,25 +490,24 @@ void givenTagHasReservedKey_whenStoreTag_thenThrowIllegalArgumentException() { @Test void givenNoSecretShareWithGivenIdInDatabase_whenStoreTag_thenThrowNotFoundException() { - when(secretEntityRepository.existsById(testSecretId.toString())).thenReturn(false); NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.storeTag(testSecretId, testTag)); + assertThrows(NotFoundException.class, () -> storageService.storeTag(testSecretId, testTag, authorizedUserId)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @Test - void givenObjectAlreadyHasTagWithGivenKey_whenStoreTag_thenThrowAlreadyExistsException() { + void givenObjectAlreadyHasTagWithGivenKey_whenStoreTag_thenThrowAlreadyExistsException() throws CsOpaException { SecretEntity existingSecretEntity = new SecretEntity().setSecretId(testSecretId.toString()); TagEntity existingTagEntity = TagEntity.fromTag(testTag); - when(secretEntityRepository.existsById(testSecretId.toString())).thenReturn(true); - when(secretEntityRepository.getById(existingSecretEntity.getSecretId())) - .thenReturn(existingSecretEntity); + when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.of(existingSecretEntity)); when(tagRepository.findBySecretAndKey(existingSecretEntity, testTag.getKey())) .thenReturn(Optional.of(existingTagEntity)); + when(opaService.canCreateTags(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + .thenReturn(true); AlreadyExistsException aee = assertThrows( - AlreadyExistsException.class, () -> storageService.storeTag(testSecretId, testTag)); + AlreadyExistsException.class, () -> storageService.storeTag(testSecretId, testTag, authorizedUserId)); assertEquals( String.format( TAG_WITH_KEY_EXISTS_FOR_SECRET_EXCEPTION_MSG, @@ -493,21 +517,21 @@ void givenObjectAlreadyHasTagWithGivenKey_whenStoreTag_thenThrowAlreadyExistsExc } @Test - void givenSuccessfulRequest_whenStoreTag_thenPersistTag() { + void givenSuccessfulRequest_whenStoreTag_thenPersistTag() throws CsOpaException, UnauthorizedException { SecretEntity existingSecretEntity = new SecretEntity().setSecretId(testSecretId.toString()); TagEntity expectedTagEntity = TagEntity.fromTag(testTag); ArgumentCaptor tagEntityArgumentCaptor = ArgumentCaptor.forClass(TagEntity.class); - when(secretEntityRepository.existsById(testSecretId.toString())).thenReturn(true); - when(secretEntityRepository.getById(existingSecretEntity.getSecretId())) - .thenReturn(existingSecretEntity); + when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.of(existingSecretEntity)); when(tagRepository.findBySecretAndKey(existingSecretEntity, testTag.getKey())) .thenReturn(Optional.empty()); when(tagRepository.save(any())).thenReturn(expectedTagEntity); + when(opaService.canCreateTags(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + .thenReturn(true); assertEquals( expectedTagEntity.getKey(), - storageService.storeTag(UUID.fromString(existingSecretEntity.getSecretId()), testTag)); + storageService.storeTag(UUID.fromString(existingSecretEntity.getSecretId()), testTag, authorizedUserId)); verify(tagRepository, times(1)).save(tagEntityArgumentCaptor.capture()); TagEntity actualTagEntity = tagEntityArgumentCaptor.getValue(); @@ -528,23 +552,22 @@ void givenListHasTagsWithSameKey_whenReplaceTags_thenThrowIllegalArgumentExcepti IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, - () -> storageService.replaceTags(testSecretId, invalidTagList)); + () -> storageService.replaceTags(testSecretId, invalidTagList, authorizedUserId)); assertEquals(TAGS_WITH_THE_SAME_KEY_DEFINED_EXCEPTION_MSG, iae.getMessage()); } @Test void givenNoSecretShareWithGivenIdInDatabase_whenReplaceTags_thenThrowNotFoundException() { - when(secretEntityRepository.existsById(testSecretId.toString())).thenReturn(false); List emptyTags = emptyList(); NotFoundException nfe = assertThrows( - NotFoundException.class, () -> storageService.replaceTags(testSecretId, emptyTags)); + NotFoundException.class, () -> storageService.replaceTags(testSecretId, emptyTags, authorizedUserId)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @Test - void givenListHasTagWithReservedKey_whenReplaceTags_thenReplaceByExistingTagAndPersist() { + void givenListHasTagWithReservedKey_whenReplaceTags_thenReplaceByExistingTagAndPersist() throws CsOpaException, UnauthorizedException { List tagListWithReservedKey = asList(testTag, testTagReservedCreationDateKey); TagEntity existingCreationTagEntity = TagEntity.fromTag( @@ -556,14 +579,14 @@ void givenListHasTagWithReservedKey_whenReplaceTags_thenReplaceByExistingTagAndP SecretEntity existingSecretEntity = new SecretEntity(testSecretId.toString(), emptySet()); ArgumentCaptor> tagEntitySetArgumentCaptor = ArgumentCaptor.forClass(Set.class); - when(secretEntityRepository.existsById(testSecretId.toString())).thenReturn(true); - when(secretEntityRepository.getById(existingSecretEntity.getSecretId())) - .thenReturn(existingSecretEntity); + when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.of(existingSecretEntity)); when(tagRepository.findBySecretAndKey(existingSecretEntity, existingCreationTagEntity.getKey())) .thenReturn(Optional.of(existingCreationTagEntity)); + when(opaService.canUpdateTags(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + .thenReturn(true); storageService.replaceTags( - UUID.fromString(existingSecretEntity.getSecretId()), tagListWithReservedKey); + UUID.fromString(existingSecretEntity.getSecretId()), tagListWithReservedKey, authorizedUserId); verify(tagRepository, times(1)).deleteBySecret(existingSecretEntity); verify(tagRepository, times(1)).saveAll(tagEntitySetArgumentCaptor.capture()); @@ -581,30 +604,31 @@ void givenNoSecretShareWithGivenIdInDatabase_whenRetrieveTags_thenThrowNotFoundE when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.empty()); NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.retrieveTags(testSecretId)); + assertThrows(NotFoundException.class, () -> storageService.retrieveTags(testSecretId, authorizedUserId)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @Test - void givenSuccessfulRequest_whenRetrieveTags_thenReturnContent() { + void givenSuccessfulRequest_whenRetrieveTags_thenReturnContent() throws CsOpaException, UnauthorizedException { List expectedTags = asList(testTag, testTag2); SecretEntity existingSecretEntity = new SecretEntity(testSecretId.toString(), TagEntity.setFromTagList(expectedTags)); when(secretEntityRepository.findById(testSecretId.toString())) .thenReturn(Optional.of(existingSecretEntity)); + when(opaService.canReadTags(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + .thenReturn(true); MatcherAssert.assertThat( - storageService.retrieveTags(UUID.fromString(existingSecretEntity.getSecretId())), + storageService.retrieveTags(UUID.fromString(existingSecretEntity.getSecretId()), authorizedUserId), containsInAnyOrder(expectedTags.toArray())); } @Test void givenNoSecretShareWithGivenIdInDatabase_whenRetrieveTag_thenThrowNotFoundException() { - when(secretEntityRepository.existsById(testSecretId.toString())).thenReturn(false); String key = testTag.getKey(); NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.retrieveTag(testSecretId, key)); + assertThrows(NotFoundException.class, () -> storageService.retrieveTag(testSecretId, key, authorizedUserId)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @@ -613,14 +637,10 @@ void givenNoSecretShareWithGivenIdInDatabase_whenRetrieveTag_thenThrowNotFoundEx void givenNoTagWithGivenKeyInDatabaseForGivenObject_whenRetrieveTag_thenThrowNotFoundException() { String key = testTag.getKey(); SecretEntity existingSecretEntity = new SecretEntity(testSecretId.toString(), emptySet()); - when(secretEntityRepository.existsById(testSecretId.toString())).thenReturn(true); - when(secretEntityRepository.getById(testSecretId.toString())).thenReturn(existingSecretEntity); - when(tagRepository.findBySecretAndKey(existingSecretEntity, testTag.getKey())) - .thenReturn(Optional.empty()); assertThrows( NotFoundException.class, - () -> storageService.retrieveTag(testSecretId, key), + () -> storageService.retrieveTag(testSecretId, key, authorizedUserId), String.format( NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, testTag.getKey(), @@ -628,16 +648,17 @@ void givenNoTagWithGivenKeyInDatabaseForGivenObject_whenRetrieveTag_thenThrowNot } @Test - void givenSuccessfulRequest_whenRetrieveTag_thenReturnContent() { + void givenSuccessfulRequest_whenRetrieveTag_thenReturnContent() throws CsOpaException, UnauthorizedException { TagEntity existingTagEntity = TagEntity.fromTag(testTag); SecretEntity existingSecretEntity = new SecretEntity(testSecretId.toString(), singleton(existingTagEntity)); - when(secretEntityRepository.existsById(testSecretId.toString())).thenReturn(true); - when(secretEntityRepository.getById(testSecretId.toString())).thenReturn(existingSecretEntity); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(existingSecretEntity)); + when(opaService.canReadTags(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + .thenReturn(true); when(tagRepository.findBySecretAndKey(existingSecretEntity, existingTagEntity.getKey())) .thenReturn(Optional.of(existingTagEntity)); - - assertEquals(testTag, storageService.retrieveTag(testSecretId, testTag.getKey())); + assertEquals(testTag, storageService.retrieveTag(testSecretId, testTag.getKey(), authorizedUserId)); } @Test @@ -645,7 +666,7 @@ void givenTagHasReservedKey_whenUpdateTag_thenThrowIllegalArgumentException() { IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, - () -> storageService.updateTag(testSecretId, testTagReservedCreationDateKey)); + () -> storageService.updateTag(testSecretId, testTagReservedCreationDateKey, authorizedUserId)); assertEquals( String.format(IS_RESERVED_KEY_EXCEPTION_MSG, testTagReservedCreationDateKey.getKey()), iae.getMessage()); @@ -653,11 +674,9 @@ void givenTagHasReservedKey_whenUpdateTag_thenThrowIllegalArgumentException() { @Test void givenNoSecretShareWithGivenIdInDatabase_whenUpdateTag_thenThrowNotFoundException() { - when(secretEntityRepository.existsById(testSecretId.toString())).thenReturn(false); - NotFoundException nfe = assertThrows( - NotFoundException.class, () -> storageService.updateTag(testSecretId, testTag)); + NotFoundException.class, () -> storageService.updateTag(testSecretId, testTag, authorizedUserId)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @@ -665,15 +684,10 @@ void givenNoSecretShareWithGivenIdInDatabase_whenUpdateTag_thenThrowNotFoundExce @Test void givenNoTagWithGivenKeyInDatabaseForGivenObject_whenUpdateTag_thenThrowNotFoundException() { SecretEntity existingSecretEntity = new SecretEntity(testSecretId.toString(), emptySet()); - when(secretEntityRepository.existsById(existingSecretEntity.getSecretId())).thenReturn(true); - when(secretEntityRepository.getById(existingSecretEntity.getSecretId())) - .thenReturn(existingSecretEntity); - when(tagRepository.findBySecretAndKey(existingSecretEntity, testTag.getKey())) - .thenReturn(Optional.empty()); assertThrows( NotFoundException.class, - () -> storageService.updateTag(testSecretId, testTag), + () -> storageService.updateTag(testSecretId, testTag, authorizedUserId), String.format( NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, testTag.getKey(), @@ -681,18 +695,19 @@ void givenNoTagWithGivenKeyInDatabaseForGivenObject_whenUpdateTag_thenThrowNotFo } @Test - void givenSuccessfulRequest_whenUpdateTag_thenUpdateTag() { + void givenSuccessfulRequest_whenUpdateTag_thenUpdateTag() throws CsOpaException, UnauthorizedException { Tag newTag = Tag.builder().key(testTag.getKey()).value("123").valueType(TagValueType.LONG).build(); TagEntity existingTagEntity = TagEntity.fromTag(testTag); SecretEntity existingSecretEntity = new SecretEntity(testSecretId.toString(), singleton(existingTagEntity)); - when(secretEntityRepository.existsById(testSecretId.toString())).thenReturn(true); - when(secretEntityRepository.getById(testSecretId.toString())).thenReturn(existingSecretEntity); + when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.of(existingSecretEntity)); when(tagRepository.findBySecretAndKey(existingSecretEntity, testTag.getKey())) .thenReturn(Optional.of(existingTagEntity)); + when(opaService.canUpdateTags(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + .thenReturn(true); - storageService.updateTag(UUID.fromString(existingSecretEntity.getSecretId()), newTag); + storageService.updateTag(UUID.fromString(existingSecretEntity.getSecretId()), newTag, authorizedUserId); ArgumentCaptor tagEntityArgumentCaptor = ArgumentCaptor.forClass(TagEntity.class); verify(tagRepository, times(1)).save(tagEntityArgumentCaptor.capture()); @@ -704,10 +719,11 @@ void givenSuccessfulRequest_whenUpdateTag_thenUpdateTag() { @Test void givenTagHasReservedKey_whenDeleteTag_thenThrowIllegalArgumentException() { String reservedKey = testTagReservedCreationDateKey.getKey(); + IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, - () -> storageService.deleteTag(testSecretId, reservedKey)); + () -> storageService.deleteTag(testSecretId, reservedKey, authorizedUserId)); assertEquals(String.format(IS_RESERVED_KEY_EXCEPTION_MSG, reservedKey), iae.getMessage()); } @@ -715,10 +731,8 @@ void givenTagHasReservedKey_whenDeleteTag_thenThrowIllegalArgumentException() { void givenNoSecretShareWithGivenIdInDatabase_whenDeleteTag_thenThrowNotFoundException() { String key = testTag.getKey(); - when(secretEntityRepository.existsById(testSecretId.toString())).thenReturn(false); - NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.deleteTag(testSecretId, key)); + assertThrows(NotFoundException.class, () -> storageService.deleteTag(testSecretId, key, authorizedUserId)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @@ -727,13 +741,10 @@ void givenNoSecretShareWithGivenIdInDatabase_whenDeleteTag_thenThrowNotFoundExce void givenNoTagWithGivenKeyInDatabaseForGivenObject_whenDeleteTag_thenThrowNotFoundException() { String key = testTag.getKey(); SecretEntity existingSecretEntity = new SecretEntity(testSecretId.toString(), emptySet()); - when(secretEntityRepository.existsById(testSecretId.toString())).thenReturn(true); - when(secretEntityRepository.getById(testSecretId.toString())).thenReturn(existingSecretEntity); - when(tagRepository.findBySecretAndKey(existingSecretEntity, key)).thenReturn(Optional.empty()); assertThrows( NotFoundException.class, - () -> storageService.deleteTag(testSecretId, key), + () -> storageService.deleteTag(testSecretId, key, authorizedUserId), String.format( NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, testTag.getKey(), @@ -741,18 +752,19 @@ void givenNoTagWithGivenKeyInDatabaseForGivenObject_whenDeleteTag_thenThrowNotFo } @Test - void givenSuccessfulRequest_whenDeleteTag_thenDelete() { + void givenSuccessfulRequest_whenDeleteTag_thenDelete() throws CsOpaException, UnauthorizedException { TagEntity tagEntityToDelete = TagEntity.fromTag(testTag); SecretEntity existingSecretEntity = - new SecretEntity(testSecretId.toString(), singleton(tagEntityToDelete)); - when(secretEntityRepository.existsById(testSecretId.toString())).thenReturn(true); - when(secretEntityRepository.getById(testSecretId.toString())).thenReturn(existingSecretEntity); + new SecretEntity(testSecretId.toString(), new HashSet<>(singleton(tagEntityToDelete))); + when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.of(existingSecretEntity)); when(tagRepository.findBySecretAndKey(existingSecretEntity, tagEntityToDelete.getKey())) .thenReturn(Optional.of(tagEntityToDelete)); + when(opaService.canDeleteTags(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + .thenReturn(true); + assertEquals(1, existingSecretEntity.getTags().size()); storageService.deleteTag( - UUID.fromString(existingSecretEntity.getSecretId()), tagEntityToDelete.getKey()); - - verify(tagRepository, times(1)).delete(tagEntityToDelete); + UUID.fromString(existingSecretEntity.getSecretId()), tagEntityToDelete.getKey(), authorizedUserId); + assertEquals(0, existingSecretEntity.getTags().size()); } } diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/IntraVcpControllerTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/IntraVcpControllerTest.java index 9a6a14e..cccdeda 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/IntraVcpControllerTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/IntraVcpControllerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 - for information on the respective copyright owner + * Copyright (c) 2023-2024 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 @@ -63,7 +63,7 @@ void givenSuccessfulRequest_whenDownloadSecretShare_thenReturnOkWithExpectedCont UUID secretShareId = UUID.fromString("3bcf8308-8f50-4d24-a37b-b0075bb5e779"); SecretShare expectedSecretShare = SecretShare.builder().secretId(secretShareId).build(); - when(storageService.getSecretShare(secretShareId)).thenReturn(expectedSecretShare); + when(storageService.getSecretShareAuthorized(secretShareId)).thenReturn(expectedSecretShare); ResponseEntity actualResponse = intraVcpController.downloadSecretShare(secretShareId); diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/MaskedInputControllerTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/MaskedInputControllerTest.java index 2cfabef..d119967 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/MaskedInputControllerTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/MaskedInputControllerTest.java @@ -1,43 +1,53 @@ /* - * Copyright (c) 2023 - for information on the respective copyright owner + * Copyright (c) 2023-2024 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 */ package io.carbynestack.amphora.service.rest; -import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.INTRA_VCP_OPERATIONS_SEGMENT; -import static io.carbynestack.amphora.service.util.ServletUriComponentsBuilderUtil.runInMockedHttpRequestContextForUri; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.when; - import io.carbynestack.amphora.common.MaskedInput; import io.carbynestack.amphora.common.MaskedInputData; +import io.carbynestack.amphora.service.exceptions.UnauthorizedException; +import io.carbynestack.amphora.service.opa.JwtReader; import io.carbynestack.amphora.service.persistence.metadata.StorageService; -import java.net.URI; -import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import java.net.URI; +import java.util.UUID; + +import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.INTRA_VCP_OPERATIONS_SEGMENT; +import static io.carbynestack.amphora.service.util.ServletUriComponentsBuilderUtil.runInMockedHttpRequestContextForUri; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class MaskedInputControllerTest { + private final String authHeader = "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5Njk0OTgyLWQzMTAtNDBkOC04ZDk4LTczOWI1ZGZjNWUyNiIsInR5cCI6IkpXVCJ9.eyJhbXIiOlsicGFzc3dvcmQiXSwiYXRfaGFzaCI6InowbjhudTNJQ19HaXN3bmFTWjgwZ2ciLCJhdWQiOlsiOGExYTIwNzUtMzY3Yi00ZGU1LTgyODgtMGMyNzQ1OTMzMmI3Il0sImF1dGhfdGltZSI6MTczMTUwMDQ0OSwiZXhwIjoxNzMxNTA0NDIyLCJpYXQiOjE3MzE1MDA4MjIsImlzcyI6Imh0dHA6Ly8xNzIuMTguMS4xMjguc3NsaXAuaW8vaWFtL29hdXRoIiwianRpIjoiZTlhMmQxYzQtZGViNy00MTgwLWE0M2YtN2QwNTZhYjNlNTk3Iiwibm9uY2UiOiJnV1JVZjhxTERaeDZpOFNhMXpMdm9IX2tSQ01OWll2WTE0WTFsLWNBU0tVIiwicmF0IjoxNzMxNTAwODIyLCJzaWQiOiJlNGVkOTc2Mi0yMmNlLTQyYzEtOTU3NC01MDVkYjAyMThhNDYiLCJzdWIiOiJhZmMwMTE3Zi1jOWNkLTRkOGMtYWNlZS1mYTE0MzNjYTBmZGQifQ.OACqa6WjpAeZbHR54b3p7saUk9plTdXlZsou41E-gfC7WxCG7ZEKfDPKXUky-r20oeIt1Ov3S2QL6Kefe5dTXEC6nhKGxeClg8ys56_FPcx_neI-p09_pSWOkMx7DHP65giaP7UubyyInVpE-2Eu1o6TpoheahNQfCahKDsJmJ-4Vvts3wA79UMfOI0WHO4vLaaG6DRAZQK_dv7ltw3p_WlncpaQAtHwY9iVhtdB3LtAI39EjplCoAF0c9uQO6W7KHWUlj24l2kc564bsJgZSrYvezw6b2-FY7YisVnicSyAORpeqhWEpLltH3D8I1NtHlSYMJhWuVZbBhAm7Iz6q1-W-Q9ttvdPchdwPSASFRkrjrdIyQf6MsFrItKzUxYck57EYL4GkyN9MWvMNxL1UTtkzGsFEczUVsJFm8OQpulYXIFZksmnPTBB0KuUUvEZ-xih8V1HsMRoHvbiCLaDJwjOFKzPevVggsSMysPKR52UAZJDZLTeHBnVCtQ3rro6T0RxNg94lXypz0AmfsGnoTF34u4FmMxzoeFZ9N5zmEpOnMRqLs7Sb3FnLL-IMitc9_2bsHuFbBJl8KbiGHBQihK5v5SIa292L7P9ChsxomWVhM29qHNFuXQMwFUr57hmveNh2Fz9mduZ5h2hLUuDf5xc6u9rSxy3_e3t_xBuUT4"; + private final String authorizedUserId ="afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; - @Mock private StorageService storageService; + private final StorageService storageService = mock(StorageService.class); + private final JwtReader jwtReader = mock(JwtReader.class); - @InjectMocks private MaskedInputController maskedInputController; + private final MaskedInputController maskedInputController = new MaskedInputController(storageService, jwtReader); + + @BeforeEach + public void setUp() throws UnauthorizedException { + when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); + } @Test void givenArgumentIsNull_whenUpload_thenThrowIllegalArgumentException() { IllegalArgumentException iae = - assertThrows(IllegalArgumentException.class, () -> maskedInputController.upload(null)); + assertThrows(IllegalArgumentException.class, () -> maskedInputController.upload(authHeader, null)); assertEquals("MaskedInput must not be null", iae.getMessage()); } @@ -48,7 +58,7 @@ void givenMaskedInputDataIsEmpty_whenUpload_thenThrowIllegalArgumentException() IllegalArgumentException iae = assertThrows( - IllegalArgumentException.class, () -> maskedInputController.upload(maskedInput)); + IllegalArgumentException.class, () -> maskedInputController.upload(authHeader, maskedInput)); assertEquals("MaskedInput data must not be empty", iae.getMessage()); } @@ -62,14 +72,19 @@ void givenSuccessfulRequest_whenUpload_thenReturnCreatedWithExpectedContent() { new MaskedInput( secretShareId, singletonList(MaskedInputData.of(new byte[16])), emptyList()); - when(storageService.createSecret(maskedInput)).thenReturn(secretShareId.toString()); + when(storageService.createSecret(maskedInput, authorizedUserId)).thenReturn(secretShareId.toString()); runInMockedHttpRequestContextForUri( expectedUri, () -> { - ResponseEntity actualResponse = maskedInputController.upload(maskedInput); + ResponseEntity actualResponse = null; + try { + actualResponse = maskedInputController.upload(authHeader, maskedInput); + } catch (UnauthorizedException e) { + fail("Unexpected exception thrown", e); + } - assertEquals(HttpStatus.CREATED, actualResponse.getStatusCode()); + assertEquals(HttpStatus.CREATED, actualResponse.getStatusCode()); assertEquals(expectedUri, actualResponse.getBody()); }); } diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/SecretShareControllerTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/SecretShareControllerTest.java index 58f7520..367d749 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/SecretShareControllerTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/SecretShareControllerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 - for information on the respective copyright owner + * Copyright (c) 2023-2024 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 @@ -7,39 +7,49 @@ package io.carbynestack.amphora.service.rest; -import static io.carbynestack.amphora.common.TagFilterOperator.EQUALS; -import static io.carbynestack.amphora.common.TagFilterOperator.LESS_THAN; -import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.CRITERIA_SEPARATOR; -import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import io.carbynestack.amphora.common.*; import io.carbynestack.amphora.service.calculation.OutputDeliveryService; +import io.carbynestack.amphora.service.exceptions.CsOpaException; +import io.carbynestack.amphora.service.exceptions.UnauthorizedException; +import io.carbynestack.amphora.service.opa.JwtReader; import io.carbynestack.amphora.service.persistence.metadata.StorageService; -import java.util.List; -import java.util.UUID; import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.*; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import java.util.List; +import java.util.UUID; + +import static io.carbynestack.amphora.common.TagFilterOperator.EQUALS; +import static io.carbynestack.amphora.common.TagFilterOperator.LESS_THAN; +import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.CRITERIA_SEPARATOR; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + @ExtendWith(MockitoExtension.class) class SecretShareControllerTest { + private final String authHeader = "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5Njk0OTgyLWQzMTAtNDBkOC04ZDk4LTczOWI1ZGZjNWUyNiIsInR5cCI6IkpXVCJ9.eyJhbXIiOlsicGFzc3dvcmQiXSwiYXRfaGFzaCI6InowbjhudTNJQ19HaXN3bmFTWjgwZ2ciLCJhdWQiOlsiOGExYTIwNzUtMzY3Yi00ZGU1LTgyODgtMGMyNzQ1OTMzMmI3Il0sImF1dGhfdGltZSI6MTczMTUwMDQ0OSwiZXhwIjoxNzMxNTA0NDIyLCJpYXQiOjE3MzE1MDA4MjIsImlzcyI6Imh0dHA6Ly8xNzIuMTguMS4xMjguc3NsaXAuaW8vaWFtL29hdXRoIiwianRpIjoiZTlhMmQxYzQtZGViNy00MTgwLWE0M2YtN2QwNTZhYjNlNTk3Iiwibm9uY2UiOiJnV1JVZjhxTERaeDZpOFNhMXpMdm9IX2tSQ01OWll2WTE0WTFsLWNBU0tVIiwicmF0IjoxNzMxNTAwODIyLCJzaWQiOiJlNGVkOTc2Mi0yMmNlLTQyYzEtOTU3NC01MDVkYjAyMThhNDYiLCJzdWIiOiJhZmMwMTE3Zi1jOWNkLTRkOGMtYWNlZS1mYTE0MzNjYTBmZGQifQ.OACqa6WjpAeZbHR54b3p7saUk9plTdXlZsou41E-gfC7WxCG7ZEKfDPKXUky-r20oeIt1Ov3S2QL6Kefe5dTXEC6nhKGxeClg8ys56_FPcx_neI-p09_pSWOkMx7DHP65giaP7UubyyInVpE-2Eu1o6TpoheahNQfCahKDsJmJ-4Vvts3wA79UMfOI0WHO4vLaaG6DRAZQK_dv7ltw3p_WlncpaQAtHwY9iVhtdB3LtAI39EjplCoAF0c9uQO6W7KHWUlj24l2kc564bsJgZSrYvezw6b2-FY7YisVnicSyAORpeqhWEpLltH3D8I1NtHlSYMJhWuVZbBhAm7Iz6q1-W-Q9ttvdPchdwPSASFRkrjrdIyQf6MsFrItKzUxYck57EYL4GkyN9MWvMNxL1UTtkzGsFEczUVsJFm8OQpulYXIFZksmnPTBB0KuUUvEZ-xih8V1HsMRoHvbiCLaDJwjOFKzPevVggsSMysPKR52UAZJDZLTeHBnVCtQ3rro6T0RxNg94lXypz0AmfsGnoTF34u4FmMxzoeFZ9N5zmEpOnMRqLs7Sb3FnLL-IMitc9_2bsHuFbBJl8KbiGHBQihK5v5SIa292L7P9ChsxomWVhM29qHNFuXQMwFUr57hmveNh2Fz9mduZ5h2hLUuDf5xc6u9rSxy3_e3t_xBuUT4"; + private final String authorizedUserId ="afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; - @Mock private OutputDeliveryService outputDeliveryService; + private final OutputDeliveryService outputDeliveryService = mock(OutputDeliveryService.class); + private final StorageService storageService = mock(StorageService.class); + private final JwtReader jwtReader = mock(JwtReader.class); - @Mock private StorageService storageService; + private final SecretShareController secretShareController = new SecretShareController(storageService, outputDeliveryService, jwtReader); - @InjectMocks private SecretShareController secretShareController; + @BeforeEach + void setUp() throws UnauthorizedException { + when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); + reset(storageService, outputDeliveryService); + } @SneakyThrows @Test @@ -149,22 +159,26 @@ void givenRequestIdArgumentIsNull_whenGetSecretShare_thenThrowIllegalArgumentExc IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, - () -> secretShareController.getSecretShare(secretId, null)); + () -> secretShareController.getSecretShare(authHeader, secretId, null)); assertEquals("Request identifier must not be omitted", iae.getMessage()); } @Test - void givenSuccessfulRequest_whenGetSecretShare_thenReturnOkAndExpectedContent() { + void givenSuccessfulRequest_whenGetSecretShare_thenReturnOkAndExpectedContent() throws CsOpaException, UnauthorizedException { UUID secretId = UUID.fromString("3bcf8308-8f50-4d24-a37b-b0075bb5e779"); UUID requestId = UUID.fromString("d6d0f4ff-df28-4c96-b7df-95170320eaee"); SecretShare secretShare = SecretShare.builder().secretId(requestId).build(); OutputDeliveryObject expectedOutputDeliveryObject = mock(OutputDeliveryObject.class); - when(storageService.getSecretShare(secretId)).thenReturn(secretShare); + when(storageService.getSecretShare(secretId, authorizedUserId)).thenReturn(secretShare); when(outputDeliveryService.computeOutputDeliveryObject(secretShare, requestId)) .thenReturn(expectedOutputDeliveryObject); + ResponseEntity responseEntity = - secretShareController.getSecretShare(secretId, requestId); + secretShareController.getSecretShare(authHeader, secretId, requestId); + verify(jwtReader, times(1)).extractUserIdFromAuthHeader(authHeader); + verify(storageService, times(1)).getSecretShare(secretId, authorizedUserId); + verify(outputDeliveryService, times(1)).computeOutputDeliveryObject(secretShare, requestId); assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); assertEquals( VerifiableSecretShare.of(secretShare, expectedOutputDeliveryObject), @@ -172,9 +186,9 @@ void givenSuccessfulRequest_whenGetSecretShare_thenReturnOkAndExpectedContent() } @Test - void givenSuccessfulRequest_whenDeleteSecretShare_thenReturnOk() { + void givenSuccessfulRequest_whenDeleteSecretShare_thenReturnOk() throws UnauthorizedException, CsOpaException { UUID secretId = UUID.fromString("3bcf8308-8f50-4d24-a37b-b0075bb5e779"); - ResponseEntity actualResponse = secretShareController.deleteSecretShare(secretId); + ResponseEntity actualResponse = secretShareController.deleteSecretShare(authHeader, secretId); assertEquals(HttpStatus.OK, actualResponse.getStatusCode()); } diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/TagsControllerTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/TagsControllerTest.java index 5320e3b..40e205f 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/TagsControllerTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/TagsControllerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 - for information on the respective copyright owner + * Copyright (c) 2023-2024 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 @@ -16,10 +16,16 @@ import io.carbynestack.amphora.common.Tag; import io.carbynestack.amphora.common.TagValueType; +import io.carbynestack.amphora.service.exceptions.CsOpaException; +import io.carbynestack.amphora.service.exceptions.UnauthorizedException; +import io.carbynestack.amphora.service.opa.JwtReader; import io.carbynestack.amphora.service.persistence.metadata.StorageService; import java.net.URI; import java.util.List; import java.util.UUID; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -30,95 +36,111 @@ @ExtendWith(MockitoExtension.class) class TagsControllerTest { + private final String authHeader = "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5Njk0OTgyLWQzMTAtNDBkOC04ZDk4LTczOWI1ZGZjNWUyNiIsInR5cCI6IkpXVCJ9.eyJhbXIiOlsicGFzc3dvcmQiXSwiYXRfaGFzaCI6InowbjhudTNJQ19HaXN3bmFTWjgwZ2ciLCJhdWQiOlsiOGExYTIwNzUtMzY3Yi00ZGU1LTgyODgtMGMyNzQ1OTMzMmI3Il0sImF1dGhfdGltZSI6MTczMTUwMDQ0OSwiZXhwIjoxNzMxNTA0NDIyLCJpYXQiOjE3MzE1MDA4MjIsImlzcyI6Imh0dHA6Ly8xNzIuMTguMS4xMjguc3NsaXAuaW8vaWFtL29hdXRoIiwianRpIjoiZTlhMmQxYzQtZGViNy00MTgwLWE0M2YtN2QwNTZhYjNlNTk3Iiwibm9uY2UiOiJnV1JVZjhxTERaeDZpOFNhMXpMdm9IX2tSQ01OWll2WTE0WTFsLWNBU0tVIiwicmF0IjoxNzMxNTAwODIyLCJzaWQiOiJlNGVkOTc2Mi0yMmNlLTQyYzEtOTU3NC01MDVkYjAyMThhNDYiLCJzdWIiOiJhZmMwMTE3Zi1jOWNkLTRkOGMtYWNlZS1mYTE0MzNjYTBmZGQifQ.OACqa6WjpAeZbHR54b3p7saUk9plTdXlZsou41E-gfC7WxCG7ZEKfDPKXUky-r20oeIt1Ov3S2QL6Kefe5dTXEC6nhKGxeClg8ys56_FPcx_neI-p09_pSWOkMx7DHP65giaP7UubyyInVpE-2Eu1o6TpoheahNQfCahKDsJmJ-4Vvts3wA79UMfOI0WHO4vLaaG6DRAZQK_dv7ltw3p_WlncpaQAtHwY9iVhtdB3LtAI39EjplCoAF0c9uQO6W7KHWUlj24l2kc564bsJgZSrYvezw6b2-FY7YisVnicSyAORpeqhWEpLltH3D8I1NtHlSYMJhWuVZbBhAm7Iz6q1-W-Q9ttvdPchdwPSASFRkrjrdIyQf6MsFrItKzUxYck57EYL4GkyN9MWvMNxL1UTtkzGsFEczUVsJFm8OQpulYXIFZksmnPTBB0KuUUvEZ-xih8V1HsMRoHvbiCLaDJwjOFKzPevVggsSMysPKR52UAZJDZLTeHBnVCtQ3rro6T0RxNg94lXypz0AmfsGnoTF34u4FmMxzoeFZ9N5zmEpOnMRqLs7Sb3FnLL-IMitc9_2bsHuFbBJl8KbiGHBQihK5v5SIa292L7P9ChsxomWVhM29qHNFuXQMwFUr57hmveNh2Fz9mduZ5h2hLUuDf5xc6u9rSxy3_e3t_xBuUT4"; + private final String authorizedUserId ="afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; private final UUID testSecretId = UUID.fromString("3bcf8308-8f50-4d24-a37b-b0075bb5e779"); private final Tag testTag = Tag.builder().key("key").value("value").valueType(TagValueType.STRING).build(); @Mock private StorageService storageService; + @Mock private JwtReader jwtReader; @InjectMocks private TagsController tagsController; @Test - void givenSuccessfulRequest_whenGetTags_thenReturnOkWithExpectedContent() { + void givenSuccessfulRequest_whenGetTags_thenReturnOkWithExpectedContent() throws UnauthorizedException, CsOpaException { List expectedList = singletonList(testTag); - when(storageService.retrieveTags(testSecretId)).thenReturn(expectedList); + when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); + when(storageService.retrieveTags(testSecretId, authorizedUserId)).thenReturn(expectedList); - ResponseEntity> actualResponse = tagsController.getTags(testSecretId); + ResponseEntity> actualResponse = tagsController.getTags(authHeader, testSecretId); assertEquals(HttpStatus.OK, actualResponse.getStatusCode()); assertEquals(expectedList, actualResponse.getBody()); } @Test - void givenTagIsNull_whenCreateTag_thenThrowIllegalArgumentException() { + void givenTagIsNull_whenCreateTag_thenThrowIllegalArgumentException() throws CsOpaException, UnauthorizedException { IllegalArgumentException iae = assertThrows( - IllegalArgumentException.class, () -> tagsController.createTag(testSecretId, null)); - verify(storageService, never()).storeTag(any(), any()); + IllegalArgumentException.class, () -> tagsController.createTag(authHeader, testSecretId, null)); + verify(storageService, never()).storeTag(any(), any(), eq(authorizedUserId)); assertEquals("Tag must not be empty", iae.getMessage()); } @Test - void givenSuccessfulRequest_whenCreateTag_thenReturnCreatedWithExpectedContent() { + void givenSuccessfulRequest_whenCreateTag_thenReturnCreatedWithExpectedContent() throws UnauthorizedException { URI expectedUri = URI.create( "https://amphora.carbynestack.io" + INTRA_VCP_OPERATIONS_SEGMENT + "/" + testSecretId); + + when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); + runInMockedHttpRequestContextForUri( expectedUri, () -> { - ResponseEntity actualResponse = tagsController.createTag(testSecretId, testTag); - verify(storageService, times(1)).storeTag(testSecretId, testTag); + ResponseEntity actualResponse = null; + try { + actualResponse = tagsController.createTag(authHeader, testSecretId, testTag); + verify(storageService, times(1)).storeTag(testSecretId, testTag, authorizedUserId); + } catch (UnauthorizedException | CsOpaException e) { + Assertions.fail("unexpected exception thrown: " + e); + } assertEquals(HttpStatus.CREATED, actualResponse.getStatusCode()); assertEquals(expectedUri, actualResponse.getBody()); }); } @Test - void givenTagsAreEmpty_whenUpdateTags_thenThrowIllegalArgumentException() { + void givenTagsAreEmpty_whenUpdateTags_thenThrowIllegalArgumentException() throws CsOpaException, UnauthorizedException { List emptyTags = emptyList(); IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, - () -> tagsController.updateTags(testSecretId, emptyTags)); - verify(storageService, never()).replaceTags(any(), any()); + () -> tagsController.updateTags(authHeader, testSecretId, emptyTags)); + verify(storageService, never()).replaceTags(any(), any(), eq(authorizedUserId)); assertEquals("At least one tag must be given.", iae.getMessage()); } @Test - void givenSuccessfulRequest_whenUpdateTags_thenReturnCreatedWithExpectedContent() { + void givenSuccessfulRequest_whenUpdateTags_thenReturnCreatedWithExpectedContent() throws UnauthorizedException, CsOpaException { List newTagList = singletonList(testTag); - ResponseEntity actualResponse = tagsController.updateTags(testSecretId, newTagList); - verify(storageService, times(1)).replaceTags(testSecretId, newTagList); + + when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); + + ResponseEntity actualResponse = tagsController.updateTags(authHeader, testSecretId, newTagList); + verify(storageService, times(1)).replaceTags(testSecretId, newTagList, authorizedUserId); assertEquals(HttpStatus.OK, actualResponse.getStatusCode()); } @Test - void givenSuccessfulRequest_whenGetTag_thenReturnOkWithExpectedContent() { - when(storageService.retrieveTag(testSecretId, testTag.getKey())).thenReturn(testTag); + void givenSuccessfulRequest_whenGetTag_thenReturnOkWithExpectedContent() throws UnauthorizedException, CsOpaException { + when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); + when(storageService.retrieveTag(testSecretId, testTag.getKey(), authorizedUserId)).thenReturn(testTag); - ResponseEntity actualResponse = tagsController.getTag(testSecretId, testTag.getKey()); + ResponseEntity actualResponse = tagsController.getTag(authHeader, testSecretId, testTag.getKey()); assertEquals(HttpStatus.OK, actualResponse.getStatusCode()); assertEquals(testTag, actualResponse.getBody()); } @Test - void givenTagIsNull_whenPutTag_thenTrowIllegalArgumentException() { + void givenTagIsNull_whenPutTag_thenTrowIllegalArgumentException() throws CsOpaException, UnauthorizedException { String key = testTag.getKey(); IllegalArgumentException iae = assertThrows( - IllegalArgumentException.class, () -> tagsController.putTag(testSecretId, key, null)); - verify(storageService, never()).updateTag(testSecretId, testTag); + IllegalArgumentException.class, () -> tagsController.putTag(authHeader, testSecretId, key, null)); + verify(storageService, never()).updateTag(testSecretId, testTag, authorizedUserId); assertEquals("Tag must not be empty", iae.getMessage()); } @Test - void givenTagConfigurationDoesNotMatchAddressedKey_whenPutTag_thenTrowIllegalArgumentException() { + void givenTagConfigurationDoesNotMatchAddressedKey_whenPutTag_thenTrowIllegalArgumentException() throws CsOpaException, UnauthorizedException { String nonMatchingKey = testTag.getKey() + "_different"; IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, - () -> tagsController.putTag(testSecretId, nonMatchingKey, testTag)); - verify(storageService, never()).updateTag(testSecretId, testTag); + () -> tagsController.putTag(authHeader, testSecretId, nonMatchingKey, testTag)); + verify(storageService, never()).updateTag(testSecretId, testTag, authorizedUserId); assertEquals( String.format( "The defined key and tag data do not match.\n%s <> %s", nonMatchingKey, testTag), @@ -126,17 +148,21 @@ void givenTagConfigurationDoesNotMatchAddressedKey_whenPutTag_thenTrowIllegalArg } @Test - void givenSuccessfulRequest_whenPutTag_thenReturnOk() { + void givenSuccessfulRequest_whenPutTag_thenReturnOk() throws UnauthorizedException, CsOpaException { + when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); + ResponseEntity actualResponse = - tagsController.putTag(testSecretId, testTag.getKey(), testTag); - verify(storageService, times(1)).updateTag(testSecretId, testTag); + tagsController.putTag(authHeader, testSecretId, testTag.getKey(), testTag); + verify(storageService, times(1)).updateTag(testSecretId, testTag, authorizedUserId); assertEquals(HttpStatus.OK, actualResponse.getStatusCode()); } @Test - void givenSuccessfulRequest_whenDeleteTag_thenReturnOk() { - ResponseEntity actualResponse = tagsController.deleteTag(testSecretId, testTag.getKey()); - verify(storageService, times(1)).deleteTag(testSecretId, testTag.getKey()); + void givenSuccessfulRequest_whenDeleteTag_thenReturnOk() throws UnauthorizedException, CsOpaException { + when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); + + ResponseEntity actualResponse = tagsController.deleteTag(authHeader, testSecretId, testTag.getKey()); + verify(storageService, times(1)).deleteTag(testSecretId, testTag.getKey(), authorizedUserId); assertEquals(HttpStatus.OK, actualResponse.getStatusCode()); } } diff --git a/amphora-service/src/test/resources/application-test.properties b/amphora-service/src/test/resources/application-test.properties index 6b2868a..dedf86d 100644 --- a/amphora-service/src/test/resources/application-test.properties +++ b/amphora-service/src/test/resources/application-test.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 - for information on the respective copyright owner +# Copyright (c) 2023-2024 - for information on the respective copyright owner # see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. # # SPDX-License-Identifier: Apache-2.0 @@ -13,7 +13,7 @@ spring.datasource.username=${POSTGRESQL_USERNAME} spring.datasource.password=${POSTGRESQL_PASSWORD} spring.jpa.database=postgresql spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.connection.autocommit=true +spring.jpa.hi.hibernate.connection.autocommit=true spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.hbm2ddl.auto=update @@ -21,6 +21,9 @@ logging.level.ROOT=INFO logging.level.io.carbynestack=DEBUG logging.level.org.springframework=INFO +carbynestack.auth.user-id-field-name=sub +carbynestack.opa.default-policy-package=default +carbynestack.opa.endpoint=http://opa.carbynestack.io carbynestack.amphora.vcPartners=http://amphora2.carbynestack.io carbynestack.amphora.minio.endpoint=${MINIO_ENDPOINT} carbynestack.amphora.minio.bucket=minio-amphora-test-bucket From 8e4318cdc08c83424ab155c3aacc8f95c76920a7 Mon Sep 17 00:00:00 2001 From: Sebastian Becker Date: Fri, 15 Nov 2024 08:36:57 +0100 Subject: [PATCH 3/8] feat(chart): update chart Signed-off-by: Sebastian Becker --- amphora-service/charts/amphora/templates/deployment.yaml | 6 ++++++ amphora-service/charts/amphora/values.yaml | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/amphora-service/charts/amphora/templates/deployment.yaml b/amphora-service/charts/amphora/templates/deployment.yaml index d100c22..7600c96 100644 --- a/amphora-service/charts/amphora/templates/deployment.yaml +++ b/amphora-service/charts/amphora/templates/deployment.yaml @@ -44,6 +44,12 @@ spec: value: {{ .Values.amphora.springActiveProfiles }} - name: CASTOR_SERVICE_URI value: "{{ .Values.amphora.castor.serviceUri }}" + - name: AMPHORA_USER_ID_FIELD_NAME + value: "{{ .Values.auth.userIdFieldName }}" + - name: AMPHORA_OPA_DEFAULT_POLICY_PACKAGE + value: "{{ .Values.opa.defaultPolicyPackage }}" + - name: AMPHORA_OPA_URL + value: "{{ .Values.opa.endpoint }}" - name: MAC_KEY value: "{{ .Values.spdz.macKey }}" - name: SPDZ_PRIME diff --git a/amphora-service/charts/amphora/values.yaml b/amphora-service/charts/amphora/values.yaml index 17d995b..9c49182 100644 --- a/amphora-service/charts/amphora/values.yaml +++ b/amphora-service/charts/amphora/values.yaml @@ -73,3 +73,10 @@ spdz: prime: "" r: "" rInv: "" + +auth: + userIdFieldName: "sub" + +opa: + defaultPolicyPackage: "default" + endpoint: "http://opa.default.svc.cluster.local:8081/" \ No newline at end of file From d301b969d460e9cf34db544aa5d76c934086cf25 Mon Sep 17 00:00:00 2001 From: Sebastian Becker Date: Mon, 18 Nov 2024 14:47:35 +0100 Subject: [PATCH 4/8] feat(service): update default policy package and improve tests Signed-off-by: Sebastian Becker --- amphora-service/charts/amphora/values.yaml | 2 +- .../amphora/service/opa/OpaClient.java | 11 +- .../amphora/service/opa/OpaRequestBody.java | 8 - .../persistence/metadata/StorageService.java | 17 -- .../amphora/service/opa/JwtReaderTest.java | 24 +++ .../amphora/service/opa/OpaResultTest.java | 31 ++++ .../metadata/StorageServiceTest.java | 147 ++++++++++++++++++ .../service/rest/TagsControllerTest.java | 5 +- 8 files changed, 213 insertions(+), 32 deletions(-) create mode 100644 amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaResultTest.java diff --git a/amphora-service/charts/amphora/values.yaml b/amphora-service/charts/amphora/values.yaml index 9c49182..556700b 100644 --- a/amphora-service/charts/amphora/values.yaml +++ b/amphora-service/charts/amphora/values.yaml @@ -78,5 +78,5 @@ auth: userIdFieldName: "sub" opa: - defaultPolicyPackage: "default" + defaultPolicyPackage: "carbynestack.def" endpoint: "http://opa.default.svc.cluster.local:8081/" \ No newline at end of file diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClient.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClient.java index 1fb544f..30ba7b9 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClient.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClient.java @@ -48,10 +48,13 @@ public boolean isAllowed(String policyPackage, String action, String subject, Li .tags(tags) .build(); try { - return csHttpClient.postForObject( - opaServiceUri.resolve(String.format("/v1/data/%s/%s", policyPackage, action)), - new OpaRequest(body), - OpaResult.class).isAllowed(); + return csHttpClient.postForObject(opaServiceUri.resolve( + String.format("/v1/data/%s/%s", + policyPackage.replace(".", "/"), + action)), + new OpaRequest(body), + OpaResult.class) + .isAllowed(); } catch (CsHttpClientException e) { log.error("Error occurred while evaluating OPA policy package: {}", e.getMessage()); } diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequestBody.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequestBody.java index 9992d96..ffd591b 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequestBody.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequestBody.java @@ -16,19 +16,11 @@ @Value public class OpaRequestBody { String subject; - long currentTime; List tags; @Builder public OpaRequestBody(String subject, List tags) { this.subject = subject; - this.currentTime = System.currentTimeMillis(); - this.tags = tags; - } - - OpaRequestBody(String subject, long currentTime, List tags) { - this.subject = subject; - this.currentTime = currentTime; this.tags = tags; } } diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java index cfa0445..98a5406 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java @@ -32,8 +32,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Isolation; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; @@ -535,20 +533,5 @@ public void deleteTag(UUID secretId, String key, String authorizedUserId) throws NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, key, secretId)))); -// SecretEntity secretEntityReference = secretEntityRepository.findById(secretId.toString()) -// .orElseThrow(() -> -// new NotFoundException( -// String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); -// if (!opaService.canDeleteTags(authorizedUserId, setToTagList(secretEntityReference.getTags()))) { -// throw new UnauthorizedException("User is not authorized to delete tags for this secret"); -// } -// TagEntity tagEntity = tagRepository.findBySecretAndKey(secretEntityReference, key) -// .orElseThrow(() -> -// new NotFoundException( -// String.format( -// NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, -// key, -// secretId))); -// tagRepository.delete(tagEntity); } } diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/JwtReaderTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/JwtReaderTest.java index fe9ddbb..7f90826 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/JwtReaderTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/JwtReaderTest.java @@ -15,6 +15,7 @@ import static org.junit.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; class JwtReaderTest { @@ -41,4 +42,27 @@ void givenNoTokenProvided_whenExtractSubject_thenThrowUnauthorizedException() th "Bearer header.%s.signature", Base64.toBase64String(invalidToken.getBytes(StandardCharsets.UTF_8))))); } + + @Test + void givenJwtOfInvalidFormat_whenExtractSubject_thenThrowUnauthorizedException() { + JwtReader jwtReader = new JwtReader("sub"); + assertThrows(UnauthorizedException.class, () -> + jwtReader.extractUserIdFromAuthHeader("Bearer invalid.jwt_missing_dot_token")); + } + + @Test + void givenJwtDataFieldOfInvalidFormat_whenExtractSubject_thenThrowUnauthorizedException() { + JwtReader jwtReader = new JwtReader("sub"); + String invalidDataJson = "{"; + assertThrows(UnauthorizedException.class, () -> + jwtReader.extractUserIdFromAuthHeader( + String.format("header.%s.signature", Base64.toBase64String(invalidDataJson.getBytes(StandardCharsets.UTF_8))))); + } + + @Test + void givenInvalidAuthHeader_whenExtractSubject_thenThrowUnauthorizedException() { + JwtReader jwtReader = new JwtReader("sub"); + assertThrows(UnauthorizedException.class, () -> + jwtReader.extractUserIdFromAuthHeader("invalid_auth_header missing Bearer prefix")); + } } \ No newline at end of file diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaResultTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaResultTest.java new file mode 100644 index 0000000..a87c03d --- /dev/null +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaResultTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 - for information on the respective copyright owner + * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.carbynestack.amphora.service.opa; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class OpaResultTest { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void givenResultIsTrue_whenParsed_thenIsAllowedShouldBeTrue() throws JsonProcessingException { + OpaResult result = objectMapper.readValue("{\"result\": true}", OpaResult.class); + assertTrue(result.isAllowed(), "isAllowed must be true"); + } + + @Test + void givenResultIsEmpty_whenParsed_thenIsAllowedShouldBeFalse() throws JsonProcessingException { + OpaResult result = objectMapper.readValue("{}", OpaResult.class); + assertFalse(result.isAllowed(), "isAllowed must be false"); + } +} \ No newline at end of file diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageServiceTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageServiceTest.java index a5617b2..4eceeea 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageServiceTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageServiceTest.java @@ -398,6 +398,18 @@ void givenNoSecretShareWithGivenIdInDatabase_whenGetSecretShare_thenThrowNotFoun String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } + @Test + void givenSubjectHasNoAccess_whenGetSecretShare_thenThrowUnauthorizedException() throws CsOpaException { + SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(secretEntity)); + when(opaService.canReadSecret(authorizedUserId, emptyList())).thenReturn(false); + + UnauthorizedException ue = + assertThrows(UnauthorizedException.class, () -> storageService.getSecretShare(testSecretId, authorizedUserId)); + assertEquals("User is not authorized to read this secret", ue.getMessage()); + } + @Test void givenDataCannotBeRetrieved_whenGetSecretShare_thenThrowAmphoraServiceException() throws CsOpaException { AmphoraServiceException expectedAse = new AmphoraServiceException("Expected this one"); @@ -437,6 +449,52 @@ void givenSuccessfulRequest_whenGetSecretShare_thenReturnContent() throws CsOpaE assertEquals(expectedData, actualSecretShare.getData()); } + @Test + void givenNoSecretShareWithGivenIdInDatabase_whenGetSecretShareAuthorized_thenThrowNotFoundException() { + when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.empty()); + + NotFoundException nfe = + assertThrows(NotFoundException.class, () -> storageService.getSecretShareAuthorized(testSecretId)); + assertEquals( + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); + } + + @Test + void givenDataCannotBeRetrieved_whenGetSecretShareAuthorized_thenThrowAmphoraServiceException() { + AmphoraServiceException expectedAse = new AmphoraServiceException("Expected this one"); + TagEntity tagEntity = TagEntity.fromTag(testTag); + SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), singleton(tagEntity)); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(secretEntity)); + when(secretShareDataStore.getSecretShareData(testSecretId)).thenThrow(expectedAse); + + assertThrows( + AmphoraServiceException.class, + () -> storageService.getSecretShareAuthorized(testSecretId), + expectedAse.getMessage()); + } + + @Test + void givenSuccessfulRequest_whenGetSecretShareAuthorized_thenReturnContent() { + List expectedTags = singletonList(testTag); + byte[] expectedData = RandomUtils.nextBytes(MpSpdzIntegrationUtils.SHARE_WIDTH); + SecretEntity existingSecretEntity = + new SecretEntity(testSecretId.toString(), TagEntity.setFromTagList(expectedTags)); + + when(secretEntityRepository.findById(existingSecretEntity.getSecretId())) + .thenReturn(Optional.of(existingSecretEntity)); + when(secretShareDataStore.getSecretShareData( + UUID.fromString(existingSecretEntity.getSecretId()))) + .thenReturn(expectedData); + + SecretShare actualSecretShare = + storageService.getSecretShareAuthorized(UUID.fromString(existingSecretEntity.getSecretId())); + assertEquals( + UUID.fromString(existingSecretEntity.getSecretId()), actualSecretShare.getSecretId()); + assertEquals(expectedTags, actualSecretShare.getTags()); + assertEquals(expectedData, actualSecretShare.getData()); + } + @Test void givenNoSecretShareWithGivenIdInDatabase_whenDeleteObject_thenThrowNotFoundException() { NotFoundException nfe = assertThrows(NotFoundException.class, () -> storageService.deleteSecret(testSecretId, authorizedUserId)); @@ -444,6 +502,17 @@ void givenNoSecretShareWithGivenIdInDatabase_whenDeleteObject_thenThrowNotFoundE String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } + @Test + void givenSubjectHasNoAccess_whenDeleteObject_thenThrowUnauthorizedException() throws CsOpaException { + SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(secretEntity)); + + UnauthorizedException ue = + assertThrows(UnauthorizedException.class, () -> storageService.deleteSecret(testSecretId, authorizedUserId)); + assertEquals("User is not authorized to delete this secret", ue.getMessage()); + } + @Test void givenDeleteObjectDataFails_whenDeleteObject_thenThrowGivenException() throws CsOpaException { AmphoraServiceException expectedAse = new AmphoraServiceException("Expected this one"); @@ -516,6 +585,18 @@ void givenObjectAlreadyHasTagWithGivenKey_whenStoreTag_thenThrowAlreadyExistsExc aee.getMessage()); } + @Test + void givenSubjectHasNoAccess_whenStoreTag_thenThrowUnauthorizedException() throws CsOpaException { + SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(secretEntity)); + + UnauthorizedException ue = + assertThrows(UnauthorizedException.class, () -> + storageService.storeTag(UUID.fromString(secretEntity.getSecretId()), testTag, authorizedUserId)); + assertEquals("User is not authorized to create tags for this secret", ue.getMessage()); + } + @Test void givenSuccessfulRequest_whenStoreTag_thenPersistTag() throws CsOpaException, UnauthorizedException { SecretEntity existingSecretEntity = new SecretEntity().setSecretId(testSecretId.toString()); @@ -566,6 +647,20 @@ void givenNoSecretShareWithGivenIdInDatabase_whenReplaceTags_thenThrowNotFoundEx String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } + @Test + void givenSubjectHasNoAccess_whenReplaceTags_thenThrowUnauthorizedException() throws CsOpaException { + List tagListWithReservedKey = asList(testTag, testTagReservedCreationDateKey); + SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(secretEntity)); + + UnauthorizedException ue = + assertThrows(UnauthorizedException.class, () -> + storageService.replaceTags( + UUID.fromString(secretEntity.getSecretId()), tagListWithReservedKey, authorizedUserId)); + assertEquals("User is not authorized to update tags for this secret", ue.getMessage()); + } + @Test void givenListHasTagWithReservedKey_whenReplaceTags_thenReplaceByExistingTagAndPersist() throws CsOpaException, UnauthorizedException { List tagListWithReservedKey = asList(testTag, testTagReservedCreationDateKey); @@ -609,6 +704,18 @@ void givenNoSecretShareWithGivenIdInDatabase_whenRetrieveTags_thenThrowNotFoundE String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } + @Test + void givenSubjectHasNoAccess_whenRetrieveTags_thenThrowUnauthorizedException() throws CsOpaException { + SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(secretEntity)); + + UnauthorizedException ue = + assertThrows(UnauthorizedException.class, () -> + storageService.retrieveTags(UUID.fromString(secretEntity.getSecretId()), authorizedUserId)); + assertEquals("User is not authorized to read tags for this secret", ue.getMessage()); + } + @Test void givenSuccessfulRequest_whenRetrieveTags_thenReturnContent() throws CsOpaException, UnauthorizedException { List expectedTags = asList(testTag, testTag2); @@ -647,6 +754,18 @@ void givenNoTagWithGivenKeyInDatabaseForGivenObject_whenRetrieveTag_thenThrowNot existingSecretEntity.getSecretId())); } + @Test + void givenSubjectHasNoAccess_whenRetrieveTag_thenThrowUnauthorizedException() throws CsOpaException { + SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(secretEntity)); + + UnauthorizedException ue = + assertThrows(UnauthorizedException.class, () -> + storageService.retrieveTag(testSecretId, testTag.getKey(), authorizedUserId)); + assertEquals("User is not authorized to read tags for this secret", ue.getMessage()); + } + @Test void givenSuccessfulRequest_whenRetrieveTag_thenReturnContent() throws CsOpaException, UnauthorizedException { TagEntity existingTagEntity = TagEntity.fromTag(testTag); @@ -694,6 +813,20 @@ void givenNoTagWithGivenKeyInDatabaseForGivenObject_whenUpdateTag_thenThrowNotFo existingSecretEntity.getSecretId())); } + @Test + void givenSubjectHasNoAccess_whenUpdateTag_thenThrowUnauthorizedException() throws CsOpaException { + Tag newTag = + Tag.builder().key(testTag.getKey()).value("123").valueType(TagValueType.LONG).build(); + SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(secretEntity)); + + UnauthorizedException ue = + assertThrows(UnauthorizedException.class, () -> + storageService.updateTag(UUID.fromString(secretEntity.getSecretId()), newTag, authorizedUserId)); + assertEquals("User is not authorized to update tags for this secret", ue.getMessage()); + } + @Test void givenSuccessfulRequest_whenUpdateTag_thenUpdateTag() throws CsOpaException, UnauthorizedException { Tag newTag = @@ -751,6 +884,20 @@ void givenNoTagWithGivenKeyInDatabaseForGivenObject_whenDeleteTag_thenThrowNotFo existingSecretEntity.getSecretId())); } + @Test + void givenSubjectHasNoAccess_whenDeleteTag_thenThrowUnauthorizedException() throws CsOpaException { + TagEntity tagEntityToDelete = TagEntity.fromTag(testTag); + SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(secretEntity)); + + UnauthorizedException ue = + assertThrows(UnauthorizedException.class, () -> + storageService.deleteTag( + UUID.fromString(secretEntity.getSecretId()), tagEntityToDelete.getKey(), authorizedUserId)); + assertEquals("User is not authorized to delete tags for this secret", ue.getMessage()); + } + @Test void givenSuccessfulRequest_whenDeleteTag_thenDelete() throws CsOpaException, UnauthorizedException { TagEntity tagEntityToDelete = TagEntity.fromTag(testTag); diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/TagsControllerTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/TagsControllerTest.java index 40e205f..1c74aac 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/TagsControllerTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/TagsControllerTest.java @@ -69,7 +69,7 @@ void givenTagIsNull_whenCreateTag_thenThrowIllegalArgumentException() throws CsO } @Test - void givenSuccessfulRequest_whenCreateTag_thenReturnCreatedWithExpectedContent() throws UnauthorizedException { + void givenSuccessfulRequest_whenCreateTag_thenReturnCreatedWithExpectedContent() throws UnauthorizedException, CsOpaException { URI expectedUri = URI.create( "https://amphora.carbynestack.io" + INTRA_VCP_OPERATIONS_SEGMENT + "/" + testSecretId); @@ -82,13 +82,14 @@ void givenSuccessfulRequest_whenCreateTag_thenReturnCreatedWithExpectedContent() ResponseEntity actualResponse = null; try { actualResponse = tagsController.createTag(authHeader, testSecretId, testTag); - verify(storageService, times(1)).storeTag(testSecretId, testTag, authorizedUserId); } catch (UnauthorizedException | CsOpaException e) { Assertions.fail("unexpected exception thrown: " + e); } assertEquals(HttpStatus.CREATED, actualResponse.getStatusCode()); assertEquals(expectedUri, actualResponse.getBody()); }); + + verify(storageService, times(1)).storeTag(testSecretId, testTag, authorizedUserId); } @Test From 7423e94eea14207225c1fa15caa38afec4b1125c Mon Sep 17 00:00:00 2001 From: Sebastian Becker Date: Mon, 18 Nov 2024 15:49:26 +0100 Subject: [PATCH 5/8] feat(service)!: add authorization for using secrets Signed-off-by: Sebastian Becker --- .../amphora/service/opa/OpaService.java | 14 ++ .../persistence/metadata/StorageService.java | 20 +-- .../service/rest/IntraVcpController.java | 13 +- .../amphora/service/opa/OpaServiceTest.java | 28 +++- .../metadata/StorageServiceTest.java | 131 ++++++++++-------- .../service/rest/IntraVcpControllerTest.java | 26 ++-- 6 files changed, 145 insertions(+), 87 deletions(-) diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaService.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaService.java index 2ee6770..d8c5618 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaService.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaService.java @@ -20,6 +20,7 @@ public class OpaService { public static final String OWNER_TAG_KEY = "owner"; static final String READ_SECRET_ACTION_NAME = "read"; + static final String USE_SECRET_ACTION_NAME = "use"; static final String DELETE_SECRET_ACTION_NAME = "delete"; static final String CREATE_TAG_ACTION_NAME = "tag/create"; static final String READ_TAG_ACTION_NAME = "tag/read"; @@ -46,6 +47,19 @@ public boolean canReadSecret(String subject, List tags) throws CsOpaExcepti return isAllowed(subject, READ_SECRET_ACTION_NAME, tags); } + /** + * Check if the subject can use the secret described by the given tags evaluating the OPA policy package. + * The policy package is extracted from the tags if present. If not present, the default policy package is used. + * + * @param subject The subject attempting to use the secret. + * @param tags The tags describing the referenced secret. + * @return True if the subject can use the secret, false otherwise. + * @throws CsOpaException If an error occurred while evaluating the policy. + */ + public boolean canUseSecret(String subject, List tags) throws CsOpaException { + return isAllowed(subject, USE_SECRET_ACTION_NAME, tags); + } + /** * Check if the subject can delete the secret described by the given tags evaluating the OPA policy package. * The policy package is extracted from the tags if present. If not present, the default policy package is used. diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java index 98a5406..4997b2d 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java @@ -220,16 +220,20 @@ public Page getSecretList(List tagFilters, Sort sort) { * if no secret with the given id exists. * @throws AmphoraServiceException if an {@link SecretShare} exists but could not be retrieved. * @throws NotFoundException if no {@link SecretShare} with the given id exists + * @throws UnauthorizedException if the requesting program is not authorized to access the {@link SecretShare} */ @Transactional(readOnly = true) - public SecretShare getSecretShareAuthorized(UUID secretId) { - return secretEntityRepository - .findById(secretId.toString()) - .map(this::getSecretShareForEntity) - .orElseThrow( - () -> - new NotFoundException( - String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + public SecretShare useSecretShare(UUID secretId, String programId) throws UnauthorizedException, CsOpaException { + SecretEntity secretEntity = secretEntityRepository + .findById(secretId.toString()) + .orElseThrow( + () -> + new NotFoundException( + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + if (!opaService.canUseSecret(programId, setToTagList(secretEntity.getTags()))) { + throw new UnauthorizedException("Requesting program is not authorized to access the secret"); + } + return getSecretShareForEntity(secretEntity); } /** diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/IntraVcpController.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/IntraVcpController.java index f8e6f8e..7120826 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/IntraVcpController.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/IntraVcpController.java @@ -10,7 +10,9 @@ import io.carbynestack.amphora.common.Tag; import io.carbynestack.amphora.common.exceptions.AmphoraServiceException; import io.carbynestack.amphora.service.exceptions.AlreadyExistsException; +import io.carbynestack.amphora.service.exceptions.CsOpaException; import io.carbynestack.amphora.service.exceptions.NotFoundException; +import io.carbynestack.amphora.service.exceptions.UnauthorizedException; import io.carbynestack.amphora.service.persistence.metadata.StorageService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -26,6 +28,7 @@ import java.util.UUID; import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.*; +import static org.springframework.util.Assert.notNull; @Slf4j @RestController @@ -62,12 +65,18 @@ public ResponseEntity uploadSecretShare(@RequestBody SecretShare secretShar * Retrieves an {@link SecretShare} with a given id. * * @param secretId id of the {@link SecretShare} to retrieve + * @param programId id of the Program requesting the {@link SecretShare} * @return {@link HttpStatus#OK} with the {@link SecretShare} if successful. * @throws AmphoraServiceException if an {@link SecretShare} exists but could not be retrieved. * @throws NotFoundException if no {@link SecretShare} with the given id exists + * @throws UnauthorizedException if the requesting Program is not authorized to access the {@link SecretShare} + * @throws CsOpaException if an error occurred while evaluating the OPA policy */ @GetMapping(path = "/{" + SECRET_ID_PARAMETER + "}") - public ResponseEntity downloadSecretShare(@PathVariable UUID secretId) { - return new ResponseEntity<>(storageService.getSecretShareAuthorized(secretId), HttpStatus.OK); + public ResponseEntity downloadSecretShare(@PathVariable UUID secretId, + @RequestParam String programId) throws UnauthorizedException, CsOpaException { + + notNull(programId, "ProgramId must not be null"); + return new ResponseEntity<>(storageService.useSecretShare(secretId, programId), HttpStatus.OK); } } diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaServiceTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaServiceTest.java index 49f536b..6dad392 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaServiceTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaServiceTest.java @@ -20,13 +20,14 @@ import java.util.List; import static io.carbynestack.amphora.service.opa.OpaService.READ_SECRET_ACTION_NAME; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static io.carbynestack.amphora.service.opa.OpaService.USE_SECRET_ACTION_NAME; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) -public class OpaServiceTest { +class OpaServiceTest { private static final String POLICY_PACKAGE = "play"; private static final String DEFAULT_POLICY_PACKAGE = "default"; @@ -54,7 +55,7 @@ public void setUp() { } @Test - public void givenPolicyDefinedInTag_whenIsAllowed_thenUsePolicyPackageProvided() throws CsOpaException { + void givenPolicyDefinedInTag_whenIsAllowed_thenUsePolicyPackageProvided() throws CsOpaException { ArgumentCaptor packageCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor subjectCaptor = ArgumentCaptor.forClass(String.class); @@ -68,7 +69,7 @@ public void givenPolicyDefinedInTag_whenIsAllowed_thenUsePolicyPackageProvided() testTags.add(POLICY_TAG); boolean result = service.isAllowed(SUBJECT, READ_SECRET_ACTION_NAME, testTags); - assertTrue("must be allowed", result); + assertTrue(result, "must be allowed"); String actualPackage = packageCaptor.getValue(); assertEquals(POLICY_TAG.getValue(), actualPackage); String actualAction = actionCaptor.getValue(); @@ -90,7 +91,7 @@ public void givenNoPolicyDefinedInTag_whenIsAllowed_thenUseDefaultPolicyPackage( OpaService service = new OpaService(opaClientMock); boolean result = service.isAllowed(READ_SECRET_ACTION_NAME, SUBJECT, TAGS); - assertTrue("must be allowed", result); + assertTrue(result, "must be allowed"); String actualPackage = packageCaptor.getValue(); assertEquals(DEFAULT_POLICY_PACKAGE, actualPackage); List actualTags = tagsCaptor.getValue(); @@ -112,6 +113,21 @@ public void whenCanReadSecret_thenUseProperAction() throws CsOpaException { } + @Test + public void whenCanUseSecret_thenUseProperAction() throws CsOpaException { + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); + when(opaClientMock.isAllowed( + any(), actionCaptor.capture(), any(), any())) + .thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + service.canUseSecret(SUBJECT, TAGS); + + String actualAction = actionCaptor.getValue(); + assertEquals(USE_SECRET_ACTION_NAME, actualAction); + + } + @Test public void whenCanDeleteSecret_thenUseProperAction() throws CsOpaException { ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageServiceTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageServiceTest.java index 4eceeea..66b7d4f 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageServiceTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageServiceTest.java @@ -16,7 +16,6 @@ import io.carbynestack.amphora.service.exceptions.CsOpaException; import io.carbynestack.amphora.service.exceptions.NotFoundException; import io.carbynestack.amphora.service.exceptions.UnauthorizedException; -import io.carbynestack.amphora.service.opa.OpaClient; import io.carbynestack.amphora.service.opa.OpaService; import io.carbynestack.amphora.service.persistence.cache.InputMaskCachingService; import io.carbynestack.amphora.service.persistence.datastore.SecretShareDataStore; @@ -25,7 +24,6 @@ import io.carbynestack.mpspdz.integration.MpSpdzIntegrationUtils; import org.apache.commons.lang3.RandomUtils; import org.hamcrest.MatcherAssert; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -37,7 +35,6 @@ import java.util.*; import java.util.stream.Collectors; -import static io.carbynestack.amphora.service.opa.OpaService.OWNER_TAG_KEY; import static io.carbynestack.amphora.service.persistence.metadata.StorageService.*; import static io.carbynestack.amphora.service.persistence.metadata.TagEntity.setToTagList; import static io.carbynestack.amphora.service.util.CreationDateTagMatchers.containsCreationDateTag; @@ -58,7 +55,7 @@ class StorageServiceTest { private final Tag testTag2 = Tag.builder().key("SUPER_KEY").value("MY#SUPER,VALUE").build(); private final Tag testTagReservedCreationDateKey = Tag.builder().key(StorageService.RESERVED_TAG_KEYS.get(0)).value("MY#SUPER,VALUE").build(); - private final String authorizedUserId ="afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; + private final String authorizedSubject ="afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; @Mock private SecretEntityRepository secretEntityRepository; @Mock private InputMaskCachingService inputMaskStore; @@ -86,7 +83,7 @@ void givenIdIsAlreadyInUse_whenCreateSecret_thenThrowAlreadyExistsException() { AlreadyExistsException aee = assertThrows( AlreadyExistsException.class, () -> - storageService.createSecret(testMaskedInput, authorizedUserId )); + storageService.createSecret(testMaskedInput, authorizedSubject)); assertEquals(SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, aee.getMessage()); } @@ -108,7 +105,7 @@ void givenMaskedInputHasTagsWithSameKey_whenCreateObject_thenThrowIllegalArgumen IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, - () -> storageService.createSecret(maskedInputDuplicateTagKeys, authorizedUserId)); + () -> storageService.createSecret(maskedInputDuplicateTagKeys, authorizedSubject)); assertEquals(TAGS_WITH_THE_SAME_KEY_DEFINED_EXCEPTION_MSG, iae.getMessage()); } @@ -140,7 +137,7 @@ void givenMaskedInputWithReservedKey_whenCreateObject_thenReplaceReservedKey() { assertThrows( RuntimeException.class, - () -> storageService.createSecret(maskedInput, authorizedUserId), + () -> storageService.createSecret(maskedInput, authorizedSubject), expectedAbortTestException.getMessage()); SecretEntity capturedSecretEntity = secretEntityArgumentCaptor.getValue(); List actualTags = TagEntity.setToTagList(capturedSecretEntity.getTags()); @@ -183,7 +180,7 @@ void givenSuccessfulRequest_whenCreateObject_thenReturnSecretId() { when(secretEntityRepository.save(secretEntityArgumentCaptor.capture())) .thenReturn(persistedSecretEntity); - assertEquals(maskedInput.getSecretId().toString(), storageService.createSecret(maskedInput, authorizedUserId)); + assertEquals(maskedInput.getSecretId().toString(), storageService.createSecret(maskedInput, authorizedSubject)); verify(secretEntityRepository, times(1)).existsById(maskedInput.getSecretId().toString()); verify(inputMaskStore, times(1)).getCachedInputMasks(maskedInput.getSecretId()); verify(secretEntityRepository, times(1)).save(secretEntityArgumentCaptor.capture()); @@ -393,7 +390,7 @@ void givenNoSecretShareWithGivenIdInDatabase_whenGetSecretShare_thenThrowNotFoun when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.empty()); NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.getSecretShare(testSecretId, authorizedUserId)); + assertThrows(NotFoundException.class, () -> storageService.getSecretShare(testSecretId, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @@ -403,10 +400,10 @@ void givenSubjectHasNoAccess_whenGetSecretShare_thenThrowUnauthorizedException() SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); when(secretEntityRepository.findById(testSecretId.toString())) .thenReturn(Optional.of(secretEntity)); - when(opaService.canReadSecret(authorizedUserId, emptyList())).thenReturn(false); + when(opaService.canReadSecret(authorizedSubject, emptyList())).thenReturn(false); UnauthorizedException ue = - assertThrows(UnauthorizedException.class, () -> storageService.getSecretShare(testSecretId, authorizedUserId)); + assertThrows(UnauthorizedException.class, () -> storageService.getSecretShare(testSecretId, authorizedSubject)); assertEquals("User is not authorized to read this secret", ue.getMessage()); } @@ -415,14 +412,14 @@ void givenDataCannotBeRetrieved_whenGetSecretShare_thenThrowAmphoraServiceExcept AmphoraServiceException expectedAse = new AmphoraServiceException("Expected this one"); TagEntity tagEntity = TagEntity.fromTag(testTag); SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), singleton(tagEntity)); - when(opaService.canReadSecret(authorizedUserId, setToTagList(secretEntity.getTags()))).thenReturn(true); + when(opaService.canReadSecret(authorizedSubject, setToTagList(secretEntity.getTags()))).thenReturn(true); when(secretEntityRepository.findById(testSecretId.toString())) .thenReturn(Optional.of(secretEntity)); when(secretShareDataStore.getSecretShareData(testSecretId)).thenThrow(expectedAse); assertThrows( AmphoraServiceException.class, - () -> storageService.getSecretShare(testSecretId, authorizedUserId), + () -> storageService.getSecretShare(testSecretId, authorizedSubject), expectedAse.getMessage()); } @@ -438,11 +435,11 @@ void givenSuccessfulRequest_whenGetSecretShare_thenReturnContent() throws CsOpaE when(secretShareDataStore.getSecretShareData( UUID.fromString(existingSecretEntity.getSecretId()))) .thenReturn(expectedData); - when(opaService.canReadSecret(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + when(opaService.canReadSecret(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) .thenReturn(true); SecretShare actualSecretShare = - storageService.getSecretShare(UUID.fromString(existingSecretEntity.getSecretId()), authorizedUserId); + storageService.getSecretShare(UUID.fromString(existingSecretEntity.getSecretId()), authorizedSubject); assertEquals( UUID.fromString(existingSecretEntity.getSecretId()), actualSecretShare.getSecretId()); assertEquals(expectedTags, actualSecretShare.getTags()); @@ -450,32 +447,44 @@ void givenSuccessfulRequest_whenGetSecretShare_thenReturnContent() throws CsOpaE } @Test - void givenNoSecretShareWithGivenIdInDatabase_whenGetSecretShareAuthorized_thenThrowNotFoundException() { + void givenNoSecretShareWithGivenIdInDatabase_whenUseSecretShare_thenThrowNotFoundException() { when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.empty()); NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.getSecretShareAuthorized(testSecretId)); + assertThrows(NotFoundException.class, () -> storageService.useSecretShare(testSecretId, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @Test - void givenDataCannotBeRetrieved_whenGetSecretShareAuthorized_thenThrowAmphoraServiceException() { + void givenSubjectIsNotAuthorized_whenUseSecretShare_thenThrowUnauthorizedException() { + TagEntity tagEntity = TagEntity.fromTag(testTag); + SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), singleton(tagEntity)); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(secretEntity)); + + assertThrows(UnauthorizedException.class, () -> storageService.useSecretShare(testSecretId, authorizedSubject)); + } + + @Test + void givenDataCannotBeRetrieved_whenUseSecretShare_thenThrowAmphoraServiceException() throws CsOpaException { AmphoraServiceException expectedAse = new AmphoraServiceException("Expected this one"); TagEntity tagEntity = TagEntity.fromTag(testTag); SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), singleton(tagEntity)); when(secretEntityRepository.findById(testSecretId.toString())) .thenReturn(Optional.of(secretEntity)); + when(opaService.canUseSecret(authorizedSubject, setToTagList(secretEntity.getTags()))) + .thenReturn(true); when(secretShareDataStore.getSecretShareData(testSecretId)).thenThrow(expectedAse); assertThrows( AmphoraServiceException.class, - () -> storageService.getSecretShareAuthorized(testSecretId), + () -> storageService.useSecretShare(testSecretId, authorizedSubject), expectedAse.getMessage()); } @Test - void givenSuccessfulRequest_whenGetSecretShareAuthorized_thenReturnContent() { + void givenSuccessfulRequest_whenUseSecretShare_thenReturnContent() throws CsOpaException, UnauthorizedException { List expectedTags = singletonList(testTag); byte[] expectedData = RandomUtils.nextBytes(MpSpdzIntegrationUtils.SHARE_WIDTH); SecretEntity existingSecretEntity = @@ -483,12 +492,14 @@ void givenSuccessfulRequest_whenGetSecretShareAuthorized_thenReturnContent() { when(secretEntityRepository.findById(existingSecretEntity.getSecretId())) .thenReturn(Optional.of(existingSecretEntity)); + when(opaService.canUseSecret(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) + .thenReturn(true); when(secretShareDataStore.getSecretShareData( UUID.fromString(existingSecretEntity.getSecretId()))) .thenReturn(expectedData); SecretShare actualSecretShare = - storageService.getSecretShareAuthorized(UUID.fromString(existingSecretEntity.getSecretId())); + storageService.useSecretShare(UUID.fromString(existingSecretEntity.getSecretId()), authorizedSubject); assertEquals( UUID.fromString(existingSecretEntity.getSecretId()), actualSecretShare.getSecretId()); assertEquals(expectedTags, actualSecretShare.getTags()); @@ -497,7 +508,7 @@ void givenSuccessfulRequest_whenGetSecretShareAuthorized_thenReturnContent() { @Test void givenNoSecretShareWithGivenIdInDatabase_whenDeleteObject_thenThrowNotFoundException() { NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.deleteSecret(testSecretId, authorizedUserId)); + assertThrows(NotFoundException.class, () -> storageService.deleteSecret(testSecretId, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @@ -509,7 +520,7 @@ void givenSubjectHasNoAccess_whenDeleteObject_thenThrowUnauthorizedException() t .thenReturn(Optional.of(secretEntity)); UnauthorizedException ue = - assertThrows(UnauthorizedException.class, () -> storageService.deleteSecret(testSecretId, authorizedUserId)); + assertThrows(UnauthorizedException.class, () -> storageService.deleteSecret(testSecretId, authorizedSubject)); assertEquals("User is not authorized to delete this secret", ue.getMessage()); } @@ -518,7 +529,7 @@ void givenDeleteObjectDataFails_whenDeleteObject_thenThrowGivenException() throw AmphoraServiceException expectedAse = new AmphoraServiceException("Expected this one"); TagEntity tagEntity = TagEntity.fromTag(testTag); SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), singleton(tagEntity)); - when(opaService.canDeleteSecret(authorizedUserId, setToTagList(secretEntity.getTags()))).thenReturn(true); + when(opaService.canDeleteSecret(authorizedSubject, setToTagList(secretEntity.getTags()))).thenReturn(true); when(secretEntityRepository.findById(testSecretId.toString())) .thenReturn(Optional.of(secretEntity)); when(secretEntityRepository.deleteBySecretId(testSecretId.toString())).thenReturn(1L); @@ -526,7 +537,7 @@ void givenDeleteObjectDataFails_whenDeleteObject_thenThrowGivenException() throw assertThrows( AmphoraServiceException.class, - () -> storageService.deleteSecret(testSecretId, authorizedUserId), + () -> storageService.deleteSecret(testSecretId, authorizedSubject), expectedAse.getMessage()); } @@ -539,10 +550,10 @@ void givenSuccessfulRequest_whenDeleteObject_thenDeleteObjectAndData() throws Cs when(secretEntityRepository.findById(testSecretId.toString())) .thenReturn(Optional.of(existingSecretEntity)); when(secretEntityRepository.deleteBySecretId(testSecretId.toString())).thenReturn(1L); - when(opaService.canDeleteSecret(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + when(opaService.canDeleteSecret(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) .thenReturn(true); - storageService.deleteSecret(testSecretId, authorizedUserId); + storageService.deleteSecret(testSecretId, authorizedSubject); verify(secretShareDataStore, times(1)).deleteSecretShareData(testSecretId); } @@ -551,7 +562,7 @@ void givenTagHasReservedKey_whenStoreTag_thenThrowIllegalArgumentException() { IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, - () -> storageService.storeTag(testSecretId, testTagReservedCreationDateKey, authorizedUserId)); + () -> storageService.storeTag(testSecretId, testTagReservedCreationDateKey, authorizedSubject)); assertEquals( String.format(IS_RESERVED_KEY_EXCEPTION_MSG, testTagReservedCreationDateKey.getKey()), iae.getMessage()); @@ -560,7 +571,7 @@ void givenTagHasReservedKey_whenStoreTag_thenThrowIllegalArgumentException() { @Test void givenNoSecretShareWithGivenIdInDatabase_whenStoreTag_thenThrowNotFoundException() { NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.storeTag(testSecretId, testTag, authorizedUserId)); + assertThrows(NotFoundException.class, () -> storageService.storeTag(testSecretId, testTag, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @@ -572,11 +583,11 @@ void givenObjectAlreadyHasTagWithGivenKey_whenStoreTag_thenThrowAlreadyExistsExc when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.of(existingSecretEntity)); when(tagRepository.findBySecretAndKey(existingSecretEntity, testTag.getKey())) .thenReturn(Optional.of(existingTagEntity)); - when(opaService.canCreateTags(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + when(opaService.canCreateTags(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) .thenReturn(true); AlreadyExistsException aee = assertThrows( - AlreadyExistsException.class, () -> storageService.storeTag(testSecretId, testTag, authorizedUserId)); + AlreadyExistsException.class, () -> storageService.storeTag(testSecretId, testTag, authorizedSubject)); assertEquals( String.format( TAG_WITH_KEY_EXISTS_FOR_SECRET_EXCEPTION_MSG, @@ -593,7 +604,7 @@ void givenSubjectHasNoAccess_whenStoreTag_thenThrowUnauthorizedException() throw UnauthorizedException ue = assertThrows(UnauthorizedException.class, () -> - storageService.storeTag(UUID.fromString(secretEntity.getSecretId()), testTag, authorizedUserId)); + storageService.storeTag(UUID.fromString(secretEntity.getSecretId()), testTag, authorizedSubject)); assertEquals("User is not authorized to create tags for this secret", ue.getMessage()); } @@ -607,12 +618,12 @@ void givenSuccessfulRequest_whenStoreTag_thenPersistTag() throws CsOpaException, when(tagRepository.findBySecretAndKey(existingSecretEntity, testTag.getKey())) .thenReturn(Optional.empty()); when(tagRepository.save(any())).thenReturn(expectedTagEntity); - when(opaService.canCreateTags(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + when(opaService.canCreateTags(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) .thenReturn(true); assertEquals( expectedTagEntity.getKey(), - storageService.storeTag(UUID.fromString(existingSecretEntity.getSecretId()), testTag, authorizedUserId)); + storageService.storeTag(UUID.fromString(existingSecretEntity.getSecretId()), testTag, authorizedSubject)); verify(tagRepository, times(1)).save(tagEntityArgumentCaptor.capture()); TagEntity actualTagEntity = tagEntityArgumentCaptor.getValue(); @@ -633,7 +644,7 @@ void givenListHasTagsWithSameKey_whenReplaceTags_thenThrowIllegalArgumentExcepti IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, - () -> storageService.replaceTags(testSecretId, invalidTagList, authorizedUserId)); + () -> storageService.replaceTags(testSecretId, invalidTagList, authorizedSubject)); assertEquals(TAGS_WITH_THE_SAME_KEY_DEFINED_EXCEPTION_MSG, iae.getMessage()); } @@ -642,7 +653,7 @@ void givenNoSecretShareWithGivenIdInDatabase_whenReplaceTags_thenThrowNotFoundEx List emptyTags = emptyList(); NotFoundException nfe = assertThrows( - NotFoundException.class, () -> storageService.replaceTags(testSecretId, emptyTags, authorizedUserId)); + NotFoundException.class, () -> storageService.replaceTags(testSecretId, emptyTags, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @@ -657,7 +668,7 @@ void givenSubjectHasNoAccess_whenReplaceTags_thenThrowUnauthorizedException() th UnauthorizedException ue = assertThrows(UnauthorizedException.class, () -> storageService.replaceTags( - UUID.fromString(secretEntity.getSecretId()), tagListWithReservedKey, authorizedUserId)); + UUID.fromString(secretEntity.getSecretId()), tagListWithReservedKey, authorizedSubject)); assertEquals("User is not authorized to update tags for this secret", ue.getMessage()); } @@ -677,11 +688,11 @@ void givenListHasTagWithReservedKey_whenReplaceTags_thenReplaceByExistingTagAndP when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.of(existingSecretEntity)); when(tagRepository.findBySecretAndKey(existingSecretEntity, existingCreationTagEntity.getKey())) .thenReturn(Optional.of(existingCreationTagEntity)); - when(opaService.canUpdateTags(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + when(opaService.canUpdateTags(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) .thenReturn(true); storageService.replaceTags( - UUID.fromString(existingSecretEntity.getSecretId()), tagListWithReservedKey, authorizedUserId); + UUID.fromString(existingSecretEntity.getSecretId()), tagListWithReservedKey, authorizedSubject); verify(tagRepository, times(1)).deleteBySecret(existingSecretEntity); verify(tagRepository, times(1)).saveAll(tagEntitySetArgumentCaptor.capture()); @@ -699,7 +710,7 @@ void givenNoSecretShareWithGivenIdInDatabase_whenRetrieveTags_thenThrowNotFoundE when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.empty()); NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.retrieveTags(testSecretId, authorizedUserId)); + assertThrows(NotFoundException.class, () -> storageService.retrieveTags(testSecretId, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @@ -712,7 +723,7 @@ void givenSubjectHasNoAccess_whenRetrieveTags_thenThrowUnauthorizedException() t UnauthorizedException ue = assertThrows(UnauthorizedException.class, () -> - storageService.retrieveTags(UUID.fromString(secretEntity.getSecretId()), authorizedUserId)); + storageService.retrieveTags(UUID.fromString(secretEntity.getSecretId()), authorizedSubject)); assertEquals("User is not authorized to read tags for this secret", ue.getMessage()); } @@ -723,11 +734,11 @@ void givenSuccessfulRequest_whenRetrieveTags_thenReturnContent() throws CsOpaExc new SecretEntity(testSecretId.toString(), TagEntity.setFromTagList(expectedTags)); when(secretEntityRepository.findById(testSecretId.toString())) .thenReturn(Optional.of(existingSecretEntity)); - when(opaService.canReadTags(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + when(opaService.canReadTags(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) .thenReturn(true); MatcherAssert.assertThat( - storageService.retrieveTags(UUID.fromString(existingSecretEntity.getSecretId()), authorizedUserId), + storageService.retrieveTags(UUID.fromString(existingSecretEntity.getSecretId()), authorizedSubject), containsInAnyOrder(expectedTags.toArray())); } @@ -735,7 +746,7 @@ void givenSuccessfulRequest_whenRetrieveTags_thenReturnContent() throws CsOpaExc void givenNoSecretShareWithGivenIdInDatabase_whenRetrieveTag_thenThrowNotFoundException() { String key = testTag.getKey(); NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.retrieveTag(testSecretId, key, authorizedUserId)); + assertThrows(NotFoundException.class, () -> storageService.retrieveTag(testSecretId, key, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @@ -747,7 +758,7 @@ void givenNoTagWithGivenKeyInDatabaseForGivenObject_whenRetrieveTag_thenThrowNot assertThrows( NotFoundException.class, - () -> storageService.retrieveTag(testSecretId, key, authorizedUserId), + () -> storageService.retrieveTag(testSecretId, key, authorizedSubject), String.format( NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, testTag.getKey(), @@ -762,7 +773,7 @@ void givenSubjectHasNoAccess_whenRetrieveTag_thenThrowUnauthorizedException() th UnauthorizedException ue = assertThrows(UnauthorizedException.class, () -> - storageService.retrieveTag(testSecretId, testTag.getKey(), authorizedUserId)); + storageService.retrieveTag(testSecretId, testTag.getKey(), authorizedSubject)); assertEquals("User is not authorized to read tags for this secret", ue.getMessage()); } @@ -773,11 +784,11 @@ void givenSuccessfulRequest_whenRetrieveTag_thenReturnContent() throws CsOpaExce new SecretEntity(testSecretId.toString(), singleton(existingTagEntity)); when(secretEntityRepository.findById(testSecretId.toString())) .thenReturn(Optional.of(existingSecretEntity)); - when(opaService.canReadTags(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + when(opaService.canReadTags(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) .thenReturn(true); when(tagRepository.findBySecretAndKey(existingSecretEntity, existingTagEntity.getKey())) .thenReturn(Optional.of(existingTagEntity)); - assertEquals(testTag, storageService.retrieveTag(testSecretId, testTag.getKey(), authorizedUserId)); + assertEquals(testTag, storageService.retrieveTag(testSecretId, testTag.getKey(), authorizedSubject)); } @Test @@ -785,7 +796,7 @@ void givenTagHasReservedKey_whenUpdateTag_thenThrowIllegalArgumentException() { IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, - () -> storageService.updateTag(testSecretId, testTagReservedCreationDateKey, authorizedUserId)); + () -> storageService.updateTag(testSecretId, testTagReservedCreationDateKey, authorizedSubject)); assertEquals( String.format(IS_RESERVED_KEY_EXCEPTION_MSG, testTagReservedCreationDateKey.getKey()), iae.getMessage()); @@ -795,7 +806,7 @@ void givenTagHasReservedKey_whenUpdateTag_thenThrowIllegalArgumentException() { void givenNoSecretShareWithGivenIdInDatabase_whenUpdateTag_thenThrowNotFoundException() { NotFoundException nfe = assertThrows( - NotFoundException.class, () -> storageService.updateTag(testSecretId, testTag, authorizedUserId)); + NotFoundException.class, () -> storageService.updateTag(testSecretId, testTag, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @@ -806,7 +817,7 @@ void givenNoTagWithGivenKeyInDatabaseForGivenObject_whenUpdateTag_thenThrowNotFo assertThrows( NotFoundException.class, - () -> storageService.updateTag(testSecretId, testTag, authorizedUserId), + () -> storageService.updateTag(testSecretId, testTag, authorizedSubject), String.format( NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, testTag.getKey(), @@ -823,7 +834,7 @@ void givenSubjectHasNoAccess_whenUpdateTag_thenThrowUnauthorizedException() thro UnauthorizedException ue = assertThrows(UnauthorizedException.class, () -> - storageService.updateTag(UUID.fromString(secretEntity.getSecretId()), newTag, authorizedUserId)); + storageService.updateTag(UUID.fromString(secretEntity.getSecretId()), newTag, authorizedSubject)); assertEquals("User is not authorized to update tags for this secret", ue.getMessage()); } @@ -837,10 +848,10 @@ void givenSuccessfulRequest_whenUpdateTag_thenUpdateTag() throws CsOpaException, when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.of(existingSecretEntity)); when(tagRepository.findBySecretAndKey(existingSecretEntity, testTag.getKey())) .thenReturn(Optional.of(existingTagEntity)); - when(opaService.canUpdateTags(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + when(opaService.canUpdateTags(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) .thenReturn(true); - storageService.updateTag(UUID.fromString(existingSecretEntity.getSecretId()), newTag, authorizedUserId); + storageService.updateTag(UUID.fromString(existingSecretEntity.getSecretId()), newTag, authorizedSubject); ArgumentCaptor tagEntityArgumentCaptor = ArgumentCaptor.forClass(TagEntity.class); verify(tagRepository, times(1)).save(tagEntityArgumentCaptor.capture()); @@ -856,7 +867,7 @@ void givenTagHasReservedKey_whenDeleteTag_thenThrowIllegalArgumentException() { IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, - () -> storageService.deleteTag(testSecretId, reservedKey, authorizedUserId)); + () -> storageService.deleteTag(testSecretId, reservedKey, authorizedSubject)); assertEquals(String.format(IS_RESERVED_KEY_EXCEPTION_MSG, reservedKey), iae.getMessage()); } @@ -865,7 +876,7 @@ void givenNoSecretShareWithGivenIdInDatabase_whenDeleteTag_thenThrowNotFoundExce String key = testTag.getKey(); NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.deleteTag(testSecretId, key, authorizedUserId)); + assertThrows(NotFoundException.class, () -> storageService.deleteTag(testSecretId, key, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @@ -877,7 +888,7 @@ void givenNoTagWithGivenKeyInDatabaseForGivenObject_whenDeleteTag_thenThrowNotFo assertThrows( NotFoundException.class, - () -> storageService.deleteTag(testSecretId, key, authorizedUserId), + () -> storageService.deleteTag(testSecretId, key, authorizedSubject), String.format( NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, testTag.getKey(), @@ -894,7 +905,7 @@ void givenSubjectHasNoAccess_whenDeleteTag_thenThrowUnauthorizedException() thro UnauthorizedException ue = assertThrows(UnauthorizedException.class, () -> storageService.deleteTag( - UUID.fromString(secretEntity.getSecretId()), tagEntityToDelete.getKey(), authorizedUserId)); + UUID.fromString(secretEntity.getSecretId()), tagEntityToDelete.getKey(), authorizedSubject)); assertEquals("User is not authorized to delete tags for this secret", ue.getMessage()); } @@ -906,12 +917,12 @@ void givenSuccessfulRequest_whenDeleteTag_thenDelete() throws CsOpaException, Un when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.of(existingSecretEntity)); when(tagRepository.findBySecretAndKey(existingSecretEntity, tagEntityToDelete.getKey())) .thenReturn(Optional.of(tagEntityToDelete)); - when(opaService.canDeleteTags(authorizedUserId, setToTagList(existingSecretEntity.getTags()))) + when(opaService.canDeleteTags(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) .thenReturn(true); assertEquals(1, existingSecretEntity.getTags().size()); storageService.deleteTag( - UUID.fromString(existingSecretEntity.getSecretId()), tagEntityToDelete.getKey(), authorizedUserId); + UUID.fromString(existingSecretEntity.getSecretId()), tagEntityToDelete.getKey(), authorizedSubject); assertEquals(0, existingSecretEntity.getTags().size()); } } diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/IntraVcpControllerTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/IntraVcpControllerTest.java index cccdeda..272fac8 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/IntraVcpControllerTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/IntraVcpControllerTest.java @@ -6,16 +6,10 @@ */ package io.carbynestack.amphora.service.rest; -import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.INTRA_VCP_OPERATIONS_SEGMENT; -import static io.carbynestack.amphora.service.util.ServletUriComponentsBuilderUtil.runInMockedHttpRequestContextForUri; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.when; - import io.carbynestack.amphora.common.SecretShare; +import io.carbynestack.amphora.service.exceptions.CsOpaException; +import io.carbynestack.amphora.service.exceptions.UnauthorizedException; import io.carbynestack.amphora.service.persistence.metadata.StorageService; -import java.net.URI; -import java.util.UUID; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -24,8 +18,18 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import java.net.URI; +import java.util.UUID; + +import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.INTRA_VCP_OPERATIONS_SEGMENT; +import static io.carbynestack.amphora.service.util.ServletUriComponentsBuilderUtil.runInMockedHttpRequestContextForUri; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class IntraVcpControllerTest { + private final String authorizedSubjectId ="afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; @Mock private StorageService storageService; @@ -59,14 +63,14 @@ void givenSuccessfulRequest_whenUploadSecretShare_thenReturnCreatedWithExpectedC } @Test - void givenSuccessfulRequest_whenDownloadSecretShare_thenReturnOkWithExpectedContent() { + void givenSuccessfulRequest_whenDownloadSecretShare_thenReturnOkWithExpectedContent() throws CsOpaException, UnauthorizedException { UUID secretShareId = UUID.fromString("3bcf8308-8f50-4d24-a37b-b0075bb5e779"); SecretShare expectedSecretShare = SecretShare.builder().secretId(secretShareId).build(); - when(storageService.getSecretShareAuthorized(secretShareId)).thenReturn(expectedSecretShare); + when(storageService.useSecretShare(secretShareId, authorizedSubjectId)).thenReturn(expectedSecretShare); ResponseEntity actualResponse = - intraVcpController.downloadSecretShare(secretShareId); + intraVcpController.downloadSecretShare(secretShareId, authorizedSubjectId); assertEquals(HttpStatus.OK, actualResponse.getStatusCode()); assertEquals(expectedSecretShare, actualResponse.getBody()); From fa07348277b9d54ea6773c601ec04b5e7494dfca Mon Sep 17 00:00:00 2001 From: Sebastian Becker Date: Tue, 19 Nov 2024 07:27:11 +0100 Subject: [PATCH 6/8] chor(service): update default policy package Signed-off-by: Sebastian Becker --- amphora-service/src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amphora-service/src/main/resources/application.properties b/amphora-service/src/main/resources/application.properties index cc9b74e..bf2065f 100644 --- a/amphora-service/src/main/resources/application.properties +++ b/amphora-service/src/main/resources/application.properties @@ -22,7 +22,7 @@ server.servlet.context-path=/ management.endpoints.web.exposure.include=info,health,prometheus carbynestack.auth.user-id-field-name=${AMPHORA_USER_ID_FIELD_NAME:sub} -carbynestack.opa.default-policy-package=${AMPHORA_OPA_DEFAULT_POLICY_PACKAGE:default} +carbynestack.opa.default-policy-package=${AMPHORA_OPA_DEFAULT_POLICY_PACKAGE:carbynestack.def} carbynestack.opa.endpoint=${AMPHORA_OPA_URL:http://opa.carbynestack.io} carbynestack.spdz.mac-key=${MAC_KEY} carbynestack.spdz.prime=${SPDZ_PRIME} From d7b55c2064bd4d2865403270430fe0f81b1d13f3 Mon Sep 17 00:00:00 2001 From: Sebastian Becker Date: Thu, 21 Nov 2024 21:01:00 +0100 Subject: [PATCH 7/8] feat(service): preserve owner for computed secrets Signed-off-by: Sebastian Becker --- .../service/persistence/metadata/StorageService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java index 4997b2d..6d257dc 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java @@ -135,7 +135,12 @@ public String storeSecretShare(SecretShare secretShare) { if (hasDuplicateKey(secretShare.getTags())) { throw new IllegalArgumentException(TAGS_WITH_THE_SAME_KEY_DEFINED_EXCEPTION_MSG); } - return persistSecretShare(secretShare, Collections.emptyList()); + List reservedTags = new ArrayList<>(); + secretShare.getTags().stream() + .filter(t -> t.getKey().equals(OWNER_TAG_KEY)) + .findFirst() + .ifPresent(reservedTags::add); + return persistSecretShare(secretShare, reservedTags); } /** From f113daa4b6ba33ebcf2aab600703a77edcdb4bac Mon Sep 17 00:00:00 2001 From: Sebastian Becker Date: Tue, 17 Dec 2024 09:01:48 +0100 Subject: [PATCH 8/8] chore(service): address PR findings Signed-off-by: Sebastian Becker --- amphora-service/README.md | 2 +- amphora-service/charts/amphora/values.yaml | 2 +- .../service/calculation/SecretShareUtil.java | 3 +- .../service/config/AuthProperties.java | 3 +- .../amphora/service/config/OpaConfig.java | 9 +- .../service/exceptions/CsOpaException.java | 6 +- .../exceptions/UnauthorizedException.java | 6 +- .../amphora/service/opa/JwtReader.java | 83 ++-- .../amphora/service/opa/OpaClient.java | 92 ++--- .../amphora/service/opa/OpaClientRequest.java | 42 +- .../amphora/service/opa/OpaRequest.java | 9 +- .../amphora/service/opa/OpaRequestBody.java | 17 +- .../amphora/service/opa/OpaResult.java | 9 +- .../amphora/service/opa/OpaService.java | 246 +++++------ .../persistence/metadata/StorageService.java | 390 ++++++++++-------- .../service/rest/IntraVcpController.java | 19 +- .../service/rest/MaskedInputController.java | 14 +- .../rest/RestControllerExceptionHandler.java | 6 +- .../service/rest/SecretShareController.java | 18 +- .../amphora/service/rest/TagsController.java | 72 ++-- .../service/AmphoraServiceSystemTest.java | 18 +- .../calculation/SecretShareUtilTest.java | 8 +- .../amphora/service/opa/JwtReaderTest.java | 107 ++--- .../amphora/service/opa/OpaClientTest.java | 291 ++++++------- .../amphora/service/opa/OpaResultTest.java | 30 +- .../amphora/service/opa/OpaServiceTest.java | 342 ++++++++------- .../persistence/metadata/StorageIT.java | 59 +-- .../metadata/StorageServiceTest.java | 314 ++++++++------ .../service/rest/IntraVcpControllerTest.java | 25 +- .../rest/MaskedInputControllerTest.java | 55 +-- .../rest/SecretShareControllerTest.java | 45 +- .../service/rest/TagsControllerTest.java | 68 +-- .../resources/application-test.properties | 2 +- amphora-service/src/test/resources/redis.conf | 2 +- 34 files changed, 1287 insertions(+), 1127 deletions(-) diff --git a/amphora-service/README.md b/amphora-service/README.md index cd8fa58..b59e2df 100644 --- a/amphora-service/README.md +++ b/amphora-service/README.md @@ -104,5 +104,5 @@ DB_USER=user DB_PASSWORD=secret MINIO_ENDPOINT=http://localhost:9000 EOF -docker run --env-file amphora.conf iotspecs/amphora-service +docker run --env-file amphora.conf iotspecs/amphora-service ``` diff --git a/amphora-service/charts/amphora/values.yaml b/amphora-service/charts/amphora/values.yaml index 556700b..914e792 100644 --- a/amphora-service/charts/amphora/values.yaml +++ b/amphora-service/charts/amphora/values.yaml @@ -79,4 +79,4 @@ auth: opa: defaultPolicyPackage: "carbynestack.def" - endpoint: "http://opa.default.svc.cluster.local:8081/" \ No newline at end of file + endpoint: "http://opa.default.svc.cluster.local:8081/" diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/calculation/SecretShareUtil.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/calculation/SecretShareUtil.java index 713a375..0536909 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/calculation/SecretShareUtil.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/calculation/SecretShareUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024 - for information on the respective copyright owner + * Copyright (c) 2021 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 @@ -13,7 +13,6 @@ import io.carbynestack.amphora.common.MaskedInput; import io.carbynestack.amphora.common.MaskedInputData; import io.carbynestack.amphora.common.SecretShare; -import io.carbynestack.amphora.common.Tag; import io.carbynestack.castor.common.entities.Field; import io.carbynestack.castor.common.entities.InputMask; import io.carbynestack.castor.common.entities.Share; diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/config/AuthProperties.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/config/AuthProperties.java index 8e448da..c985b0b 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/config/AuthProperties.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/config/AuthProperties.java @@ -7,7 +7,6 @@ package io.carbynestack.amphora.service.config; - import lombok.Data; import lombok.experimental.Accessors; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -18,5 +17,5 @@ @Data @Accessors(chain = true) public class AuthProperties { - private String userIdFieldName; + private String userIdFieldName; } diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaConfig.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaConfig.java index de8cedd..d415d9e 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaConfig.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/config/OpaConfig.java @@ -9,11 +9,10 @@ import io.carbynestack.amphora.service.opa.JwtReader; import io.carbynestack.amphora.service.opa.OpaClient; +import java.net.URI; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import java.net.URI; - @Configuration public class OpaConfig { @@ -25,8 +24,8 @@ JwtReader jwtReader(AuthProperties authProperties) { @Bean OpaClient opaClient(OpaProperties opaProperties) { return OpaClient.builder() - .opaServiceUri(URI.create(opaProperties.getEndpoint())) - .defaultPolicyPackage(opaProperties.getDefaultPolicyPackage()) - .build(); + .opaServiceUri(URI.create(opaProperties.getEndpoint())) + .defaultPolicyPackage(opaProperties.getDefaultPolicyPackage()) + .build(); } } diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/exceptions/CsOpaException.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/exceptions/CsOpaException.java index 1513f40..1412fb2 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/exceptions/CsOpaException.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/exceptions/CsOpaException.java @@ -8,7 +8,7 @@ package io.carbynestack.amphora.service.exceptions; public class CsOpaException extends Exception { - public CsOpaException(String message) { - super(message); - } + public CsOpaException(String message) { + super(message); + } } diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/exceptions/UnauthorizedException.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/exceptions/UnauthorizedException.java index 9828698..ea9e8e6 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/exceptions/UnauthorizedException.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/exceptions/UnauthorizedException.java @@ -8,7 +8,7 @@ package io.carbynestack.amphora.service.exceptions; public class UnauthorizedException extends Exception { - public UnauthorizedException(String message) { - super(message); - } + public UnauthorizedException(String message) { + super(message); + } } diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/JwtReader.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/JwtReader.java index fba703a..041d0dc 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/JwtReader.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/JwtReader.java @@ -12,58 +12,57 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.carbynestack.amphora.service.exceptions.UnauthorizedException; import io.vavr.control.Option; - import java.util.Base64; public class JwtReader { - static ObjectMapper mapper = new ObjectMapper(); - private final String userIdField; + static ObjectMapper mapper = new ObjectMapper(); + private final String userIdField; - public JwtReader(String userIdField) { - this.userIdField = userIdField; - } + public JwtReader(String userIdField) { + this.userIdField = userIdField; + } - public String extractUserIdFromAuthHeader(String header) throws UnauthorizedException { - return extractFieldFromAuthHeader(header, userIdField); - } + public String extractUserIdFromAuthHeader(String header) throws UnauthorizedException { + return extractFieldFromAuthHeader(header, userIdField); + } - private static String extractFieldFromAuthHeader(String header, String field) throws UnauthorizedException { - return tokenFromHeader(header) - .flatMap(JwtReader::dataNodeFromToken) - .flatMap(node -> fieldFromNode(node, field)) - .getOrElseThrow(() -> new UnauthorizedException("No token provided")); - } + private static String extractFieldFromAuthHeader(String header, String field) + throws UnauthorizedException { + return tokenFromHeader(header) + .flatMap(JwtReader::dataNodeFromToken) + .flatMap(node -> fieldFromNode(node, field)) + .getOrElseThrow(() -> new UnauthorizedException("No token provided")); + } - private static Option dataNodeFromToken(String token) { - String[] parts = token.split("\\."); - if (parts.length != 3) { - return Option.none(); - } - try { - String jwt = new String(Base64.getDecoder().decode(parts[1])); - return Option.of(mapper.reader().readTree(jwt)); - } catch (JsonProcessingException e) { - return Option.none(); - } + private static Option dataNodeFromToken(String token) { + String[] parts = token.split("\\."); + if (parts.length != 3) { + return Option.none(); + } + try { + String jwt = new String(Base64.getDecoder().decode(parts[1])); + return Option.of(mapper.reader().readTree(jwt)); + } catch (JsonProcessingException e) { + return Option.none(); } + } - private static Option tokenFromHeader(String header) { - if (header != null && header.startsWith("Bearer ")) { - return Option.of(header.substring(7)); - } - return Option.none(); + private static Option tokenFromHeader(String header) { + if (header != null && header.startsWith("Bearer ")) { + return Option.of(header.substring(7)); } + return Option.none(); + } - private static Option fieldFromNode(JsonNode node, String fieldName) { - JsonNode field = node; - try { - for(String f : fieldName.split("\\.")) { - field = field.get(f); - } - return Option.of(field.asText()); - } catch (NullPointerException e) { - return Option.none(); - } + private static Option fieldFromNode(JsonNode node, String fieldName) { + JsonNode field = node; + try { + for (String f : fieldName.split("\\.")) { + field = field.get(f); + } + return Option.of(field.asText()); + } catch (NullPointerException e) { + return Option.none(); } + } } - diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClient.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClient.java index 30ba7b9..fe9c40e 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClient.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClient.java @@ -10,58 +10,54 @@ import io.carbynestack.amphora.common.Tag; import io.carbynestack.httpclient.CsHttpClient; import io.carbynestack.httpclient.CsHttpClientException; -import lombok.Builder; -import lombok.extern.slf4j.Slf4j; - import java.net.URI; import java.util.List; +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; @Slf4j public class OpaClient { - private final CsHttpClient csHttpClient; - private final URI opaServiceUri; - private final String defaultPolicyPackage; - - @Builder - public OpaClient(URI opaServiceUri, String defaultPolicyPackage) { - this(CsHttpClient.createDefault(), opaServiceUri, defaultPolicyPackage); + private final CsHttpClient csHttpClient; + private final URI opaServiceUri; + private final String defaultPolicyPackage; + + @Builder + public OpaClient(URI opaServiceUri, String defaultPolicyPackage) { + this(CsHttpClient.createDefault(), opaServiceUri, defaultPolicyPackage); + } + + OpaClient(CsHttpClient httpClient, URI opaServiceUri, String defaultPolicyPackage) { + this.csHttpClient = httpClient; + this.opaServiceUri = opaServiceUri; + this.defaultPolicyPackage = defaultPolicyPackage; + } + + /** + * Evaluate the OPA policy package with the given action, subject and tags. + * + * @param policyPackage The OPA policy package to evaluate. + * @param action The action to evaluate. + * @param subject The subject attempting to perform the action. + * @param tags The tags describing the accessed object. + * @return True if the subject can perform the action, false otherwise (or if an error occurred). + */ + public boolean isAllowed(String policyPackage, String action, String subject, List tags) { + OpaRequestBody body = OpaRequestBody.builder().subject(subject).tags(tags).build(); + try { + return csHttpClient + .postForObject( + opaServiceUri.resolve( + String.format("/v1/data/%s/%s", policyPackage.replace(".", "/"), action)), + new OpaRequest(body), + OpaResult.class) + .isAllowed(); + } catch (CsHttpClientException e) { + log.error("Error occurred while evaluating OPA policy package", e); } + return false; + } - OpaClient(CsHttpClient httpClient, URI opaServiceUri, String defaultPolicyPackage) { - this.csHttpClient = httpClient; - this.opaServiceUri = opaServiceUri; - this.defaultPolicyPackage = defaultPolicyPackage; - } - - /** - * Evaluate the OPA policy package with the given action, subject and tags. - * - * @param policyPackage The OPA policy package to evaluate. - * @param action The action to evaluate. - * @param subject The subject attempting to perform the action. - * @param tags The tags describing the accessed object. - * @return True if the subject can perform the action, false otherwise (or if an error occurred). - */ - public boolean isAllowed(String policyPackage, String action, String subject, List tags) { - OpaRequestBody body = OpaRequestBody.builder() - .subject(subject) - .tags(tags) - .build(); - try { - return csHttpClient.postForObject(opaServiceUri.resolve( - String.format("/v1/data/%s/%s", - policyPackage.replace(".", "/"), - action)), - new OpaRequest(body), - OpaResult.class) - .isAllowed(); - } catch (CsHttpClientException e) { - log.error("Error occurred while evaluating OPA policy package: {}", e.getMessage()); - } - return false; - } - - public OpaClientRequest newRequest() { - return new OpaClientRequest(this, defaultPolicyPackage); - } -} \ No newline at end of file + public OpaClientRequest newRequest() { + return new OpaClientRequest(this, defaultPolicyPackage); + } +} diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClientRequest.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClientRequest.java index a2f2c98..3e68ad4 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClientRequest.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaClientRequest.java @@ -9,37 +9,35 @@ import io.carbynestack.amphora.common.Tag; import io.carbynestack.amphora.service.exceptions.CsOpaException; +import java.util.List; +import java.util.Objects; import lombok.Setter; import lombok.experimental.Accessors; import org.apache.logging.log4j.util.Strings; -import java.util.List; -import java.util.Objects; - @Setter() @Accessors(chain = true, fluent = true) public class OpaClientRequest { - private String withPolicyPackage; - private String withSubject; - private List withTags; - private String withAction; + private String withPolicyPackage; + private String withSubject; + private List withTags; + private String withAction; - private final OpaClient opaClient; + private final OpaClient opaClient; - public OpaClientRequest(OpaClient opaClient, String defaultPolicyPackage) { - this.withPolicyPackage = defaultPolicyPackage; - this.opaClient = opaClient; - } + public OpaClientRequest(OpaClient opaClient, String defaultPolicyPackage) { + this.withPolicyPackage = defaultPolicyPackage; + this.opaClient = opaClient; + } - public boolean evaluate() throws CsOpaException { - if(Strings.isEmpty(withSubject)) { - throw new CsOpaException("Subject is required to evaluate the policy"); - } - if(Strings.isEmpty(withAction)) { - throw new CsOpaException("Action is required to evaluate the policy"); - } - withTags.removeIf(Objects::isNull); - return opaClient.isAllowed(withPolicyPackage, withAction, withSubject, withTags); + public boolean evaluate() throws CsOpaException { + if (Strings.isEmpty(withSubject)) { + throw new CsOpaException("Subject is required to evaluate the policy"); } + if (Strings.isEmpty(withAction)) { + throw new CsOpaException("Action is required to evaluate the policy"); + } + withTags.removeIf(Objects::isNull); + return opaClient.isAllowed(withPolicyPackage, withAction, withSubject, withTags); + } } - diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequest.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequest.java index 6f276b5..12fa776 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequest.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequest.java @@ -11,10 +11,9 @@ @Getter class OpaRequest { - OpaRequestBody input; - - public OpaRequest(OpaRequestBody input) { - this.input = input; - } + OpaRequestBody input; + public OpaRequest(OpaRequestBody input) { + this.input = input; + } } diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequestBody.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequestBody.java index ffd591b..2fc2b18 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequestBody.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaRequestBody.java @@ -8,19 +8,18 @@ package io.carbynestack.amphora.service.opa; import io.carbynestack.amphora.common.Tag; +import java.util.List; import lombok.Builder; import lombok.Value; -import java.util.List; - @Value public class OpaRequestBody { - String subject; - List tags; + String subject; + List tags; - @Builder - public OpaRequestBody(String subject, List tags) { - this.subject = subject; - this.tags = tags; - } + @Builder + public OpaRequestBody(String subject, List tags) { + this.subject = subject; + this.tags = tags; + } } diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaResult.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaResult.java index da0d2e0..4c17f01 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaResult.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaResult.java @@ -10,10 +10,9 @@ import lombok.Setter; public class OpaResult { - @Setter - private boolean result; + @Setter private boolean result; - boolean isAllowed() { - return result; - } + boolean isAllowed() { + return result; + } } diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaService.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaService.java index d8c5618..07c2c00 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaService.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/opa/OpaService.java @@ -9,140 +9,146 @@ import io.carbynestack.amphora.common.Tag; import io.carbynestack.amphora.service.exceptions.CsOpaException; +import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.util.List; - @Service public class OpaService { - public static final String POLICY_PACKAGE_TAG_KEY = "accessPolicy"; - public static final String OWNER_TAG_KEY = "owner"; + public static final String POLICY_PACKAGE_TAG_KEY = "accessPolicy"; + public static final String OWNER_TAG_KEY = "owner"; - static final String READ_SECRET_ACTION_NAME = "read"; - static final String USE_SECRET_ACTION_NAME = "use"; - static final String DELETE_SECRET_ACTION_NAME = "delete"; - static final String CREATE_TAG_ACTION_NAME = "tag/create"; - static final String READ_TAG_ACTION_NAME = "tag/read"; - static final String UPDATE_TAG_ACTION_NAME = "tag/update"; - static final String DELETE_TAG_ACTION_NAME = "tag/delete"; + static final String READ_SECRET_ACTION_NAME = "read"; + static final String USE_SECRET_ACTION_NAME = "use"; + static final String DELETE_SECRET_ACTION_NAME = "delete"; + static final String CREATE_TAG_ACTION_NAME = "tag/create"; + static final String READ_TAG_ACTION_NAME = "tag/read"; + static final String UPDATE_TAG_ACTION_NAME = "tag/update"; + static final String DELETE_TAG_ACTION_NAME = "tag/delete"; - private final OpaClient opaClient; + private final OpaClient opaClient; - @Autowired - public OpaService(OpaClient opaClient) { - this.opaClient = opaClient; - } + @Autowired + public OpaService(OpaClient opaClient) { + this.opaClient = opaClient; + } - /** - * Check if the subject can read the secret described by the given tags evaluating the OPA policy package. - * The policy package is extracted from the tags if present. If not present, the default policy package is used. - * - * @param subject The subject attempting to read the secret. - * @param tags The tags describing the referenced secret. - * @return True if the subject can read the secret, false otherwise. - * @throws CsOpaException If an error occurred while evaluating the policy. - */ - public boolean canReadSecret(String subject, List tags) throws CsOpaException { - return isAllowed(subject, READ_SECRET_ACTION_NAME, tags); - } + /** + * Check if the subject can read the secret described by the given tags evaluating the OPA policy + * package. The policy package is extracted from the tags if present. If not present, the default + * policy package is used. + * + * @param subject The subject attempting to read the secret. + * @param tags The tags describing the referenced secret. + * @return True if the subject can read the secret, false otherwise. + * @throws CsOpaException If an error occurred while evaluating the policy. + */ + public boolean canReadSecret(String subject, List tags) throws CsOpaException { + return isAllowed(subject, READ_SECRET_ACTION_NAME, tags); + } - /** - * Check if the subject can use the secret described by the given tags evaluating the OPA policy package. - * The policy package is extracted from the tags if present. If not present, the default policy package is used. - * - * @param subject The subject attempting to use the secret. - * @param tags The tags describing the referenced secret. - * @return True if the subject can use the secret, false otherwise. - * @throws CsOpaException If an error occurred while evaluating the policy. - */ - public boolean canUseSecret(String subject, List tags) throws CsOpaException { - return isAllowed(subject, USE_SECRET_ACTION_NAME, tags); - } + /** + * Check if the subject can use the secret described by the given tags evaluating the OPA policy + * package. The policy package is extracted from the tags if present. If not present, the default + * policy package is used. + * + * @param subject The subject attempting to use the secret. + * @param tags The tags describing the referenced secret. + * @return True if the subject can use the secret, false otherwise. + * @throws CsOpaException If an error occurred while evaluating the policy. + */ + public boolean canUseSecret(String subject, List tags) throws CsOpaException { + return isAllowed(subject, USE_SECRET_ACTION_NAME, tags); + } - /** - * Check if the subject can delete the secret described by the given tags evaluating the OPA policy package. - * The policy package is extracted from the tags if present. If not present, the default policy package is used. - * - * @param subject The subject attempting to delete the secret. - * @param tags The tags describing the referenced secret. - * @return True if the subject can delete the secret, false otherwise. - * @throws CsOpaException If an error occurred while evaluating the policy. - */ - public boolean canDeleteSecret(String subject, List tags) throws CsOpaException { - return isAllowed(subject, DELETE_SECRET_ACTION_NAME, tags); - } + /** + * Check if the subject can delete the secret described by the given tags evaluating the OPA + * policy package. The policy package is extracted from the tags if present. If not present, the + * default policy package is used. + * + * @param subject The subject attempting to delete the secret. + * @param tags The tags describing the referenced secret. + * @return True if the subject can delete the secret, false otherwise. + * @throws CsOpaException If an error occurred while evaluating the policy. + */ + public boolean canDeleteSecret(String subject, List tags) throws CsOpaException { + return isAllowed(subject, DELETE_SECRET_ACTION_NAME, tags); + } - /** - * Check if the subject can create the given tags evaluating the OPA policy package. - * The policy package is extracted from the tags if present. If not present, the default policy package is used. - * - * @param subject The subject attempting to create the tags. - * @param tags The tags to create. - * @return True if the subject can create the tags, false otherwise. - * @throws CsOpaException If an error occurred while evaluating the policy. - */ - public boolean canCreateTags(String subject, List tags) throws CsOpaException { - return isAllowed(subject, CREATE_TAG_ACTION_NAME, tags); - } + /** + * Check if the subject can create the given tags evaluating the OPA policy package. The policy + * package is extracted from the tags if present. If not present, the default policy package is + * used. + * + * @param subject The subject attempting to create the tags. + * @param tags The tags to create. + * @return True if the subject can create the tags, false otherwise. + * @throws CsOpaException If an error occurred while evaluating the policy. + */ + public boolean canCreateTags(String subject, List tags) throws CsOpaException { + return isAllowed(subject, CREATE_TAG_ACTION_NAME, tags); + } - /** - * Check if the subject can read the given tags evaluating the OPA policy package. - * The policy package is extracted from the tags if present. If not present, the default policy package is used. - * - * @param subject The subject attempting to read the tags. - * @param tags The tags to read. - * @return True if the subject can read the tags, false otherwise. - * @throws CsOpaException If an error occurred while evaluating the policy. - */ - public boolean canReadTags(String subject, List tags) throws CsOpaException { - return isAllowed(subject, READ_TAG_ACTION_NAME, tags); - } + /** + * Check if the subject can read the given tags evaluating the OPA policy package. The policy + * package is extracted from the tags if present. If not present, the default policy package is + * used. + * + * @param subject The subject attempting to read the tags. + * @param tags The tags to read. + * @return True if the subject can read the tags, false otherwise. + * @throws CsOpaException If an error occurred while evaluating the policy. + */ + public boolean canReadTags(String subject, List tags) throws CsOpaException { + return isAllowed(subject, READ_TAG_ACTION_NAME, tags); + } - /** - * Check if the subject can update the given tags evaluating the OPA policy package. - * The policy package is extracted from the tags if present. If not present, the default policy package is used. - * - * @param subject The subject attempting to update the tags. - * @param tags The tags to update. - * @return True if the subject can update the tags, false otherwise. - * @throws CsOpaException If an error occurred while evaluating the policy. - */ - public boolean canUpdateTags(String subject, List tags) throws CsOpaException { - return isAllowed(subject, UPDATE_TAG_ACTION_NAME, tags); - } + /** + * Check if the subject can update the given tags evaluating the OPA policy package. The policy + * package is extracted from the tags if present. If not present, the default policy package is + * used. + * + * @param subject The subject attempting to update the tags. + * @param tags The tags to update. + * @return True if the subject can update the tags, false otherwise. + * @throws CsOpaException If an error occurred while evaluating the policy. + */ + public boolean canUpdateTags(String subject, List tags) throws CsOpaException { + return isAllowed(subject, UPDATE_TAG_ACTION_NAME, tags); + } - /** - * Check if the subject can delete the given tags evaluating the OPA policy package. - * The policy package is extracted from the tags if present. If not present, the default policy package is used. - * - * @param subject The subject attempting to delete the tags. - * @param tags The tags to delete. - * @return True if the subject can delete the tags, false otherwise. - * @throws CsOpaException If an error occurred while evaluating the policy. - */ - public boolean canDeleteTags(String subject, List tags) throws CsOpaException { - return isAllowed(subject, DELETE_TAG_ACTION_NAME, tags); - } + /** + * Check if the subject can delete the given tags evaluating the OPA policy package. The policy + * package is extracted from the tags if present. If not present, the default policy package is + * used. + * + * @param subject The subject attempting to delete the tags. + * @param tags The tags to delete. + * @return True if the subject can delete the tags, false otherwise. + * @throws CsOpaException If an error occurred while evaluating the policy. + */ + public boolean canDeleteTags(String subject, List tags) throws CsOpaException { + return isAllowed(subject, DELETE_TAG_ACTION_NAME, tags); + } - /** - * Check if the subject can perform the given action evaluating the OPA policy package. - * The policy package is extracted from the tags if present. If not present, the default policy package is used. - * - * @param action The action to evaluate. - * @param subject The subject attempting to perform the action. - * @param tags The tags describing the accessed object. - * @return True if the subject can perform the action, false otherwise. - * @throws CsOpaException If an error occurred while evaluating the policy. - */ - boolean isAllowed(String subject, String action, List tags) throws CsOpaException { - OpaClientRequest request = opaClient.newRequest() - .withSubject(subject) - .withAction(action) - .withTags(tags); - tags.stream().filter(tag -> tag.getKey().equals(POLICY_PACKAGE_TAG_KEY)) - .findFirst() - .ifPresent(tag -> request.withPolicyPackage(tag.getValue())); - return request.evaluate(); - } + /** + * Check if the subject can perform the given action evaluating the OPA policy package. The policy + * package is extracted from the tags if present. If not present, the default policy package is + * used. + * + * @param action The action to evaluate. + * @param subject The subject attempting to perform the action. + * @param tags The tags describing the accessed object. + * @return True if the subject can perform the action, false otherwise. + * @throws CsOpaException If an error occurred while evaluating the policy. + */ + boolean isAllowed(String subject, String action, List tags) throws CsOpaException { + OpaClientRequest request = + opaClient.newRequest().withSubject(subject).withAction(action).withTags(tags); + tags.stream() + .filter(tag -> tag.getKey().equals(POLICY_PACKAGE_TAG_KEY)) + .findFirst() + .ifPresent(tag -> request.withPolicyPackage(tag.getValue())); + return request.evaluate(); + } } diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java index 6d257dc..5a85a14 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/persistence/metadata/StorageService.java @@ -7,6 +7,10 @@ package io.carbynestack.amphora.service.persistence.metadata; +import static io.carbynestack.amphora.service.opa.OpaService.OWNER_TAG_KEY; +import static io.carbynestack.amphora.service.persistence.metadata.TagEntity.setFromTagList; +import static io.carbynestack.amphora.service.persistence.metadata.TagEntity.setToTagList; + import com.google.common.collect.Lists; import io.carbynestack.amphora.common.*; import io.carbynestack.amphora.common.exceptions.AmphoraServiceException; @@ -24,6 +28,8 @@ import io.carbynestack.castor.common.entities.InputMask; import io.carbynestack.castor.common.entities.TupleList; import io.vavr.control.Option; +import java.util.*; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -35,13 +41,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; -import java.util.*; -import java.util.stream.Collectors; - -import static io.carbynestack.amphora.service.opa.OpaService.OWNER_TAG_KEY; -import static io.carbynestack.amphora.service.persistence.metadata.TagEntity.setFromTagList; -import static io.carbynestack.amphora.service.persistence.metadata.TagEntity.setToTagList; - /** * A service to persist and manipulate {@link SecretEntity SecretEntities} and related data like * {@link TagEntity TagEntities} and {@link SecretShare SecretShares}. @@ -51,20 +50,19 @@ @RequiredArgsConstructor(onConstructor_ = @Autowired) public class StorageService { public static final String CREATION_DATE_KEY = "creation-date"; - public static final List RESERVED_TAG_KEYS = Lists.newArrayList( - CREATION_DATE_KEY, - OWNER_TAG_KEY); + public static final List RESERVED_TAG_KEYS = + Lists.newArrayList(CREATION_DATE_KEY, OWNER_TAG_KEY); public static final String TAGS_WITH_THE_SAME_KEY_DEFINED_EXCEPTION_MSG = - "Two or more tags with the same key defined."; + "Two or more tags with the same key defined."; public static final String SECRET_WITH_ID_EXISTS_EXCEPTION_MSG = - "A secret with the given id already exists."; + "A secret with the given id already exists."; public static final String IS_RESERVED_KEY_EXCEPTION_MSG = "\"%s\" is a reserved key."; public static final String NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG = - "No secret with the given id #%s exists."; + "No secret with the given id #%s exists."; public static final String NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG = - "No tag with key \"%s\" exists for secret with id #%s."; + "No tag with key \"%s\" exists for secret with id #%s."; public static final String TAG_WITH_KEY_EXISTS_FOR_SECRET_EXCEPTION_MSG = - "A tag with key \"%s\" already exists for secret #%s"; + "A tag with key \"%s\" already exists for secret #%s"; private final SecretEntityRepository secretEntityRepository; private final InputMaskCachingService inputMaskStore; @@ -83,7 +81,8 @@ public class StorageService { * persisting the secret without further notice. * * @param maskedInput the {@link MaskedInput} to persist - * @param authorizedUserId the id of the authenticated user becoming the owner of the persisted {@link SecretShare} + * @param authorizedUserId the id of the authenticated user becoming the owner of the persisted + * {@link SecretShare} * @return the id of the new {@link SecretShare} as {@link String} * @throws AlreadyExistsException if an {@link SecretShare} with the given id already exists. * @throws IllegalArgumentException if one or more {@link Tag}s with the same {@link Tag#getKey() @@ -101,15 +100,18 @@ public String createSecret(MaskedInput maskedInput, String authorizedUserId) { throw new IllegalArgumentException(TAGS_WITH_THE_SAME_KEY_DEFINED_EXCEPTION_MSG); } TupleList, Field.Gfp> inputMasks = - inputMaskStore.getCachedInputMasks(maskedInput.getSecretId()); + inputMaskStore.getCachedInputMasks(maskedInput.getSecretId()); SecretShare secretShare = - secretShareUtil.convertToSecretShare( - maskedInput, - spdzProperties.getMacKey(), - inputMasks, - amphoraServiceProperties.getPlayerId() != 0); - String secretId = persistSecretShare(secretShare, - Collections.singletonList(Tag.builder().key(OWNER_TAG_KEY).value(authorizedUserId).build())); + secretShareUtil.convertToSecretShare( + maskedInput, + spdzProperties.getMacKey(), + inputMasks, + amphoraServiceProperties.getPlayerId() != 0); + String secretId = + persistSecretShare( + secretShare, + Collections.singletonList( + Tag.builder().key(OWNER_TAG_KEY).value(authorizedUserId).build())); inputMaskStore.removeInputMasks(secretShare.getSecretId()); return secretId; } @@ -137,9 +139,9 @@ public String storeSecretShare(SecretShare secretShare) { } List reservedTags = new ArrayList<>(); secretShare.getTags().stream() - .filter(t -> t.getKey().equals(OWNER_TAG_KEY)) - .findFirst() - .ifPresent(reservedTags::add); + .filter(t -> t.getKey().equals(OWNER_TAG_KEY)) + .findFirst() + .ifPresent(reservedTags::add); return persistSecretShare(secretShare, reservedTags); } @@ -150,18 +152,18 @@ public String storeSecretShare(SecretShare secretShare) { private String persistSecretShare(SecretShare secretShare, List reservedTags) { List tags = dropReservedTags(new ArrayList<>(secretShare.getTags())); tags.add( - Tag.builder() - .key(StorageService.CREATION_DATE_KEY) - .value(Long.toString(System.currentTimeMillis())) - .build()); + Tag.builder() + .key(StorageService.CREATION_DATE_KEY) + .value(Long.toString(System.currentTimeMillis())) + .build()); tags.addAll(reservedTags); Set tagEntities = setFromTagList(tags); String persistedSecretId = - secretEntityRepository - .save(new SecretEntity(secretShare.getSecretId().toString(), tagEntities)) - .getSecretId(); + secretEntityRepository + .save(new SecretEntity(secretShare.getSecretId().toString(), tagEntities)) + .getSecretId(); secretShareDataStore.storeSecretShareData( - UUID.fromString(persistedSecretId), secretShare.getData()); + UUID.fromString(persistedSecretId), secretShare.getData()); return persistedSecretId; } @@ -181,12 +183,12 @@ private static boolean hasDuplicateKey(List tags) { private static List dropReservedTags(List tags) { if (!CollectionUtils.isEmpty(tags)) { List itemsToDrop = - tags.stream().filter(StorageService::tagIsReserved).collect(Collectors.toList()); + tags.stream().filter(StorageService::tagIsReserved).collect(Collectors.toList()); itemsToDrop.forEach( - t -> { - log.debug("Dropped tag {} for using reserved key.", t.toString()); - tags.remove(t); - }); + t -> { + log.debug("Dropped tag {} for using reserved key.", t.toString()); + tags.remove(t); + }); } return tags; } @@ -208,8 +210,8 @@ public Page getSecretList(Sort sort) { @Transactional(readOnly = true) public Page getSecretList(List tagFilters, Pageable pageable) { return secretEntityRepository - .findAll(SecretEntitySpecification.with(tagFilters), pageable) - .map(SecretEntity::toMetadata); + .findAll(SecretEntitySpecification.with(tagFilters), pageable) + .map(SecretEntity::toMetadata); } @Transactional(readOnly = true) @@ -225,16 +227,19 @@ public Page getSecretList(List tagFilters, Sort sort) { * if no secret with the given id exists. * @throws AmphoraServiceException if an {@link SecretShare} exists but could not be retrieved. * @throws NotFoundException if no {@link SecretShare} with the given id exists - * @throws UnauthorizedException if the requesting program is not authorized to access the {@link SecretShare} + * @throws UnauthorizedException if the requesting program is not authorized to access the {@link + * SecretShare} */ @Transactional(readOnly = true) - public SecretShare useSecretShare(UUID secretId, String programId) throws UnauthorizedException, CsOpaException { - SecretEntity secretEntity = secretEntityRepository - .findById(secretId.toString()) - .orElseThrow( - () -> - new NotFoundException( - String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + public SecretShare useSecretShare(UUID secretId, String programId) + throws UnauthorizedException, CsOpaException { + SecretEntity secretEntity = + secretEntityRepository + .findById(secretId.toString()) + .orElseThrow( + () -> + new NotFoundException( + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); if (!opaService.canUseSecret(programId, setToTagList(secretEntity.getTags()))) { throw new UnauthorizedException("Requesting program is not authorized to access the secret"); } @@ -243,10 +248,10 @@ public SecretShare useSecretShare(UUID secretId, String programId) throws Unauth /** * Retrieves an {@link SecretShare} with a given id. - *

- * This method requires the authenticated user to be authorized to perform the operation. The authorization is - * checked by the {@link OpaService#canReadSecret(String, List)} method using the {@link Tag}s of - * the related {@link SecretEntity}. + * + *

This method requires the authenticated user to be authorized to perform the operation. The + * authorization is checked by the {@link OpaService#canReadSecret(String, List)} method using the + * {@link Tag}s of the related {@link SecretEntity}. * * @param secretId id of the {@link SecretShare} to retrieve * @param authorizedUserId the id of the authenticated user requesting the operation @@ -258,15 +263,17 @@ public SecretShare useSecretShare(UUID secretId, String programId) throws Unauth * @throws CsOpaException if an error occurred while evaluating the policy */ @Transactional(readOnly = true) - public SecretShare getSecretShare(UUID secretId, String authorizedUserId) throws CsOpaException, UnauthorizedException { - SecretEntity secretEntity = secretEntityRepository + public SecretShare getSecretShare(UUID secretId, String authorizedUserId) + throws CsOpaException, UnauthorizedException { + SecretEntity secretEntity = + secretEntityRepository .findById(secretId.toString()) .orElseThrow( - () -> - new NotFoundException( - String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + () -> + new NotFoundException( + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); - if(!opaService.canReadSecret(authorizedUserId, setToTagList(secretEntity.getTags()))) { + if (!opaService.canReadSecret(authorizedUserId, setToTagList(secretEntity.getTags()))) { throw new UnauthorizedException("User is not authorized to read this secret"); } return getSecretShareForEntity(secretEntity); @@ -274,19 +281,19 @@ public SecretShare getSecretShare(UUID secretId, String authorizedUserId) throws private SecretShare getSecretShareForEntity(SecretEntity entity) { return SecretShare.builder() - .secretId(UUID.fromString(entity.getSecretId())) - .data(secretShareDataStore.getSecretShareData(UUID.fromString(entity.getSecretId()))) - .tags(setToTagList(entity.getTags())) - .build(); + .secretId(UUID.fromString(entity.getSecretId())) + .data(secretShareDataStore.getSecretShareData(UUID.fromString(entity.getSecretId()))) + .tags(setToTagList(entity.getTags())) + .build(); } /** * Removes an {@link SecretEntity} and all related information ({@link TagEntity tags} and {@link * SecretShare data}) from the storage. - *

- * This method requires the authenticated user to be authorized to perform the operation. The authorization is - * checked by the {@link OpaService#canDeleteSecret(String, List)} method using the {@link Tag}s of - * the related {@link SecretEntity}. + * + *

This method requires the authenticated user to be authorized to perform the operation. The + * authorization is checked by the {@link OpaService#canDeleteSecret(String, List)} method using + * the {@link Tag}s of the related {@link SecretEntity}. * * @param secretId the id of the secret to be removed. * @param authorizedUserId the id of the authenticated user requesting the deletion @@ -296,14 +303,18 @@ private SecretShare getSecretShareForEntity(SecretEntity entity) { * @throws CsOpaException if an error occurred while evaluating the policy */ @Transactional - public void deleteSecret(UUID secretId, String authorizedUserId) throws CsOpaException, UnauthorizedException { + public void deleteSecret(UUID secretId, String authorizedUserId) + throws CsOpaException, UnauthorizedException { // Better to accept String as input once - instead of repeatedly converting it. - SecretEntity secretEntity = secretEntityRepository.findById(secretId.toString()) - .orElseThrow(() -> - new NotFoundException( - String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); - if(!opaService.canDeleteSecret(authorizedUserId, setToTagList(secretEntity.getTags()))) { - throw new UnauthorizedException("User is not authorized to delete this secret"); + SecretEntity secretEntity = + secretEntityRepository + .findById(secretId.toString()) + .orElseThrow( + () -> + new NotFoundException( + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + if (!opaService.canDeleteSecret(authorizedUserId, setToTagList(secretEntity.getTags()))) { + throw new UnauthorizedException("User is not authorized to delete this secret"); } secretEntityRepository.deleteBySecretId(secretId.toString()); secretShareDataStore.deleteSecretShareData(secretId); @@ -311,10 +322,10 @@ public void deleteSecret(UUID secretId, String authorizedUserId) throws CsOpaExc /** * Persists a {@link Tag} related to a specified {@link SecretShare} - *

- * This method requires the authenticated user to be authorized to perform the operation. The authorization is - * checked by the {@link OpaService#canCreateTags(String, List)} method using the {@link Tag}s of - * the related {@link SecretEntity}. + * + *

This method requires the authenticated user to be authorized to perform the operation. The + * authorization is checked by the {@link OpaService#canCreateTags(String, List)} method using the + * {@link Tag}s of the related {@link SecretEntity}. * * @param secretId id of the secret this {@link Tag} belongs to * @param tag the tag to persist @@ -326,40 +337,41 @@ public void deleteSecret(UUID secretId, String authorizedUserId) throws CsOpaExc * exists. */ @Transactional - public String storeTag(UUID secretId, Tag tag, String authorizedUserId) throws CsOpaException, UnauthorizedException { + public String storeTag(UUID secretId, Tag tag, String authorizedUserId) + throws CsOpaException, UnauthorizedException { if (tagIsReserved(tag)) { throw new IllegalArgumentException( - String.format(IS_RESERVED_KEY_EXCEPTION_MSG, tag.getKey())); + String.format(IS_RESERVED_KEY_EXCEPTION_MSG, tag.getKey())); } SecretEntity secretEntity = - secretEntityRepository - .findById(secretId.toString()) - .orElseThrow( - () -> - new NotFoundException( - String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + secretEntityRepository + .findById(secretId.toString()) + .orElseThrow( + () -> + new NotFoundException( + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); if (!opaService.canCreateTags(authorizedUserId, setToTagList(secretEntity.getTags()))) { - throw new UnauthorizedException("User is not authorized to create tags for this secret"); + throw new UnauthorizedException("User is not authorized to create tags for this secret"); } tagRepository - .findBySecretAndKey(secretEntity, tag.getKey()) - .ifPresent( - t -> { - throw new AlreadyExistsException( - String.format( - TAG_WITH_KEY_EXISTS_FOR_SECRET_EXCEPTION_MSG, tag.getKey(), secretId)); - }); + .findBySecretAndKey(secretEntity, tag.getKey()) + .ifPresent( + t -> { + throw new AlreadyExistsException( + String.format( + TAG_WITH_KEY_EXISTS_FOR_SECRET_EXCEPTION_MSG, tag.getKey(), secretId)); + }); return tagRepository.save(TagEntity.fromTag(tag).setSecret(secretEntity)).getKey(); } /** * Replaces the {@link Tag}s for a {@link SecretEntity} with the given id. - *

- * This method requires the authenticated user to be authorized to perform the operation. The authorization is - * checked by the {@link OpaService#canUpdateTags(String, List)} method using the {@link Tag}s of - * the related {@link SecretEntity}. - *

- * {@link Tag}s that use a reserved tag {@link #RESERVED_TAG_KEYS} will be removed before + * + *

This method requires the authenticated user to be authorized to perform the operation. The + * authorization is checked by the {@link OpaService#canUpdateTags(String, List)} method using the + * {@link Tag}s of the related {@link SecretEntity}. + * + *

{@link Tag}s that use a reserved tag {@link #RESERVED_TAG_KEYS} will be removed before * persisting the secret without further notice. * * @param secretId the id of the {@link SecretEntity} whose tags should be replaced. @@ -372,23 +384,28 @@ public String storeTag(UUID secretId, Tag tag, String authorizedUserId) throws C * @throws CsOpaException if an error occurred while evaluating the policy */ @Transactional - public void replaceTags(UUID secretId, List tags, String authorizedUserId) throws CsOpaException, UnauthorizedException { + public void replaceTags(UUID secretId, List tags, String authorizedUserId) + throws CsOpaException, UnauthorizedException { if (hasDuplicateKey(tags)) { throw new IllegalArgumentException(TAGS_WITH_THE_SAME_KEY_DEFINED_EXCEPTION_MSG); } - SecretEntity secretEntityReference = secretEntityRepository.findById(secretId.toString()) - .orElseThrow(() -> + SecretEntity secretEntityReference = + secretEntityRepository + .findById(secretId.toString()) + .orElseThrow( + () -> new NotFoundException( - String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); - if (!opaService.canUpdateTags(authorizedUserId, setToTagList(secretEntityReference.getTags()))) { - throw new UnauthorizedException("User is not authorized to update tags for this secret"); + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + if (!opaService.canUpdateTags( + authorizedUserId, setToTagList(secretEntityReference.getTags()))) { + throw new UnauthorizedException("User is not authorized to update tags for this secret"); } List existingReservedTags = - RESERVED_TAG_KEYS.stream() - .map(key -> tagRepository.findBySecretAndKey(secretEntityReference, key)) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toList()); + RESERVED_TAG_KEYS.stream() + .map(key -> tagRepository.findBySecretAndKey(secretEntityReference, key)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); List newTags = dropReservedTags(new ArrayList<>(tags)); tagRepository.deleteBySecret(secretEntityReference); Set newTagList = setFromTagList(newTags); @@ -399,10 +416,10 @@ public void replaceTags(UUID secretId, List tags, String authorizedUserId) /** * Returns all {@link Tag}s associated to an {@link SecretEntity} with the given id. - *

- * This method requires the authenticated user to be authorized to perform the operation. The authorization is - * checked by the {@link OpaService#canReadTags(String, List)} method using the {@link Tag}s of - * the related {@link SecretEntity}. + * + *

This method requires the authenticated user to be authorized to perform the operation. The + * authorization is checked by the {@link OpaService#canReadTags(String, List)} method using the + * {@link Tag}s of the related {@link SecretEntity}. * * @param secretId the id of the {@link SecretEntity} whose tags should be retrieved. * @param authorizedUserId the id of the authenticated user requesting the operation @@ -412,16 +429,17 @@ public void replaceTags(UUID secretId, List tags, String authorizedUserId) * @throws CsOpaException if an error occurred while evaluating the policy */ @Transactional(readOnly = true) - public List retrieveTags(UUID secretId, String authorizedUserId) throws CsOpaException, UnauthorizedException { + public List retrieveTags(UUID secretId, String authorizedUserId) + throws CsOpaException, UnauthorizedException { SecretEntity secretEntity = - secretEntityRepository - .findById(secretId.toString()) - .orElseThrow( - () -> - new NotFoundException( - String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + secretEntityRepository + .findById(secretId.toString()) + .orElseThrow( + () -> + new NotFoundException( + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); if (!opaService.canReadTags(authorizedUserId, setToTagList(secretEntity.getTags()))) { - throw new UnauthorizedException("User is not authorized to read tags for this secret"); + throw new UnauthorizedException("User is not authorized to read tags for this secret"); } return setToTagList(secretEntity.getTags()); } @@ -429,10 +447,10 @@ public List retrieveTags(UUID secretId, String authorizedUserId) throws CsO /** * Returns a single {@link Tag}s associated to an {@link SecretEntity} with the given id, and a * specified {@link Tag#getKey() key}. - *

- * This method requires the authenticated user to be authorized to perform the operation. The authorization is - * checked by the {@link OpaService#canReadTags(String, List)} method using the {@link Tag}s of - * the related {@link SecretEntity}. + * + *

This method requires the authenticated user to be authorized to perform the operation. The + * authorization is checked by the {@link OpaService#canReadTags(String, List)} method using the + * {@link Tag}s of the related {@link SecretEntity}. * * @param secretId the id of the {@link SecretShare} whose {@link Tag} to be retrieved. * @param key the {@link Tag#getKey() key} of the {@link Tag} to be retrieved. @@ -444,30 +462,34 @@ public List retrieveTags(UUID secretId, String authorizedUserId) throws CsO * @throws CsOpaException if an error occurred while evaluating the policy */ @Transactional(readOnly = true) - public Tag retrieveTag(UUID secretId, String key, String authorizedUserId) throws CsOpaException, UnauthorizedException { - SecretEntity secretEntityReference = secretEntityRepository.findById(secretId.toString()) - .orElseThrow(() -> + public Tag retrieveTag(UUID secretId, String key, String authorizedUserId) + throws CsOpaException, UnauthorizedException { + SecretEntity secretEntityReference = + secretEntityRepository + .findById(secretId.toString()) + .orElseThrow( + () -> new NotFoundException( - String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); if (!opaService.canReadTags(authorizedUserId, setToTagList(secretEntityReference.getTags()))) { - throw new UnauthorizedException("User is not authorized to read tags for this secret"); + throw new UnauthorizedException("User is not authorized to read tags for this secret"); } return tagRepository - .findBySecretAndKey(secretEntityReference, key) - .map(TagEntity::toTag) - .orElseThrow( - () -> - new NotFoundException( - String.format( - NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, key, secretId))); + .findBySecretAndKey(secretEntityReference, key) + .map(TagEntity::toTag) + .orElseThrow( + () -> + new NotFoundException( + String.format( + NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, key, secretId))); } /** * Updates an existing {@link Tag} linked to an {@link SecretEntity} with the given id. - *

- * This method requires the authenticated user to be authorized to perform the operation. The authorization is - * checked by the {@link OpaService#canUpdateTags(String, List)} method using the {@link Tag}s of - * the related {@link SecretEntity}. + * + *

This method requires the authenticated user to be authorized to perform the operation. The + * authorization is checked by the {@link OpaService#canUpdateTags(String, List)} method using the + * {@link Tag}s of the related {@link SecretEntity}. * * @param secretId the id of the {@link SecretShare} whose {@link Tag} to be updated. * @param tag the new tag @@ -479,38 +501,43 @@ public Tag retrieveTag(UUID secretId, String key, String authorizedUserId) throw * @throws CsOpaException if an error occurred while evaluating the policy */ @Transactional - public void updateTag(UUID secretId, Tag tag, String authorizedUserId) throws CsOpaException, UnauthorizedException { + public void updateTag(UUID secretId, Tag tag, String authorizedUserId) + throws CsOpaException, UnauthorizedException { if (tagIsReserved(tag)) { throw new IllegalArgumentException( - String.format(IS_RESERVED_KEY_EXCEPTION_MSG, tag.getKey())); + String.format(IS_RESERVED_KEY_EXCEPTION_MSG, tag.getKey())); } - SecretEntity secretEntityReference = secretEntityRepository.findById(secretId.toString()) - .orElseThrow(() -> + SecretEntity secretEntityReference = + secretEntityRepository + .findById(secretId.toString()) + .orElseThrow( + () -> new NotFoundException( - String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); - if (!opaService.canUpdateTags(authorizedUserId, setToTagList(secretEntityReference.getTags()))) { - throw new UnauthorizedException("User is not authorized to update tags for this secret"); + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + if (!opaService.canUpdateTags( + authorizedUserId, setToTagList(secretEntityReference.getTags()))) { + throw new UnauthorizedException("User is not authorized to update tags for this secret"); } TagEntity existingTag = - tagRepository - .findBySecretAndKey(secretEntityReference, tag.getKey()) - .orElseThrow( - () -> - new NotFoundException( - String.format( - NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, - tag.getKey(), - secretId))); + tagRepository + .findBySecretAndKey(secretEntityReference, tag.getKey()) + .orElseThrow( + () -> + new NotFoundException( + String.format( + NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, + tag.getKey(), + secretId))); tagRepository.save( - existingTag.setValue(tag.getValue()).setValueType(tag.getValueType().toString())); + existingTag.setValue(tag.getValue()).setValueType(tag.getValueType().toString())); } /** * Deletes an existing {@link Tag} linked to an {@link SecretEntity} with the given id. - *

- * This method requires the authenticated user to be authorized to perform the operation. The authorization is - * checked by the {@link OpaService#canDeleteTags(String, List)} method using the {@link Tag}s of - * the related {@link SecretEntity}. + * + *

This method requires the authenticated user to be authorized to perform the operation. The + * authorization is checked by the {@link OpaService#canDeleteTags(String, List)} method using the + * {@link Tag}s of the related {@link SecretEntity}. * * @param secretId the id of the {@link SecretShare} whose {@link Tag} to be deleted. * @param key the {@link Tag#getKey() key} of the {@link Tag} to be deleted. @@ -522,25 +549,34 @@ public void updateTag(UUID secretId, Tag tag, String authorizedUserId) throws Cs * @throws CsOpaException if an error occurred while evaluating the policy */ @Transactional - public void deleteTag(UUID secretId, String key, String authorizedUserId) throws CsOpaException, UnauthorizedException { + public void deleteTag(UUID secretId, String key, String authorizedUserId) + throws CsOpaException, UnauthorizedException { if (RESERVED_TAG_KEYS.contains(key)) { throw new IllegalArgumentException(String.format(IS_RESERVED_KEY_EXCEPTION_MSG, key)); } - SecretEntity secretEntityReference = secretEntityRepository.findById(secretId.toString()) - .orElseThrow(() -> + SecretEntity secretEntityReference = + secretEntityRepository + .findById(secretId.toString()) + .orElseThrow( + () -> new NotFoundException( - String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId))); - if (!opaService.canDeleteTags(authorizedUserId, setToTagList(secretEntityReference.getTags()))) { + if (!opaService.canDeleteTags( + authorizedUserId, setToTagList(secretEntityReference.getTags()))) { throw new UnauthorizedException("User is not authorized to delete tags for this secret"); } - secretEntityReference.getTags().remove(tagRepository - .findBySecretAndKey(secretEntityReference, key).orElseThrow( + secretEntityReference + .getTags() + .remove( + tagRepository + .findBySecretAndKey(secretEntityReference, key) + .orElseThrow( () -> - new NotFoundException( - String.format( - NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, - key, - secretId)))); + new NotFoundException( + String.format( + NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, + key, + secretId)))); } } diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/IntraVcpController.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/IntraVcpController.java index 7120826..f35d027 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/IntraVcpController.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/IntraVcpController.java @@ -6,6 +6,9 @@ */ package io.carbynestack.amphora.service.rest; +import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.*; +import static org.springframework.util.Assert.notNull; + import io.carbynestack.amphora.common.SecretShare; import io.carbynestack.amphora.common.Tag; import io.carbynestack.amphora.common.exceptions.AmphoraServiceException; @@ -14,6 +17,8 @@ import io.carbynestack.amphora.service.exceptions.NotFoundException; import io.carbynestack.amphora.service.exceptions.UnauthorizedException; import io.carbynestack.amphora.service.persistence.metadata.StorageService; +import java.net.URI; +import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -24,12 +29,6 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import java.net.URI; -import java.util.UUID; - -import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.*; -import static org.springframework.util.Assert.notNull; - @Slf4j @RestController @RequiredArgsConstructor(onConstructor_ = @Autowired) @@ -69,12 +68,14 @@ public ResponseEntity uploadSecretShare(@RequestBody SecretShare secretShar * @return {@link HttpStatus#OK} with the {@link SecretShare} if successful. * @throws AmphoraServiceException if an {@link SecretShare} exists but could not be retrieved. * @throws NotFoundException if no {@link SecretShare} with the given id exists - * @throws UnauthorizedException if the requesting Program is not authorized to access the {@link SecretShare} + * @throws UnauthorizedException if the requesting Program is not authorized to access the {@link + * SecretShare} * @throws CsOpaException if an error occurred while evaluating the OPA policy */ @GetMapping(path = "/{" + SECRET_ID_PARAMETER + "}") - public ResponseEntity downloadSecretShare(@PathVariable UUID secretId, - @RequestParam String programId) throws UnauthorizedException, CsOpaException { + public ResponseEntity downloadSecretShare( + @PathVariable UUID secretId, @RequestParam String programId) + throws UnauthorizedException, CsOpaException { notNull(programId, "ProgramId must not be null"); return new ResponseEntity<>(storageService.useSecretShare(secretId, programId), HttpStatus.OK); diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/MaskedInputController.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/MaskedInputController.java index 919ef54..c4bce67 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/MaskedInputController.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/MaskedInputController.java @@ -16,10 +16,9 @@ import io.carbynestack.amphora.common.Tag; import io.carbynestack.amphora.service.exceptions.AlreadyExistsException; import io.carbynestack.amphora.service.exceptions.UnauthorizedException; +import io.carbynestack.amphora.service.opa.JwtReader; import io.carbynestack.amphora.service.persistence.metadata.StorageService; import java.net.URI; - -import io.carbynestack.amphora.service.opa.JwtReader; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -52,16 +51,17 @@ public class MaskedInputController { * @throws AlreadyExistsException if an {@link SecretShare} with the given id already exists. */ @PostMapping - public ResponseEntity upload(@RequestHeader("Authorization") String authorizationHeader, - @RequestBody MaskedInput maskedInput) throws UnauthorizedException { + public ResponseEntity upload( + @RequestHeader("Authorization") String authorizationHeader, + @RequestBody MaskedInput maskedInput) + throws UnauthorizedException { notNull(maskedInput, "MaskedInput must not be null"); notEmpty(maskedInput.getData(), "MaskedInput data must not be empty"); return new ResponseEntity<>( ServletUriComponentsBuilder.fromCurrentRequestUri() .pathSegment( - storageService.createSecret( - maskedInput, - jwtReader.extractUserIdFromAuthHeader(authorizationHeader))) + storageService.createSecret( + maskedInput, jwtReader.extractUserIdFromAuthHeader(authorizationHeader))) .build() .toUri(), CREATED); diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/RestControllerExceptionHandler.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/RestControllerExceptionHandler.java index 71fb037..e2dcb68 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/RestControllerExceptionHandler.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/RestControllerExceptionHandler.java @@ -53,9 +53,9 @@ protected ResponseEntity handleBadRequestException(Exception e) @ExceptionHandler({UnauthorizedException.class}) protected ResponseEntity handleUnauthorizedException(UnauthorizedException e) throws JsonProcessingException { - logger.debug("Handling Unauthorized Error", e); - return new ResponseEntity<>( - OBJECT_WRITER.writeValueAsString(e.getMessage()), HttpStatus.UNAUTHORIZED); + logger.debug("Handling Unauthorized Error", e); + return new ResponseEntity<>( + OBJECT_WRITER.writeValueAsString(e.getMessage()), HttpStatus.UNAUTHORIZED); } @ExceptionHandler({AmphoraServiceException.class, CastorServiceException.class, Exception.class}) diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/SecretShareController.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/SecretShareController.java index d8719fe..d7fcfc2 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/SecretShareController.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/SecretShareController.java @@ -15,8 +15,8 @@ import io.carbynestack.amphora.service.exceptions.CsOpaException; import io.carbynestack.amphora.service.exceptions.NotFoundException; import io.carbynestack.amphora.service.exceptions.UnauthorizedException; -import io.carbynestack.amphora.service.persistence.metadata.StorageService; import io.carbynestack.amphora.service.opa.JwtReader; +import io.carbynestack.amphora.service.persistence.metadata.StorageService; import io.vavr.control.Try; import java.io.UnsupportedEncodingException; import java.util.*; @@ -111,10 +111,12 @@ public ResponseEntity getObjectList( public ResponseEntity getSecretShare( @RequestHeader("Authorization") String authorizationHeader, @PathVariable final UUID secretId, - @RequestParam(value = REQUEST_ID_PARAMETER) final UUID requestId) throws UnauthorizedException, CsOpaException { + @RequestParam(value = REQUEST_ID_PARAMETER) final UUID requestId) + throws UnauthorizedException, CsOpaException { Assert.notNull(requestId, "Request identifier must not be omitted"); - SecretShare secretShare = storageService.getSecretShare(secretId, - jwtReader.extractUserIdFromAuthHeader(authorizationHeader)); + SecretShare secretShare = + storageService.getSecretShare( + secretId, jwtReader.extractUserIdFromAuthHeader(authorizationHeader)); OutputDeliveryObject outputDeliveryObject = outputDeliveryService.computeOutputDeliveryObject(secretShare, requestId); return new ResponseEntity<>( @@ -131,10 +133,10 @@ public ResponseEntity getSecretShare( */ @DeleteMapping(path = "/{" + SECRET_ID_PARAMETER + "}") public ResponseEntity deleteSecretShare( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable UUID secretId) throws UnauthorizedException, CsOpaException { - storageService.deleteSecret(secretId, - jwtReader.extractUserIdFromAuthHeader(authorizationHeader)); + @RequestHeader("Authorization") String authorizationHeader, @PathVariable UUID secretId) + throws UnauthorizedException, CsOpaException { + storageService.deleteSecret( + secretId, jwtReader.extractUserIdFromAuthHeader(authorizationHeader)); return new ResponseEntity<>(HttpStatus.OK); } diff --git a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/TagsController.java b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/TagsController.java index cc4d0b9..d9895bd 100644 --- a/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/TagsController.java +++ b/amphora-service/src/main/java/io/carbynestack/amphora/service/rest/TagsController.java @@ -14,12 +14,11 @@ import io.carbynestack.amphora.service.exceptions.CsOpaException; import io.carbynestack.amphora.service.exceptions.NotFoundException; import io.carbynestack.amphora.service.exceptions.UnauthorizedException; +import io.carbynestack.amphora.service.opa.JwtReader; import io.carbynestack.amphora.service.persistence.metadata.StorageService; import java.net.URI; import java.util.List; import java.util.UUID; - -import io.carbynestack.amphora.service.opa.JwtReader; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -47,13 +46,12 @@ public class TagsController { */ @GetMapping public ResponseEntity> getTags( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable UUID secretId) throws UnauthorizedException, CsOpaException { + @RequestHeader("Authorization") String authorizationHeader, @PathVariable UUID secretId) + throws UnauthorizedException, CsOpaException { return new ResponseEntity<>( - storageService.retrieveTags( - secretId, - jwtReader.extractUserIdFromAuthHeader(authorizationHeader)), - HttpStatus.OK); + storageService.retrieveTags( + secretId, jwtReader.extractUserIdFromAuthHeader(authorizationHeader)), + HttpStatus.OK); } /** @@ -71,16 +69,16 @@ public ResponseEntity> getTags( @Transactional @PostMapping public ResponseEntity createTag( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable UUID secretId, @RequestBody Tag tag) throws UnauthorizedException, CsOpaException { + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable UUID secretId, + @RequestBody Tag tag) + throws UnauthorizedException, CsOpaException { Assert.notNull(tag, "Tag must not be empty"); return new ResponseEntity<>( ServletUriComponentsBuilder.fromCurrentRequestUri() .pathSegment( - storageService.storeTag( - secretId, - tag, - jwtReader.extractUserIdFromAuthHeader(authorizationHeader))) + storageService.storeTag( + secretId, tag, jwtReader.extractUserIdFromAuthHeader(authorizationHeader))) .build() .toUri(), HttpStatus.CREATED); @@ -103,13 +101,13 @@ public ResponseEntity createTag( @Transactional @PutMapping public ResponseEntity updateTags( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable UUID secretId, @RequestBody List tags) throws UnauthorizedException, CsOpaException { + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable UUID secretId, + @RequestBody List tags) + throws UnauthorizedException, CsOpaException { Assert.notEmpty(tags, "At least one tag must be given."); storageService.replaceTags( - secretId, - tags, - jwtReader.extractUserIdFromAuthHeader(authorizationHeader)); + secretId, tags, jwtReader.extractUserIdFromAuthHeader(authorizationHeader)); return new ResponseEntity<>(HttpStatus.OK); } @@ -124,14 +122,14 @@ public ResponseEntity updateTags( */ @GetMapping(path = "/{" + TAG_KEY_PARAMETER + ":.+}") public ResponseEntity getTag( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable UUID secretId, @PathVariable String tagKey) throws UnauthorizedException, CsOpaException { + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable UUID secretId, + @PathVariable String tagKey) + throws UnauthorizedException, CsOpaException { return new ResponseEntity<>( - storageService.retrieveTag( - secretId, - tagKey, - jwtReader.extractUserIdFromAuthHeader(authorizationHeader)), - HttpStatus.OK); + storageService.retrieveTag( + secretId, tagKey, jwtReader.extractUserIdFromAuthHeader(authorizationHeader)), + HttpStatus.OK); } /** @@ -152,17 +150,18 @@ public ResponseEntity getTag( @Transactional @PutMapping(path = "/{" + TAG_KEY_PARAMETER + ":.+}") public ResponseEntity putTag( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable UUID secretId, @PathVariable String tagKey, @RequestBody Tag tag) throws UnauthorizedException, CsOpaException { + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable UUID secretId, + @PathVariable String tagKey, + @RequestBody Tag tag) + throws UnauthorizedException, CsOpaException { Assert.notNull(tag, "Tag must not be empty"); if (!tagKey.equals(tag.getKey())) { throw new IllegalArgumentException( String.format("The defined key and tag data do not match.%n%s <> %s", tagKey, tag)); } storageService.updateTag( - secretId, - tag, - jwtReader.extractUserIdFromAuthHeader(authorizationHeader)); + secretId, tag, jwtReader.extractUserIdFromAuthHeader(authorizationHeader)); return new ResponseEntity<>(HttpStatus.OK); } @@ -180,11 +179,12 @@ public ResponseEntity putTag( @Transactional @DeleteMapping(path = "/{" + TAG_KEY_PARAMETER + ":.+}") public ResponseEntity deleteTag( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable UUID secretId, @PathVariable String tagKey) throws UnauthorizedException, CsOpaException { - storageService.deleteTag(secretId, - tagKey, - jwtReader.extractUserIdFromAuthHeader(authorizationHeader)); + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable UUID secretId, + @PathVariable String tagKey) + throws UnauthorizedException, CsOpaException { + storageService.deleteTag( + secretId, tagKey, jwtReader.extractUserIdFromAuthHeader(authorizationHeader)); return new ResponseEntity<>(HttpStatus.OK); } } diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/AmphoraServiceSystemTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/AmphoraServiceSystemTest.java index 299d5f0..14c83ee 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/AmphoraServiceSystemTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/AmphoraServiceSystemTest.java @@ -24,7 +24,6 @@ import io.carbynestack.amphora.common.Tag; import io.carbynestack.amphora.service.exceptions.UnauthorizedException; import io.carbynestack.amphora.service.opa.OpaClient; -import io.carbynestack.amphora.service.persistence.metadata.StorageService; import io.carbynestack.amphora.service.testconfig.PersistenceTestEnvironment; import io.carbynestack.amphora.service.testconfig.ReusableMinioContainer; import io.carbynestack.amphora.service.testconfig.ReusablePostgreSQLContainer; @@ -40,7 +39,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; @@ -66,8 +64,9 @@ public class AmphoraServiceSystemTest { private final UUID testSecretId2 = UUID.fromString("82a73814-321c-4261-abcd-27c6c0ebfb27"); private final UUID testSecretId3 = UUID.fromString("82a73814-321c-4261-abcd-27c6c0ebfb28"); private final UUID testSecretId4 = UUID.fromString("82a73814-321c-4261-abcd-27c6c0ebfb29"); - private final String bearerToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5Njk0OTgyLWQzMTAtNDBkOC04ZDk4LTczOWI1ZGZjNWUyNiIsInR5cCI6IkpXVCJ9.eyJhbXIiOlsicGFzc3dvcmQiXSwiYXRfaGFzaCI6InowbjhudTNJQ19HaXN3bmFTWjgwZ2ciLCJhdWQiOlsiOGExYTIwNzUtMzY3Yi00ZGU1LTgyODgtMGMyNzQ1OTMzMmI3Il0sImF1dGhfdGltZSI6MTczMTUwMDQ0OSwiZXhwIjoxNzMxNTA0NDIyLCJpYXQiOjE3MzE1MDA4MjIsImlzcyI6Imh0dHA6Ly8xNzIuMTguMS4xMjguc3NsaXAuaW8vaWFtL29hdXRoIiwianRpIjoiZTlhMmQxYzQtZGViNy00MTgwLWE0M2YtN2QwNTZhYjNlNTk3Iiwibm9uY2UiOiJnV1JVZjhxTERaeDZpOFNhMXpMdm9IX2tSQ01OWll2WTE0WTFsLWNBU0tVIiwicmF0IjoxNzMxNTAwODIyLCJzaWQiOiJlNGVkOTc2Mi0yMmNlLTQyYzEtOTU3NC01MDVkYjAyMThhNDYiLCJzdWIiOiJhZmMwMTE3Zi1jOWNkLTRkOGMtYWNlZS1mYTE0MzNjYTBmZGQifQ.OACqa6WjpAeZbHR54b3p7saUk9plTdXlZsou41E-gfC7WxCG7ZEKfDPKXUky-r20oeIt1Ov3S2QL6Kefe5dTXEC6nhKGxeClg8ys56_FPcx_neI-p09_pSWOkMx7DHP65giaP7UubyyInVpE-2Eu1o6TpoheahNQfCahKDsJmJ-4Vvts3wA79UMfOI0WHO4vLaaG6DRAZQK_dv7ltw3p_WlncpaQAtHwY9iVhtdB3LtAI39EjplCoAF0c9uQO6W7KHWUlj24l2kc564bsJgZSrYvezw6b2-FY7YisVnicSyAORpeqhWEpLltH3D8I1NtHlSYMJhWuVZbBhAm7Iz6q1-W-Q9ttvdPchdwPSASFRkrjrdIyQf6MsFrItKzUxYck57EYL4GkyN9MWvMNxL1UTtkzGsFEczUVsJFm8OQpulYXIFZksmnPTBB0KuUUvEZ-xih8V1HsMRoHvbiCLaDJwjOFKzPevVggsSMysPKR52UAZJDZLTeHBnVCtQ3rro6T0RxNg94lXypz0AmfsGnoTF34u4FmMxzoeFZ9N5zmEpOnMRqLs7Sb3FnLL-IMitc9_2bsHuFbBJl8KbiGHBQihK5v5SIa292L7P9ChsxomWVhM29qHNFuXQMwFUr57hmveNh2Fz9mduZ5h2hLUuDf5xc6u9rSxy3_e3t_xBuUT4"; - private final String authorizedUserId ="afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; + private final String bearerToken = + "eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5Njk0OTgyLWQzMTAtNDBkOC04ZDk4LTczOWI1ZGZjNWUyNiIsInR5cCI6IkpXVCJ9.eyJhbXIiOlsicGFzc3dvcmQiXSwiYXRfaGFzaCI6InowbjhudTNJQ19HaXN3bmFTWjgwZ2ciLCJhdWQiOlsiOGExYTIwNzUtMzY3Yi00ZGU1LTgyODgtMGMyNzQ1OTMzMmI3Il0sImF1dGhfdGltZSI6MTczMTUwMDQ0OSwiZXhwIjoxNzMxNTA0NDIyLCJpYXQiOjE3MzE1MDA4MjIsImlzcyI6Imh0dHA6Ly8xNzIuMTguMS4xMjguc3NsaXAuaW8vaWFtL29hdXRoIiwianRpIjoiZTlhMmQxYzQtZGViNy00MTgwLWE0M2YtN2QwNTZhYjNlNTk3Iiwibm9uY2UiOiJnV1JVZjhxTERaeDZpOFNhMXpMdm9IX2tSQ01OWll2WTE0WTFsLWNBU0tVIiwicmF0IjoxNzMxNTAwODIyLCJzaWQiOiJlNGVkOTc2Mi0yMmNlLTQyYzEtOTU3NC01MDVkYjAyMThhNDYiLCJzdWIiOiJhZmMwMTE3Zi1jOWNkLTRkOGMtYWNlZS1mYTE0MzNjYTBmZGQifQ.OACqa6WjpAeZbHR54b3p7saUk9plTdXlZsou41E-gfC7WxCG7ZEKfDPKXUky-r20oeIt1Ov3S2QL6Kefe5dTXEC6nhKGxeClg8ys56_FPcx_neI-p09_pSWOkMx7DHP65giaP7UubyyInVpE-2Eu1o6TpoheahNQfCahKDsJmJ-4Vvts3wA79UMfOI0WHO4vLaaG6DRAZQK_dv7ltw3p_WlncpaQAtHwY9iVhtdB3LtAI39EjplCoAF0c9uQO6W7KHWUlj24l2kc564bsJgZSrYvezw6b2-FY7YisVnicSyAORpeqhWEpLltH3D8I1NtHlSYMJhWuVZbBhAm7Iz6q1-W-Q9ttvdPchdwPSASFRkrjrdIyQf6MsFrItKzUxYck57EYL4GkyN9MWvMNxL1UTtkzGsFEczUVsJFm8OQpulYXIFZksmnPTBB0KuUUvEZ-xih8V1HsMRoHvbiCLaDJwjOFKzPevVggsSMysPKR52UAZJDZLTeHBnVCtQ3rro6T0RxNg94lXypz0AmfsGnoTF34u4FmMxzoeFZ9N5zmEpOnMRqLs7Sb3FnLL-IMitc9_2bsHuFbBJl8KbiGHBQihK5v5SIa292L7P9ChsxomWVhM29qHNFuXQMwFUr57hmveNh2Fz9mduZ5h2hLUuDf5xc6u9rSxy3_e3t_xBuUT4"; + private final String authorizedUserId = "afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; @Container public static ReusableRedisContainer reusableRedisContainer = @@ -81,8 +80,7 @@ public class AmphoraServiceSystemTest { public static ReusablePostgreSQLContainer reusablePostgreSQLContainer = ReusablePostgreSQLContainer.getInstance(); - @MockBean - private OpaClient opaClientMock; + @MockBean private OpaClient opaClientMock; @Autowired private TestRestTemplate restTemplate; @@ -227,8 +225,12 @@ void givenSuccessfulRequest_whenDeleteSecret_thenReturnRemoveAndDoNoLongerReturn HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(bearerToken); HttpEntity entity = new HttpEntity<>(headers); - ResponseEntity response = restTemplate.exchange(new URI(String.format("/secret-shares/%s", testSecretShare1.getSecretId())), - HttpMethod.DELETE, entity, Void.class); + ResponseEntity response = + restTemplate.exchange( + new URI(String.format("/secret-shares/%s", testSecretShare1.getSecretId())), + HttpMethod.DELETE, + entity, + Void.class); assertTrue("Request must be successful.", response.getStatusCode().is2xxSuccessful()); MetadataPage responsePostDel = restTemplate.getForObject(SECRET_SHARES_ENDPOINT, MetadataPage.class); diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/calculation/SecretShareUtilTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/calculation/SecretShareUtilTest.java index d469288..44512b5 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/calculation/SecretShareUtilTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/calculation/SecretShareUtilTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023-2024 - for information on the respective copyright owner + * Copyright (c) 2023 - for information on the respective copyright owner * see the NOTICE file and/or the repository https://github.com/carbynestack/amphora. * * SPDX-License-Identifier: Apache-2.0 @@ -19,8 +19,10 @@ import io.carbynestack.mpspdz.integration.MpSpdzIntegrationUtils; import java.math.BigInteger; import java.nio.ByteBuffer; -import java.util.*; - +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/JwtReaderTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/JwtReaderTest.java index 7f90826..33e8f2a 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/JwtReaderTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/JwtReaderTest.java @@ -7,62 +7,73 @@ package io.carbynestack.amphora.service.opa; +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + import io.carbynestack.amphora.service.exceptions.UnauthorizedException; +import java.nio.charset.StandardCharsets; import org.bouncycastle.util.encoders.Base64; import org.junit.jupiter.api.Test; -import java.nio.charset.StandardCharsets; - -import static org.junit.Assert.assertThrows; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - class JwtReaderTest { - @Test - void givenTokenProvided_whenExtractSubject_thenReturnSubject() throws UnauthorizedException { - JwtReader jwtReader = new JwtReader("sub"); - String header = "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5Njk0OTgyLWQzMTAtNDBkOC04ZDk4LTczOWI1ZGZjNWUyNiIsInR5cCI6IkpXVCJ9.eyJhbXIiOlsicGFzc3dvcmQiXSwiYXRfaGFzaCI6InowbjhudTNJQ19HaXN3bmFTWjgwZ2ciLCJhdWQiOlsiOGExYTIwNzUtMzY3Yi00ZGU1LTgyODgtMGMyNzQ1OTMzMmI3Il0sImF1dGhfdGltZSI6MTczMTUwMDQ0OSwiZXhwIjoxNzMxNTA0NDIyLCJpYXQiOjE3MzE1MDA4MjIsImlzcyI6Imh0dHA6Ly8xNzIuMTguMS4xMjguc3NsaXAuaW8vaWFtL29hdXRoIiwianRpIjoiZTlhMmQxYzQtZGViNy00MTgwLWE0M2YtN2QwNTZhYjNlNTk3Iiwibm9uY2UiOiJnV1JVZjhxTERaeDZpOFNhMXpMdm9IX2tSQ01OWll2WTE0WTFsLWNBU0tVIiwicmF0IjoxNzMxNTAwODIyLCJzaWQiOiJlNGVkOTc2Mi0yMmNlLTQyYzEtOTU3NC01MDVkYjAyMThhNDYiLCJzdWIiOiJhZmMwMTE3Zi1jOWNkLTRkOGMtYWNlZS1mYTE0MzNjYTBmZGQifQ.OACqa6WjpAeZbHR54b3p7saUk9plTdXlZsou41E-gfC7WxCG7ZEKfDPKXUky-r20oeIt1Ov3S2QL6Kefe5dTXEC6nhKGxeClg8ys56_FPcx_neI-p09_pSWOkMx7DHP65giaP7UubyyInVpE-2Eu1o6TpoheahNQfCahKDsJmJ-4Vvts3wA79UMfOI0WHO4vLaaG6DRAZQK_dv7ltw3p_WlncpaQAtHwY9iVhtdB3LtAI39EjplCoAF0c9uQO6W7KHWUlj24l2kc564bsJgZSrYvezw6b2-FY7YisVnicSyAORpeqhWEpLltH3D8I1NtHlSYMJhWuVZbBhAm7Iz6q1-W-Q9ttvdPchdwPSASFRkrjrdIyQf6MsFrItKzUxYck57EYL4GkyN9MWvMNxL1UTtkzGsFEczUVsJFm8OQpulYXIFZksmnPTBB0KuUUvEZ-xih8V1HsMRoHvbiCLaDJwjOFKzPevVggsSMysPKR52UAZJDZLTeHBnVCtQ3rro6T0RxNg94lXypz0AmfsGnoTF34u4FmMxzoeFZ9N5zmEpOnMRqLs7Sb3FnLL-IMitc9_2bsHuFbBJl8KbiGHBQihK5v5SIa292L7P9ChsxomWVhM29qHNFuXQMwFUr57hmveNh2Fz9mduZ5h2hLUuDf5xc6u9rSxy3_e3t_xBuUT4"; - String result = jwtReader.extractUserIdFromAuthHeader(header); - String expectedSubject = "afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; - assertEquals(expectedSubject, result); - } + @Test + void givenTokenProvided_whenExtractSubject_thenReturnSubject() throws UnauthorizedException { + JwtReader jwtReader = new JwtReader("sub"); + String header = + "Bearer" + + " eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5Njk0OTgyLWQzMTAtNDBkOC04ZDk4LTczOWI1ZGZjNWUyNiIsInR5cCI6IkpXVCJ9.eyJhbXIiOlsicGFzc3dvcmQiXSwiYXRfaGFzaCI6InowbjhudTNJQ19HaXN3bmFTWjgwZ2ciLCJhdWQiOlsiOGExYTIwNzUtMzY3Yi00ZGU1LTgyODgtMGMyNzQ1OTMzMmI3Il0sImF1dGhfdGltZSI6MTczMTUwMDQ0OSwiZXhwIjoxNzMxNTA0NDIyLCJpYXQiOjE3MzE1MDA4MjIsImlzcyI6Imh0dHA6Ly8xNzIuMTguMS4xMjguc3NsaXAuaW8vaWFtL29hdXRoIiwianRpIjoiZTlhMmQxYzQtZGViNy00MTgwLWE0M2YtN2QwNTZhYjNlNTk3Iiwibm9uY2UiOiJnV1JVZjhxTERaeDZpOFNhMXpMdm9IX2tSQ01OWll2WTE0WTFsLWNBU0tVIiwicmF0IjoxNzMxNTAwODIyLCJzaWQiOiJlNGVkOTc2Mi0yMmNlLTQyYzEtOTU3NC01MDVkYjAyMThhNDYiLCJzdWIiOiJhZmMwMTE3Zi1jOWNkLTRkOGMtYWNlZS1mYTE0MzNjYTBmZGQifQ.OACqa6WjpAeZbHR54b3p7saUk9plTdXlZsou41E-gfC7WxCG7ZEKfDPKXUky-r20oeIt1Ov3S2QL6Kefe5dTXEC6nhKGxeClg8ys56_FPcx_neI-p09_pSWOkMx7DHP65giaP7UubyyInVpE-2Eu1o6TpoheahNQfCahKDsJmJ-4Vvts3wA79UMfOI0WHO4vLaaG6DRAZQK_dv7ltw3p_WlncpaQAtHwY9iVhtdB3LtAI39EjplCoAF0c9uQO6W7KHWUlj24l2kc564bsJgZSrYvezw6b2-FY7YisVnicSyAORpeqhWEpLltH3D8I1NtHlSYMJhWuVZbBhAm7Iz6q1-W-Q9ttvdPchdwPSASFRkrjrdIyQf6MsFrItKzUxYck57EYL4GkyN9MWvMNxL1UTtkzGsFEczUVsJFm8OQpulYXIFZksmnPTBB0KuUUvEZ-xih8V1HsMRoHvbiCLaDJwjOFKzPevVggsSMysPKR52UAZJDZLTeHBnVCtQ3rro6T0RxNg94lXypz0AmfsGnoTF34u4FmMxzoeFZ9N5zmEpOnMRqLs7Sb3FnLL-IMitc9_2bsHuFbBJl8KbiGHBQihK5v5SIa292L7P9ChsxomWVhM29qHNFuXQMwFUr57hmveNh2Fz9mduZ5h2hLUuDf5xc6u9rSxy3_e3t_xBuUT4"; + String result = jwtReader.extractUserIdFromAuthHeader(header); + String expectedSubject = "afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; + assertEquals(expectedSubject, result); + } - @Test - void givenNoTokenProvided_whenExtractSubject_thenThrowUnauthorizedException() throws UnauthorizedException { - JwtReader jwtReader = new JwtReader("sub"); - String invalidToken = "{\"auth_time\": 1731500449,\n" + - " \"exp\": 1731504422,\n" + - " \"iat\": 1731500822,\n" + - " \"something\": {\n" + - " \"what\": \"is this\"\n"+ - " }}"; - assertThrows(UnauthorizedException.class, () -> jwtReader.extractUserIdFromAuthHeader( + @Test + void givenInvalidTokenProvided_whenExtractSubject_thenThrowUnauthorizedException() + throws UnauthorizedException { + JwtReader jwtReader = new JwtReader("sub"); + String invalidToken = + "{\"auth_time\": 1731500449,\n" + + " \"exp\": 1731504422,\n" + + " \"iat\": 1731500822,\n" + + " \"something\": {\n" + + " \"what\": \"is this\"\n" + + " }}"; + assertThrows( + UnauthorizedException.class, + () -> + jwtReader.extractUserIdFromAuthHeader( String.format( - "Bearer header.%s.signature", - Base64.toBase64String(invalidToken.getBytes(StandardCharsets.UTF_8))))); - } + "Bearer header.%s.signature", + Base64.toBase64String(invalidToken.getBytes(StandardCharsets.UTF_8))))); + } - @Test - void givenJwtOfInvalidFormat_whenExtractSubject_thenThrowUnauthorizedException() { - JwtReader jwtReader = new JwtReader("sub"); - assertThrows(UnauthorizedException.class, () -> - jwtReader.extractUserIdFromAuthHeader("Bearer invalid.jwt_missing_dot_token")); - } + @Test + void givenJwtOfInvalidFormat_whenExtractSubject_thenThrowUnauthorizedException() { + JwtReader jwtReader = new JwtReader("sub"); + assertThrows( + UnauthorizedException.class, + () -> jwtReader.extractUserIdFromAuthHeader("Bearer invalid.jwt_missing_dot_token")); + } - @Test - void givenJwtDataFieldOfInvalidFormat_whenExtractSubject_thenThrowUnauthorizedException() { - JwtReader jwtReader = new JwtReader("sub"); - String invalidDataJson = "{"; - assertThrows(UnauthorizedException.class, () -> - jwtReader.extractUserIdFromAuthHeader( - String.format("header.%s.signature", Base64.toBase64String(invalidDataJson.getBytes(StandardCharsets.UTF_8))))); - } + @Test + void givenJwtDataFieldOfInvalidFormat_whenExtractSubject_thenThrowUnauthorizedException() { + JwtReader jwtReader = new JwtReader("sub"); + String invalidDataJson = "{"; + assertThrows( + UnauthorizedException.class, + () -> + jwtReader.extractUserIdFromAuthHeader( + String.format( + "header.%s.signature", + Base64.toBase64String(invalidDataJson.getBytes(StandardCharsets.UTF_8))))); + } - @Test - void givenInvalidAuthHeader_whenExtractSubject_thenThrowUnauthorizedException() { - JwtReader jwtReader = new JwtReader("sub"); - assertThrows(UnauthorizedException.class, () -> - jwtReader.extractUserIdFromAuthHeader("invalid_auth_header missing Bearer prefix")); - } -} \ No newline at end of file + @Test + void givenInvalidAuthHeader_whenExtractSubject_thenThrowUnauthorizedException() { + JwtReader jwtReader = new JwtReader("sub"); + assertThrows( + UnauthorizedException.class, + () -> jwtReader.extractUserIdFromAuthHeader("invalid_auth_header missing Bearer prefix")); + } +} diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaClientTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaClientTest.java index 72b76ba..ec77015 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaClientTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaClientTest.java @@ -7,11 +7,21 @@ package io.carbynestack.amphora.service.opa; +import static io.carbynestack.amphora.service.opa.OpaService.READ_SECRET_ACTION_NAME; +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + import com.google.common.collect.Lists; import io.carbynestack.amphora.common.Tag; import io.carbynestack.amphora.service.exceptions.CsOpaException; import io.carbynestack.httpclient.CsHttpClient; import io.carbynestack.httpclient.CsHttpClientException; +import java.net.URI; +import java.util.List; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; @@ -21,150 +31,147 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.net.URI; -import java.util.List; - -import static io.carbynestack.amphora.service.opa.OpaService.READ_SECRET_ACTION_NAME; -import static org.junit.Assert.fail; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) class OpaClientTest { - private static final URI OPA_SERVICE_URI = URI.create("http://localhost:8081"); - private static final String POLICY_PACKAGE = "play"; - private static final String DEFAULT_POLICY_PACKAGE = "default"; - private static final String SUBJECT = "me"; - private static final List TAGS = Lists.newArrayList( - Tag.builder().key("created").value("yesterday").build(), - Tag.builder().key("owner").value("me").build() - ); - private static final OpaResult POSITIVE_RESULT; - static { - POSITIVE_RESULT = new OpaResult(); - POSITIVE_RESULT.setResult(true); + private static final URI OPA_SERVICE_URI = URI.create("http://localhost:8081"); + private static final String POLICY_PACKAGE = "play"; + private static final String DEFAULT_POLICY_PACKAGE = "default"; + private static final String SUBJECT = "me"; + private static final List TAGS = + Lists.newArrayList( + Tag.builder().key("created").value("yesterday").build(), + Tag.builder().key("owner").value("me").build()); + private static final OpaResult POSITIVE_RESULT; + + static { + POSITIVE_RESULT = new OpaResult(); + POSITIVE_RESULT.setResult(true); + } + + private static final OpaResult NEGATIVE_RESULT = new OpaResult(); + + @Mock private CsHttpClient csHttpClientMock = mock(CsHttpClient.class); + + @BeforeEach + public void setUp() { + reset(csHttpClientMock); + } + + @Test + void givenValidRequest_whenEvaluate_thenReturnTrue() + throws CsOpaException, CsHttpClientException { + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(OpaRequest.class); + when(csHttpClientMock.postForObject( + uriCaptor.capture(), requestCaptor.capture(), eq(OpaResult.class))) + .thenReturn(NEGATIVE_RESULT); + + OpaClient opaClient = new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); + opaClient + .newRequest() + .withPolicyPackage(POLICY_PACKAGE) + .withAction(READ_SECRET_ACTION_NAME) + .withSubject(SUBJECT) + .withTags(TAGS) + .evaluate(); + + URI actualUri = uriCaptor.getValue(); + MatcherAssert.assertThat(actualUri.toString(), Matchers.startsWith(OPA_SERVICE_URI.toString())); + MatcherAssert.assertThat( + actualUri.toString(), + Matchers.endsWith( + String.format("/v1/data/%s/%s", POLICY_PACKAGE, READ_SECRET_ACTION_NAME))); + OpaRequestBody actualRequestBody = requestCaptor.getValue().getInput(); + assertEquals(SUBJECT, actualRequestBody.getSubject()); + assertEquals(TAGS, actualRequestBody.getTags()); + } + + @Test + void givenNoPolicyPackageDefined_whenEvaluate_thenReturnUseDefaultPackage() + throws CsOpaException, CsHttpClientException { + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(OpaRequest.class); + when(csHttpClientMock.postForObject( + uriCaptor.capture(), requestCaptor.capture(), eq(OpaResult.class))) + .thenReturn(NEGATIVE_RESULT); + + OpaClient opaClient = new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); + opaClient + .newRequest() + .withAction(READ_SECRET_ACTION_NAME) + .withSubject(SUBJECT) + .withTags(TAGS) + .evaluate(); + + URI actualUri = uriCaptor.getValue(); + MatcherAssert.assertThat(actualUri.toString(), Matchers.startsWith(OPA_SERVICE_URI.toString())); + MatcherAssert.assertThat( + actualUri.toString(), + Matchers.endsWith( + String.format("/v1/data/%s/%s", DEFAULT_POLICY_PACKAGE, READ_SECRET_ACTION_NAME))); + OpaRequestBody actualRequestBody = requestCaptor.getValue().getInput(); + assertEquals(SUBJECT, actualRequestBody.getSubject()); + assertEquals(TAGS, actualRequestBody.getTags()); + } + + @Test + void givenOpaReturnsFalse_whenEvaluate_thenReturnFalse() + throws CsHttpClientException, CsOpaException { + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(OpaRequest.class); + when(csHttpClientMock.postForObject( + uriCaptor.capture(), requestCaptor.capture(), eq(OpaResult.class))) + .thenReturn(NEGATIVE_RESULT); + + OpaClient opaClient = new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); + boolean result = + opaClient + .newRequest() + .withAction(READ_SECRET_ACTION_NAME) + .withSubject(SUBJECT) + .withTags(TAGS) + .evaluate(); + + assertFalse(result, "must not be allowed"); + } + + @Test + void givenNoSubjectDefined_whenEvaluate_thenThrowException() { + OpaClient opaClient = new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); + try { + opaClient.newRequest().withAction(READ_SECRET_ACTION_NAME).withTags(TAGS).evaluate(); + fail("must throw exception"); + } catch (CsOpaException e) { + assertEquals("Subject is required to evaluate the policy", e.getMessage()); } - private static final OpaResult NEGATIVE_RESULT = new OpaResult(); - - @Mock - private CsHttpClient csHttpClientMock = mock(CsHttpClient.class); - - @BeforeEach - public void setUp() { - reset(csHttpClientMock); - } - - @Test - void givenValidRequest_whenEvaluate_thenReturnTrue() throws CsOpaException, CsHttpClientException { - ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(OpaRequest.class); - when(csHttpClientMock.postForObject(uriCaptor.capture(), requestCaptor.capture(), eq(OpaResult.class))) - .thenReturn(NEGATIVE_RESULT); - - OpaClient opaClient = new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); - opaClient.newRequest() - .withPolicyPackage(POLICY_PACKAGE) - .withAction(READ_SECRET_ACTION_NAME) - .withSubject(SUBJECT) - .withTags(TAGS) - .evaluate(); - - URI actualUri = uriCaptor.getValue(); - MatcherAssert.assertThat(actualUri.toString(), - Matchers.startsWith(OPA_SERVICE_URI.toString())); - MatcherAssert.assertThat( - actualUri.toString(), - Matchers.endsWith(String.format("/v1/data/%s/%s", POLICY_PACKAGE, READ_SECRET_ACTION_NAME))); - OpaRequestBody actualRequestBody = requestCaptor.getValue().getInput(); - assertEquals(SUBJECT, actualRequestBody.getSubject()); - assertEquals(TAGS, actualRequestBody.getTags()); - } - - @Test - void givenNoPolicyPackageDefined_whenEvaluate_thenReturnUseDefaultPackage() throws CsOpaException, CsHttpClientException { - ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(OpaRequest.class); - when(csHttpClientMock.postForObject(uriCaptor.capture(), requestCaptor.capture(), eq(OpaResult.class))) - .thenReturn(NEGATIVE_RESULT); - - OpaClient opaClient = new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); - opaClient.newRequest() - .withAction(READ_SECRET_ACTION_NAME) - .withSubject(SUBJECT) - .withTags(TAGS) - .evaluate(); - - URI actualUri = uriCaptor.getValue(); - MatcherAssert.assertThat(actualUri.toString(), - Matchers.startsWith(OPA_SERVICE_URI.toString())); - MatcherAssert.assertThat( - actualUri.toString(), - Matchers.endsWith(String.format("/v1/data/%s/%s", DEFAULT_POLICY_PACKAGE, READ_SECRET_ACTION_NAME))); - OpaRequestBody actualRequestBody = requestCaptor.getValue().getInput(); - assertEquals(SUBJECT, actualRequestBody.getSubject()); - assertEquals(TAGS, actualRequestBody.getTags()); - } - - @Test - void givenOpaReturnsFalse_whenEvaluate_thenReturnFalse() throws CsHttpClientException, CsOpaException { - ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(OpaRequest.class); - when(csHttpClientMock.postForObject(uriCaptor.capture(), requestCaptor.capture(), eq(OpaResult.class))) - .thenReturn(NEGATIVE_RESULT); - - OpaClient opaClient =new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); - boolean result = opaClient.newRequest() - .withAction(READ_SECRET_ACTION_NAME) - .withSubject(SUBJECT) - .withTags(TAGS) - .evaluate(); - - assertFalse(result, "must not be allowed"); - } - - @Test - void givenNoSubjectDefined_whenEvaluate_thenThrowException() { - OpaClient opaClient = new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); - try { - opaClient.newRequest() - .withAction(READ_SECRET_ACTION_NAME) - .withTags(TAGS) - .evaluate(); - fail("must throw exception"); - } catch (CsOpaException e) { - assertEquals("Subject is required to evaluate the policy", e.getMessage()); - } - } - - @Test - void givenNoActionDefined_whenEvaluate_thenThrowException() { - OpaClient opaClient = new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); - try { - opaClient.newRequest() - .withSubject(SUBJECT) - .withTags(TAGS) - .evaluate(); - fail("must throw exception"); - } catch (CsOpaException e) { - assertEquals("Action is required to evaluate the policy", e.getMessage()); - } - } - - @Test - void givenClientThrows_whenEvaluate_thenReturnFalse() throws CsHttpClientException, CsOpaException { - when(csHttpClientMock.postForObject(any(), any(), eq(OpaResult.class))) - .thenThrow(new CsHttpClientException("")); - - OpaClient opaClient =new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); - boolean result = opaClient.newRequest() - .withAction(READ_SECRET_ACTION_NAME) - .withSubject(SUBJECT) - .withTags(TAGS) - .evaluate(); - assertFalse(result, "must not be allowed"); + } + + @Test + void givenNoActionDefined_whenEvaluate_thenThrowException() { + OpaClient opaClient = new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); + try { + opaClient.newRequest().withSubject(SUBJECT).withTags(TAGS).evaluate(); + fail("must throw exception"); + } catch (CsOpaException e) { + assertEquals("Action is required to evaluate the policy", e.getMessage()); } -} \ No newline at end of file + } + + @Test + void givenClientThrows_whenEvaluate_thenReturnFalse() + throws CsHttpClientException, CsOpaException { + when(csHttpClientMock.postForObject(any(), any(), eq(OpaResult.class))) + .thenThrow(new CsHttpClientException("")); + + OpaClient opaClient = new OpaClient(csHttpClientMock, OPA_SERVICE_URI, DEFAULT_POLICY_PACKAGE); + boolean result = + opaClient + .newRequest() + .withAction(READ_SECRET_ACTION_NAME) + .withSubject(SUBJECT) + .withTags(TAGS) + .evaluate(); + assertFalse(result, "must not be allowed"); + } +} diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaResultTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaResultTest.java index a87c03d..a3aeab3 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaResultTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaResultTest.java @@ -7,25 +7,25 @@ package io.carbynestack.amphora.service.opa; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - class OpaResultTest { - private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper = new ObjectMapper(); - @Test - void givenResultIsTrue_whenParsed_thenIsAllowedShouldBeTrue() throws JsonProcessingException { - OpaResult result = objectMapper.readValue("{\"result\": true}", OpaResult.class); - assertTrue(result.isAllowed(), "isAllowed must be true"); - } + @Test + void givenResultIsTrue_whenParsed_thenIsAllowedShouldBeTrue() throws JsonProcessingException { + OpaResult result = objectMapper.readValue("{\"result\": true}", OpaResult.class); + assertTrue(result.isAllowed(), "isAllowed must be true"); + } - @Test - void givenResultIsEmpty_whenParsed_thenIsAllowedShouldBeFalse() throws JsonProcessingException { - OpaResult result = objectMapper.readValue("{}", OpaResult.class); - assertFalse(result.isAllowed(), "isAllowed must be false"); - } -} \ No newline at end of file + @Test + void givenResultIsEmpty_whenParsed_thenIsAllowedShouldBeFalse() throws JsonProcessingException { + OpaResult result = objectMapper.readValue("{}", OpaResult.class); + assertFalse(result.isAllowed(), "isAllowed must be false"); + } +} diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaServiceTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaServiceTest.java index 6dad392..c4d7759 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaServiceTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/opa/OpaServiceTest.java @@ -7,9 +7,17 @@ package io.carbynestack.amphora.service.opa; +import static io.carbynestack.amphora.service.opa.OpaService.READ_SECRET_ACTION_NAME; +import static io.carbynestack.amphora.service.opa.OpaService.USE_SECRET_ACTION_NAME; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + import com.google.common.collect.Lists; import io.carbynestack.amphora.common.Tag; import io.carbynestack.amphora.service.exceptions.CsOpaException; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -17,184 +25,162 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.List; - -import static io.carbynestack.amphora.service.opa.OpaService.READ_SECRET_ACTION_NAME; -import static io.carbynestack.amphora.service.opa.OpaService.USE_SECRET_ACTION_NAME; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) class OpaServiceTest { - private static final String POLICY_PACKAGE = "play"; - private static final String DEFAULT_POLICY_PACKAGE = "default"; - private static final String SUBJECT = "me"; - private static final List TAGS = Lists.newArrayList( - Tag.builder().key("created").value("yesterday").build(), - Tag.builder().key("owner").value("me").build() - ); - private static final Tag POLICY_TAG = Tag.builder() - .key(OpaService.POLICY_PACKAGE_TAG_KEY) - .value(POLICY_PACKAGE).build(); - private static final OpaResult POSITIVE_RESULT; - static { - POSITIVE_RESULT = new OpaResult(); - POSITIVE_RESULT.setResult(true); - } - - @Mock - private OpaClient opaClientMock = mock(OpaClient.class); - - @BeforeEach - public void setUp() { - reset(opaClientMock); - when(opaClientMock.newRequest()).thenReturn(new OpaClientRequest(opaClientMock, DEFAULT_POLICY_PACKAGE)); - } - - @Test - void givenPolicyDefinedInTag_whenIsAllowed_thenUsePolicyPackageProvided() throws CsOpaException { - ArgumentCaptor packageCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor subjectCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> tagsCaptor = ArgumentCaptor.forClass(List.class); - when(opaClientMock.isAllowed( - packageCaptor.capture(), actionCaptor.capture(), subjectCaptor.capture(), tagsCaptor.capture())) - .thenReturn(true); - - OpaService service = new OpaService(opaClientMock); - List testTags = Lists.newArrayList(TAGS); - testTags.add(POLICY_TAG); - boolean result = service.isAllowed(SUBJECT, READ_SECRET_ACTION_NAME, testTags); - - assertTrue(result, "must be allowed"); - String actualPackage = packageCaptor.getValue(); - assertEquals(POLICY_TAG.getValue(), actualPackage); - String actualAction = actionCaptor.getValue(); - assertEquals(READ_SECRET_ACTION_NAME, actualAction); - String actualSubject = subjectCaptor.getValue(); - assertEquals(SUBJECT, actualSubject); - List actualTags = tagsCaptor.getValue(); - assertEquals(testTags, actualTags); - } - - @Test - public void givenNoPolicyDefinedInTag_whenIsAllowed_thenUseDefaultPolicyPackage() throws CsOpaException { - ArgumentCaptor packageCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> tagsCaptor = ArgumentCaptor.forClass(List.class); - when(opaClientMock.isAllowed( - packageCaptor.capture(), any(), any(), tagsCaptor.capture())) - .thenReturn(true); - - OpaService service = new OpaService(opaClientMock); - boolean result = service.isAllowed(READ_SECRET_ACTION_NAME, SUBJECT, TAGS); - - assertTrue(result, "must be allowed"); - String actualPackage = packageCaptor.getValue(); - assertEquals(DEFAULT_POLICY_PACKAGE, actualPackage); - List actualTags = tagsCaptor.getValue(); - assertEquals(TAGS, actualTags); - } - - @Test - public void whenCanReadSecret_thenUseProperAction() throws CsOpaException { - ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); - when(opaClientMock.isAllowed( - any(), actionCaptor.capture(), any(), any())) - .thenReturn(true); - - OpaService service = new OpaService(opaClientMock); - service.canReadSecret(SUBJECT, TAGS); - - String actualAction = actionCaptor.getValue(); - assertEquals(READ_SECRET_ACTION_NAME, actualAction); - - } - - @Test - public void whenCanUseSecret_thenUseProperAction() throws CsOpaException { - ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); - when(opaClientMock.isAllowed( - any(), actionCaptor.capture(), any(), any())) - .thenReturn(true); - - OpaService service = new OpaService(opaClientMock); - service.canUseSecret(SUBJECT, TAGS); - - String actualAction = actionCaptor.getValue(); - assertEquals(USE_SECRET_ACTION_NAME, actualAction); - - } - - @Test - public void whenCanDeleteSecret_thenUseProperAction() throws CsOpaException { - ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); - when(opaClientMock.isAllowed( - any(), actionCaptor.capture(), any(), any())) - .thenReturn(true); - - OpaService service = new OpaService(opaClientMock); - service.canDeleteSecret(SUBJECT, TAGS); - - String actualAction = actionCaptor.getValue(); - assertEquals(OpaService.DELETE_SECRET_ACTION_NAME, actualAction); - } - - @Test - public void whenCanCreateTags_thenUseProperAction() throws CsOpaException { - ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); - when(opaClientMock.isAllowed( - any(), actionCaptor.capture(), any(), any())) - .thenReturn(true); - - OpaService service = new OpaService(opaClientMock); - service.canCreateTags(SUBJECT, TAGS); - - String actualAction = actionCaptor.getValue(); - assertEquals(OpaService.CREATE_TAG_ACTION_NAME, actualAction); - } - - @Test - public void whenCanReadTags_thenUseProperAction() throws CsOpaException { - ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); - when(opaClientMock.isAllowed( - any(), actionCaptor.capture(), any(), any())) - .thenReturn(true); - - OpaService service = new OpaService(opaClientMock); - service.canReadTags(SUBJECT, TAGS); - - String actualAction = actionCaptor.getValue(); - assertEquals(OpaService.READ_TAG_ACTION_NAME, actualAction); - } - - @Test - public void whenCanUpdateTags_thenUseProperAction() throws CsOpaException { - ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); - when(opaClientMock.isAllowed( - any(), actionCaptor.capture(), any(), any())) - .thenReturn(true); - - OpaService service = new OpaService(opaClientMock); - service.canUpdateTags(SUBJECT, TAGS); - - String actualAction = actionCaptor.getValue(); - assertEquals(OpaService.UPDATE_TAG_ACTION_NAME, actualAction); - } - - @Test - public void whenCanDeleteTags_thenUseProperAction() throws CsOpaException { - ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); - when(opaClientMock.isAllowed( - any(), actionCaptor.capture(), any(), any())) - .thenReturn(true); - - OpaService service = new OpaService(opaClientMock); - service.canDeleteTags(SUBJECT, TAGS); - - String actualAction = actionCaptor.getValue(); - assertEquals(OpaService.DELETE_TAG_ACTION_NAME, actualAction); - } -} \ No newline at end of file + private static final String POLICY_PACKAGE = "play"; + private static final String DEFAULT_POLICY_PACKAGE = "default"; + private static final String SUBJECT = "me"; + private static final List TAGS = + Lists.newArrayList( + Tag.builder().key("created").value("yesterday").build(), + Tag.builder().key("owner").value("me").build()); + private static final Tag POLICY_TAG = + Tag.builder().key(OpaService.POLICY_PACKAGE_TAG_KEY).value(POLICY_PACKAGE).build(); + private static final OpaResult POSITIVE_RESULT; + + static { + POSITIVE_RESULT = new OpaResult(); + POSITIVE_RESULT.setResult(true); + } + + @Mock private OpaClient opaClientMock = mock(OpaClient.class); + + @BeforeEach + public void setUp() { + reset(opaClientMock); + when(opaClientMock.newRequest()) + .thenReturn(new OpaClientRequest(opaClientMock, DEFAULT_POLICY_PACKAGE)); + } + + @Test + void givenPolicyDefinedInTag_whenIsAllowed_thenUsePolicyPackageProvided() throws CsOpaException { + ArgumentCaptor packageCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor subjectCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor> tagsCaptor = ArgumentCaptor.forClass(List.class); + when(opaClientMock.isAllowed( + packageCaptor.capture(), + actionCaptor.capture(), + subjectCaptor.capture(), + tagsCaptor.capture())) + .thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + List testTags = Lists.newArrayList(TAGS); + testTags.add(POLICY_TAG); + boolean result = service.isAllowed(SUBJECT, READ_SECRET_ACTION_NAME, testTags); + + assertTrue(result, "must be allowed"); + String actualPackage = packageCaptor.getValue(); + assertEquals(POLICY_TAG.getValue(), actualPackage); + String actualAction = actionCaptor.getValue(); + assertEquals(READ_SECRET_ACTION_NAME, actualAction); + String actualSubject = subjectCaptor.getValue(); + assertEquals(SUBJECT, actualSubject); + List actualTags = tagsCaptor.getValue(); + assertEquals(testTags, actualTags); + } + + @Test + public void givenNoPolicyDefinedInTag_whenIsAllowed_thenUseDefaultPolicyPackage() + throws CsOpaException { + ArgumentCaptor packageCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor> tagsCaptor = ArgumentCaptor.forClass(List.class); + when(opaClientMock.isAllowed(packageCaptor.capture(), any(), any(), tagsCaptor.capture())) + .thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + boolean result = service.isAllowed(READ_SECRET_ACTION_NAME, SUBJECT, TAGS); + + assertTrue(result, "must be allowed"); + String actualPackage = packageCaptor.getValue(); + assertEquals(DEFAULT_POLICY_PACKAGE, actualPackage); + List actualTags = tagsCaptor.getValue(); + assertEquals(TAGS, actualTags); + } + + @Test + public void whenCanReadSecret_thenUseProperAction() throws CsOpaException { + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); + when(opaClientMock.isAllowed(any(), actionCaptor.capture(), any(), any())).thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + service.canReadSecret(SUBJECT, TAGS); + + String actualAction = actionCaptor.getValue(); + assertEquals(READ_SECRET_ACTION_NAME, actualAction); + } + + @Test + public void whenCanUseSecret_thenUseProperAction() throws CsOpaException { + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); + when(opaClientMock.isAllowed(any(), actionCaptor.capture(), any(), any())).thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + service.canUseSecret(SUBJECT, TAGS); + + String actualAction = actionCaptor.getValue(); + assertEquals(USE_SECRET_ACTION_NAME, actualAction); + } + + @Test + public void whenCanDeleteSecret_thenUseProperAction() throws CsOpaException { + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); + when(opaClientMock.isAllowed(any(), actionCaptor.capture(), any(), any())).thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + service.canDeleteSecret(SUBJECT, TAGS); + + String actualAction = actionCaptor.getValue(); + assertEquals(OpaService.DELETE_SECRET_ACTION_NAME, actualAction); + } + + @Test + public void whenCanCreateTags_thenUseProperAction() throws CsOpaException { + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); + when(opaClientMock.isAllowed(any(), actionCaptor.capture(), any(), any())).thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + service.canCreateTags(SUBJECT, TAGS); + + String actualAction = actionCaptor.getValue(); + assertEquals(OpaService.CREATE_TAG_ACTION_NAME, actualAction); + } + + @Test + public void whenCanReadTags_thenUseProperAction() throws CsOpaException { + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); + when(opaClientMock.isAllowed(any(), actionCaptor.capture(), any(), any())).thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + service.canReadTags(SUBJECT, TAGS); + + String actualAction = actionCaptor.getValue(); + assertEquals(OpaService.READ_TAG_ACTION_NAME, actualAction); + } + + @Test + public void whenCanUpdateTags_thenUseProperAction() throws CsOpaException { + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); + when(opaClientMock.isAllowed(any(), actionCaptor.capture(), any(), any())).thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + service.canUpdateTags(SUBJECT, TAGS); + + String actualAction = actionCaptor.getValue(); + assertEquals(OpaService.UPDATE_TAG_ACTION_NAME, actualAction); + } + + @Test + public void whenCanDeleteTags_thenUseProperAction() throws CsOpaException { + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(String.class); + when(opaClientMock.isAllowed(any(), actionCaptor.capture(), any(), any())).thenReturn(true); + + OpaService service = new OpaService(opaClientMock); + service.canDeleteTags(SUBJECT, TAGS); + + String actualAction = actionCaptor.getValue(); + assertEquals(OpaService.DELETE_TAG_ACTION_NAME, actualAction); + } +} diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageIT.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageIT.java index f9b7be6..2f98655 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageIT.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageIT.java @@ -51,8 +51,6 @@ import org.springframework.data.domain.Sort; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.transaction.annotation.Isolation; -import org.springframework.transaction.annotation.Transactional; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -67,7 +65,7 @@ public class StorageIT { private final UUID testSecretId = UUID.fromString("3bcf8308-8f50-4d24-a37b-b0075bb5e779"); private final UUID testSecretId2 = UUID.fromString("0e7cd962-d98e-4eea-82ae-4641399c9ad7"); private final Tag testTag = Tag.builder().key("TEST_KEY").value("TEST_VALUE").build(); - private final String authorizedUserId ="afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; + private final String authorizedUserId = "afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; @Container public static ReusableRedisContainer reusableRedisContainer = @@ -92,8 +90,7 @@ public class StorageIT { @Autowired private MinioClient minioClient; @Autowired private MinioProperties minioProperties; - @MockBean - private OpaClient opaClientMock; + @MockBean private OpaClient opaClientMock; @BeforeEach public void setUp() { @@ -108,14 +105,16 @@ private SecretEntity persistObjectWithIdAndTags(UUID id, Tag... tags) { } @Test - void givenSuccessfulRequest_whenStoreTag_thenPersist() throws CsOpaException, UnauthorizedException { + void givenSuccessfulRequest_whenStoreTag_thenPersist() + throws CsOpaException, UnauthorizedException { persistObjectWithIdAndTags(testSecretId, testTag); storageService.storeTag( - testSecretId, Tag.builder().key("ANOTHER_KEY").value(testTag.getValue()).build(), + testSecretId, + Tag.builder().key("ANOTHER_KEY").value(testTag.getValue()).build(), authorizedUserId); - assertEquals(testTag, - storageService.retrieveTag(testSecretId, testTag.getKey(),authorizedUserId)); + assertEquals( + testTag, storageService.retrieveTag(testSecretId, testTag.getKey(), authorizedUserId)); } @Test @@ -130,8 +129,9 @@ void givenSuccessfulRequest_whenGetObjectList_thenReturnExpectedResult() { @Test void givenNoObjectWithReferencedIdDefined_whenRetrieveTags_thenThrowNotFoundException() { NotFoundException nfe = - assertThrows(NotFoundException.class, () -> - storageService.retrieveTags(testSecretId, authorizedUserId)); + assertThrows( + NotFoundException.class, + () -> storageService.retrieveTags(testSecretId, authorizedUserId)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @@ -139,14 +139,16 @@ void givenNoObjectWithReferencedIdDefined_whenRetrieveTags_thenThrowNotFoundExce @Test void givenSecretIdUnknown_whenStoreTag_thenThrowNotFoundException() { NotFoundException nfe = - assertThrows(NotFoundException.class, () -> - storageService.storeTag(unknownId, testTag, authorizedUserId)); + assertThrows( + NotFoundException.class, + () -> storageService.storeTag(unknownId, testTag, authorizedUserId)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, unknownId), nfe.getMessage()); } @Test - void givenSuccessfulRequests_whenStoreAndRetrieveTag_thenPersistAndReturnExpectedContent() throws CsOpaException, UnauthorizedException { + void givenSuccessfulRequests_whenStoreAndRetrieveTag_thenPersistAndReturnExpectedContent() + throws CsOpaException, UnauthorizedException { persistObjectWithIdAndTags(testSecretId); storageService.storeTag(testSecretId, testTag, authorizedUserId); List tags = storageService.retrieveTags(testSecretId, authorizedUserId); @@ -155,9 +157,11 @@ void givenSuccessfulRequests_whenStoreAndRetrieveTag_thenPersistAndReturnExpecte } @Test - void givenObjectWithoutTagsDefined_whenRetrieveTags_thenReturnEmptyList() throws CsOpaException, UnauthorizedException { + void givenObjectWithoutTagsDefined_whenRetrieveTags_thenReturnEmptyList() + throws CsOpaException, UnauthorizedException { persistObjectWithIdAndTags(testSecretId); - assertEquals(Collections.emptyList(), storageService.retrieveTags(testSecretId, authorizedUserId)); + assertEquals( + Collections.emptyList(), storageService.retrieveTags(testSecretId, authorizedUserId)); } @SneakyThrows @@ -170,8 +174,9 @@ void givenObjectWithoutTagsDefined_whenRetrieveSecretShare_thenReturnEmptyListFo .object(testSecretId.toString()) .stream(new ByteArrayInputStream(new byte[0]), 0, -1) .build()); - assertEquals(Collections.emptyList(), - storageService.getSecretShare(testSecretId, authorizedUserId).getTags()); + assertEquals( + Collections.emptyList(), + storageService.getSecretShare(testSecretId, authorizedUserId).getTags()); } @Test @@ -179,8 +184,8 @@ void givenObjectHasNoDataPersisted_whenGetSecretShare_thenThrowAmphoraServiceExc persistObjectWithIdAndTags(testSecretId); AmphoraServiceException ase = assertThrows( - AmphoraServiceException.class, () -> - storageService.getSecretShare(testSecretId, authorizedUserId)); + AmphoraServiceException.class, + () -> storageService.getSecretShare(testSecretId, authorizedUserId)); assertEquals( String.format( GET_DATA_FOR_SECRET_EXCEPTION_MSG, testSecretId, "The specified key does not exist."), @@ -201,7 +206,8 @@ void givenSecretIdUnknown_whenReplaceTags_thenThrowNotFoundException() { } @Test - void givenSuccessfulRequest_whenDeleteTag_thenDoNoLongerReturn() throws CsOpaException, UnauthorizedException { + void givenSuccessfulRequest_whenDeleteTag_thenDoNoLongerReturn() + throws CsOpaException, UnauthorizedException { SecretEntity secretEntity = persistObjectWithIdAndTags(testSecretId, testTag); Tag expectedTag = Tag.builder().key(testTag.getKey() + "new").value(testTag.getValue()).build(); TagEntity expectedTagEntity = TagEntity.fromTag(expectedTag).setSecret(secretEntity); @@ -232,8 +238,8 @@ void givenObjectHasNoTagWithRequestedKey_whenDeleteKey_thenThrowNotFoundExceptio NotFoundException nfe = assertThrows( - NotFoundException.class, () -> - storageService.deleteTag(testSecretId, unknownKey, authorizedUserId)); + NotFoundException.class, + () -> storageService.deleteTag(testSecretId, unknownKey, authorizedUserId)); assertEquals( String.format( NO_TAG_WITH_KEY_EXISTS_FOR_SECRET_WITH_ID_EXCEPTION_MSG, unknownKey, testSecretId), @@ -241,7 +247,8 @@ void givenObjectHasNoTagWithRequestedKey_whenDeleteKey_thenThrowNotFoundExceptio } @Test - void givenMultipleObjectsWIthIdenticalTag_whenDeleteTagOnOneObject_thenKeepTagForOtherObjects() throws CsOpaException, UnauthorizedException { + void givenMultipleObjectsWIthIdenticalTag_whenDeleteTagOnOneObject_thenKeepTagForOtherObjects() + throws CsOpaException, UnauthorizedException { persistObjectWithIdAndTags(testSecretId, testTag); persistObjectWithIdAndTags(testSecretId2, testTag); storageService.deleteTag(testSecretId, testTag.getKey(), authorizedUserId); @@ -261,7 +268,9 @@ void givenMultipleObjectsWIthIdenticalTag_whenDeleteTagOnOneObject_thenKeepTagFo @Test void givenAnUnknownId_whenStoringATag_thenThrow() { NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.storeTag(testSecretId, testTag, authorizedUserId)); + assertThrows( + NotFoundException.class, + () -> storageService.storeTag(testSecretId, testTag, authorizedUserId)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageServiceTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageServiceTest.java index 66b7d4f..e4aa8a1 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageServiceTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/persistence/metadata/StorageServiceTest.java @@ -7,6 +7,18 @@ package io.carbynestack.amphora.service.persistence.metadata; +import static io.carbynestack.amphora.service.persistence.metadata.StorageService.*; +import static io.carbynestack.amphora.service.persistence.metadata.TagEntity.setToTagList; +import static io.carbynestack.amphora.service.util.CreationDateTagMatchers.containsCreationDateTag; +import static io.carbynestack.castor.common.entities.Field.GFP; +import static java.util.Arrays.asList; +import static java.util.Collections.*; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; + import io.carbynestack.amphora.common.*; import io.carbynestack.amphora.common.exceptions.AmphoraServiceException; import io.carbynestack.amphora.service.calculation.SecretShareUtil; @@ -22,6 +34,8 @@ import io.carbynestack.castor.common.entities.InputMask; import io.carbynestack.castor.common.entities.TupleList; import io.carbynestack.mpspdz.integration.MpSpdzIntegrationUtils; +import java.util.*; +import java.util.stream.Collectors; import org.apache.commons.lang3.RandomUtils; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Test; @@ -32,21 +46,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.*; -import java.util.*; -import java.util.stream.Collectors; - -import static io.carbynestack.amphora.service.persistence.metadata.StorageService.*; -import static io.carbynestack.amphora.service.persistence.metadata.TagEntity.setToTagList; -import static io.carbynestack.amphora.service.util.CreationDateTagMatchers.containsCreationDateTag; -import static io.carbynestack.castor.common.entities.Field.GFP; -import static java.util.Arrays.asList; -import static java.util.Collections.*; -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) class StorageServiceTest { private final UUID testSecretId = UUID.fromString("3bcf8308-8f50-4d24-a37b-b0075bb5e779"); @@ -55,7 +54,7 @@ class StorageServiceTest { private final Tag testTag2 = Tag.builder().key("SUPER_KEY").value("MY#SUPER,VALUE").build(); private final Tag testTagReservedCreationDateKey = Tag.builder().key(StorageService.RESERVED_TAG_KEYS.get(0)).value("MY#SUPER,VALUE").build(); - private final String authorizedSubject ="afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; + private final String authorizedSubject = "afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; @Mock private SecretEntityRepository secretEntityRepository; @Mock private InputMaskCachingService inputMaskStore; @@ -82,8 +81,8 @@ void givenIdIsAlreadyInUse_whenCreateSecret_thenThrowAlreadyExistsException() { AlreadyExistsException aee = assertThrows( - AlreadyExistsException.class, () -> - storageService.createSecret(testMaskedInput, authorizedSubject)); + AlreadyExistsException.class, + () -> storageService.createSecret(testMaskedInput, authorizedSubject)); assertEquals(SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, aee.getMessage()); } @@ -180,7 +179,9 @@ void givenSuccessfulRequest_whenCreateObject_thenReturnSecretId() { when(secretEntityRepository.save(secretEntityArgumentCaptor.capture())) .thenReturn(persistedSecretEntity); - assertEquals(maskedInput.getSecretId().toString(), storageService.createSecret(maskedInput, authorizedSubject)); + assertEquals( + maskedInput.getSecretId().toString(), + storageService.createSecret(maskedInput, authorizedSubject)); verify(secretEntityRepository, times(1)).existsById(maskedInput.getSecretId().toString()); verify(inputMaskStore, times(1)).getCachedInputMasks(maskedInput.getSecretId()); verify(secretEntityRepository, times(1)).save(secretEntityArgumentCaptor.capture()); @@ -390,29 +391,36 @@ void givenNoSecretShareWithGivenIdInDatabase_whenGetSecretShare_thenThrowNotFoun when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.empty()); NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.getSecretShare(testSecretId, authorizedSubject)); + assertThrows( + NotFoundException.class, + () -> storageService.getSecretShare(testSecretId, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @Test - void givenSubjectHasNoAccess_whenGetSecretShare_thenThrowUnauthorizedException() throws CsOpaException { + void givenSubjectHasNoAccess_whenGetSecretShare_thenThrowUnauthorizedException() + throws CsOpaException { SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); when(secretEntityRepository.findById(testSecretId.toString())) - .thenReturn(Optional.of(secretEntity)); + .thenReturn(Optional.of(secretEntity)); when(opaService.canReadSecret(authorizedSubject, emptyList())).thenReturn(false); UnauthorizedException ue = - assertThrows(UnauthorizedException.class, () -> storageService.getSecretShare(testSecretId, authorizedSubject)); + assertThrows( + UnauthorizedException.class, + () -> storageService.getSecretShare(testSecretId, authorizedSubject)); assertEquals("User is not authorized to read this secret", ue.getMessage()); } @Test - void givenDataCannotBeRetrieved_whenGetSecretShare_thenThrowAmphoraServiceException() throws CsOpaException { + void givenDataCannotBeRetrieved_whenGetSecretShare_thenThrowAmphoraServiceException() + throws CsOpaException { AmphoraServiceException expectedAse = new AmphoraServiceException("Expected this one"); TagEntity tagEntity = TagEntity.fromTag(testTag); SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), singleton(tagEntity)); - when(opaService.canReadSecret(authorizedSubject, setToTagList(secretEntity.getTags()))).thenReturn(true); + when(opaService.canReadSecret(authorizedSubject, setToTagList(secretEntity.getTags()))) + .thenReturn(true); when(secretEntityRepository.findById(testSecretId.toString())) .thenReturn(Optional.of(secretEntity)); when(secretShareDataStore.getSecretShareData(testSecretId)).thenThrow(expectedAse); @@ -424,7 +432,8 @@ void givenDataCannotBeRetrieved_whenGetSecretShare_thenThrowAmphoraServiceExcept } @Test - void givenSuccessfulRequest_whenGetSecretShare_thenReturnContent() throws CsOpaException, UnauthorizedException { + void givenSuccessfulRequest_whenGetSecretShare_thenReturnContent() + throws CsOpaException, UnauthorizedException { List expectedTags = singletonList(testTag); byte[] expectedData = RandomUtils.nextBytes(MpSpdzIntegrationUtils.SHARE_WIDTH); SecretEntity existingSecretEntity = @@ -439,7 +448,8 @@ void givenSuccessfulRequest_whenGetSecretShare_thenReturnContent() throws CsOpaE .thenReturn(true); SecretShare actualSecretShare = - storageService.getSecretShare(UUID.fromString(existingSecretEntity.getSecretId()), authorizedSubject); + storageService.getSecretShare( + UUID.fromString(existingSecretEntity.getSecretId()), authorizedSubject); assertEquals( UUID.fromString(existingSecretEntity.getSecretId()), actualSecretShare.getSecretId()); assertEquals(expectedTags, actualSecretShare.getTags()); @@ -451,9 +461,11 @@ void givenNoSecretShareWithGivenIdInDatabase_whenUseSecretShare_thenThrowNotFoun when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.empty()); NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.useSecretShare(testSecretId, authorizedSubject)); + assertThrows( + NotFoundException.class, + () -> storageService.useSecretShare(testSecretId, authorizedSubject)); assertEquals( - String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); + String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @Test @@ -461,66 +473,77 @@ void givenSubjectIsNotAuthorized_whenUseSecretShare_thenThrowUnauthorizedExcepti TagEntity tagEntity = TagEntity.fromTag(testTag); SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), singleton(tagEntity)); when(secretEntityRepository.findById(testSecretId.toString())) - .thenReturn(Optional.of(secretEntity)); + .thenReturn(Optional.of(secretEntity)); - assertThrows(UnauthorizedException.class, () -> storageService.useSecretShare(testSecretId, authorizedSubject)); + assertThrows( + UnauthorizedException.class, + () -> storageService.useSecretShare(testSecretId, authorizedSubject)); } @Test - void givenDataCannotBeRetrieved_whenUseSecretShare_thenThrowAmphoraServiceException() throws CsOpaException { + void givenDataCannotBeRetrieved_whenUseSecretShare_thenThrowAmphoraServiceException() + throws CsOpaException { AmphoraServiceException expectedAse = new AmphoraServiceException("Expected this one"); TagEntity tagEntity = TagEntity.fromTag(testTag); SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), singleton(tagEntity)); when(secretEntityRepository.findById(testSecretId.toString())) - .thenReturn(Optional.of(secretEntity)); + .thenReturn(Optional.of(secretEntity)); when(opaService.canUseSecret(authorizedSubject, setToTagList(secretEntity.getTags()))) - .thenReturn(true); + .thenReturn(true); when(secretShareDataStore.getSecretShareData(testSecretId)).thenThrow(expectedAse); assertThrows( - AmphoraServiceException.class, - () -> storageService.useSecretShare(testSecretId, authorizedSubject), - expectedAse.getMessage()); + AmphoraServiceException.class, + () -> storageService.useSecretShare(testSecretId, authorizedSubject), + expectedAse.getMessage()); } @Test - void givenSuccessfulRequest_whenUseSecretShare_thenReturnContent() throws CsOpaException, UnauthorizedException { + void givenSuccessfulRequest_whenUseSecretShare_thenReturnContent() + throws CsOpaException, UnauthorizedException { List expectedTags = singletonList(testTag); byte[] expectedData = RandomUtils.nextBytes(MpSpdzIntegrationUtils.SHARE_WIDTH); SecretEntity existingSecretEntity = - new SecretEntity(testSecretId.toString(), TagEntity.setFromTagList(expectedTags)); + new SecretEntity(testSecretId.toString(), TagEntity.setFromTagList(expectedTags)); when(secretEntityRepository.findById(existingSecretEntity.getSecretId())) - .thenReturn(Optional.of(existingSecretEntity)); + .thenReturn(Optional.of(existingSecretEntity)); when(opaService.canUseSecret(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) - .thenReturn(true); + .thenReturn(true); when(secretShareDataStore.getSecretShareData( UUID.fromString(existingSecretEntity.getSecretId()))) - .thenReturn(expectedData); + .thenReturn(expectedData); SecretShare actualSecretShare = - storageService.useSecretShare(UUID.fromString(existingSecretEntity.getSecretId()), authorizedSubject); + storageService.useSecretShare( + UUID.fromString(existingSecretEntity.getSecretId()), authorizedSubject); assertEquals( - UUID.fromString(existingSecretEntity.getSecretId()), actualSecretShare.getSecretId()); + UUID.fromString(existingSecretEntity.getSecretId()), actualSecretShare.getSecretId()); assertEquals(expectedTags, actualSecretShare.getTags()); assertEquals(expectedData, actualSecretShare.getData()); } @Test - void givenNoSecretShareWithGivenIdInDatabase_whenDeleteObject_thenThrowNotFoundException() { NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.deleteSecret(testSecretId, authorizedSubject)); + void givenNoSecretShareWithGivenIdInDatabase_whenDeleteObject_thenThrowNotFoundException() { + NotFoundException nfe = + assertThrows( + NotFoundException.class, + () -> storageService.deleteSecret(testSecretId, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @Test - void givenSubjectHasNoAccess_whenDeleteObject_thenThrowUnauthorizedException() throws CsOpaException { + void givenSubjectHasNoAccess_whenDeleteObject_thenThrowUnauthorizedException() + throws CsOpaException { SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); when(secretEntityRepository.findById(testSecretId.toString())) - .thenReturn(Optional.of(secretEntity)); + .thenReturn(Optional.of(secretEntity)); UnauthorizedException ue = - assertThrows(UnauthorizedException.class, () -> storageService.deleteSecret(testSecretId, authorizedSubject)); + assertThrows( + UnauthorizedException.class, + () -> storageService.deleteSecret(testSecretId, authorizedSubject)); assertEquals("User is not authorized to delete this secret", ue.getMessage()); } @@ -529,7 +552,8 @@ void givenDeleteObjectDataFails_whenDeleteObject_thenThrowGivenException() throw AmphoraServiceException expectedAse = new AmphoraServiceException("Expected this one"); TagEntity tagEntity = TagEntity.fromTag(testTag); SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), singleton(tagEntity)); - when(opaService.canDeleteSecret(authorizedSubject, setToTagList(secretEntity.getTags()))).thenReturn(true); + when(opaService.canDeleteSecret(authorizedSubject, setToTagList(secretEntity.getTags()))) + .thenReturn(true); when(secretEntityRepository.findById(testSecretId.toString())) .thenReturn(Optional.of(secretEntity)); when(secretEntityRepository.deleteBySecretId(testSecretId.toString())).thenReturn(1L); @@ -542,16 +566,18 @@ void givenDeleteObjectDataFails_whenDeleteObject_thenThrowGivenException() throw } @Test - void givenSuccessfulRequest_whenDeleteObject_thenDeleteObjectAndData() throws CsOpaException, UnauthorizedException { + void givenSuccessfulRequest_whenDeleteObject_thenDeleteObjectAndData() + throws CsOpaException, UnauthorizedException { TagEntity existingTagEntity = TagEntity.fromTag(testTag); SecretEntity existingSecretEntity = - new SecretEntity(testSecretId.toString(), singleton(existingTagEntity)); + new SecretEntity(testSecretId.toString(), singleton(existingTagEntity)); when(secretEntityRepository.findById(testSecretId.toString())) - .thenReturn(Optional.of(existingSecretEntity)); + .thenReturn(Optional.of(existingSecretEntity)); when(secretEntityRepository.deleteBySecretId(testSecretId.toString())).thenReturn(1L); - when(opaService.canDeleteSecret(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) - .thenReturn(true); + when(opaService.canDeleteSecret( + authorizedSubject, setToTagList(existingSecretEntity.getTags()))) + .thenReturn(true); storageService.deleteSecret(testSecretId, authorizedSubject); verify(secretShareDataStore, times(1)).deleteSecretShareData(testSecretId); @@ -562,7 +588,9 @@ void givenTagHasReservedKey_whenStoreTag_thenThrowIllegalArgumentException() { IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, - () -> storageService.storeTag(testSecretId, testTagReservedCreationDateKey, authorizedSubject)); + () -> + storageService.storeTag( + testSecretId, testTagReservedCreationDateKey, authorizedSubject)); assertEquals( String.format(IS_RESERVED_KEY_EXCEPTION_MSG, testTagReservedCreationDateKey.getKey()), iae.getMessage()); @@ -571,23 +599,28 @@ void givenTagHasReservedKey_whenStoreTag_thenThrowIllegalArgumentException() { @Test void givenNoSecretShareWithGivenIdInDatabase_whenStoreTag_thenThrowNotFoundException() { NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.storeTag(testSecretId, testTag, authorizedSubject)); + assertThrows( + NotFoundException.class, + () -> storageService.storeTag(testSecretId, testTag, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @Test - void givenObjectAlreadyHasTagWithGivenKey_whenStoreTag_thenThrowAlreadyExistsException() throws CsOpaException { + void givenObjectAlreadyHasTagWithGivenKey_whenStoreTag_thenThrowAlreadyExistsException() + throws CsOpaException { SecretEntity existingSecretEntity = new SecretEntity().setSecretId(testSecretId.toString()); TagEntity existingTagEntity = TagEntity.fromTag(testTag); - when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.of(existingSecretEntity)); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(existingSecretEntity)); when(tagRepository.findBySecretAndKey(existingSecretEntity, testTag.getKey())) .thenReturn(Optional.of(existingTagEntity)); when(opaService.canCreateTags(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) - .thenReturn(true); + .thenReturn(true); AlreadyExistsException aee = assertThrows( - AlreadyExistsException.class, () -> storageService.storeTag(testSecretId, testTag, authorizedSubject)); + AlreadyExistsException.class, + () -> storageService.storeTag(testSecretId, testTag, authorizedSubject)); assertEquals( String.format( TAG_WITH_KEY_EXISTS_FOR_SECRET_EXCEPTION_MSG, @@ -600,30 +633,36 @@ void givenObjectAlreadyHasTagWithGivenKey_whenStoreTag_thenThrowAlreadyExistsExc void givenSubjectHasNoAccess_whenStoreTag_thenThrowUnauthorizedException() throws CsOpaException { SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); when(secretEntityRepository.findById(testSecretId.toString())) - .thenReturn(Optional.of(secretEntity)); + .thenReturn(Optional.of(secretEntity)); UnauthorizedException ue = - assertThrows(UnauthorizedException.class, () -> - storageService.storeTag(UUID.fromString(secretEntity.getSecretId()), testTag, authorizedSubject)); + assertThrows( + UnauthorizedException.class, + () -> + storageService.storeTag( + UUID.fromString(secretEntity.getSecretId()), testTag, authorizedSubject)); assertEquals("User is not authorized to create tags for this secret", ue.getMessage()); } @Test - void givenSuccessfulRequest_whenStoreTag_thenPersistTag() throws CsOpaException, UnauthorizedException { + void givenSuccessfulRequest_whenStoreTag_thenPersistTag() + throws CsOpaException, UnauthorizedException { SecretEntity existingSecretEntity = new SecretEntity().setSecretId(testSecretId.toString()); TagEntity expectedTagEntity = TagEntity.fromTag(testTag); ArgumentCaptor tagEntityArgumentCaptor = ArgumentCaptor.forClass(TagEntity.class); - when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.of(existingSecretEntity)); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(existingSecretEntity)); when(tagRepository.findBySecretAndKey(existingSecretEntity, testTag.getKey())) .thenReturn(Optional.empty()); when(tagRepository.save(any())).thenReturn(expectedTagEntity); when(opaService.canCreateTags(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) - .thenReturn(true); + .thenReturn(true); assertEquals( expectedTagEntity.getKey(), - storageService.storeTag(UUID.fromString(existingSecretEntity.getSecretId()), testTag, authorizedSubject)); + storageService.storeTag( + UUID.fromString(existingSecretEntity.getSecretId()), testTag, authorizedSubject)); verify(tagRepository, times(1)).save(tagEntityArgumentCaptor.capture()); TagEntity actualTagEntity = tagEntityArgumentCaptor.getValue(); @@ -653,27 +692,34 @@ void givenNoSecretShareWithGivenIdInDatabase_whenReplaceTags_thenThrowNotFoundEx List emptyTags = emptyList(); NotFoundException nfe = assertThrows( - NotFoundException.class, () -> storageService.replaceTags(testSecretId, emptyTags, authorizedSubject)); + NotFoundException.class, + () -> storageService.replaceTags(testSecretId, emptyTags, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @Test - void givenSubjectHasNoAccess_whenReplaceTags_thenThrowUnauthorizedException() throws CsOpaException { + void givenSubjectHasNoAccess_whenReplaceTags_thenThrowUnauthorizedException() + throws CsOpaException { List tagListWithReservedKey = asList(testTag, testTagReservedCreationDateKey); SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); when(secretEntityRepository.findById(testSecretId.toString())) - .thenReturn(Optional.of(secretEntity)); + .thenReturn(Optional.of(secretEntity)); UnauthorizedException ue = - assertThrows(UnauthorizedException.class, () -> - storageService.replaceTags( - UUID.fromString(secretEntity.getSecretId()), tagListWithReservedKey, authorizedSubject)); + assertThrows( + UnauthorizedException.class, + () -> + storageService.replaceTags( + UUID.fromString(secretEntity.getSecretId()), + tagListWithReservedKey, + authorizedSubject)); assertEquals("User is not authorized to update tags for this secret", ue.getMessage()); } @Test - void givenListHasTagWithReservedKey_whenReplaceTags_thenReplaceByExistingTagAndPersist() throws CsOpaException, UnauthorizedException { + void givenListHasTagWithReservedKey_whenReplaceTags_thenReplaceByExistingTagAndPersist() + throws CsOpaException, UnauthorizedException { List tagListWithReservedKey = asList(testTag, testTagReservedCreationDateKey); TagEntity existingCreationTagEntity = TagEntity.fromTag( @@ -685,14 +731,17 @@ void givenListHasTagWithReservedKey_whenReplaceTags_thenReplaceByExistingTagAndP SecretEntity existingSecretEntity = new SecretEntity(testSecretId.toString(), emptySet()); ArgumentCaptor> tagEntitySetArgumentCaptor = ArgumentCaptor.forClass(Set.class); - when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.of(existingSecretEntity)); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(existingSecretEntity)); when(tagRepository.findBySecretAndKey(existingSecretEntity, existingCreationTagEntity.getKey())) .thenReturn(Optional.of(existingCreationTagEntity)); when(opaService.canUpdateTags(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) - .thenReturn(true); + .thenReturn(true); storageService.replaceTags( - UUID.fromString(existingSecretEntity.getSecretId()), tagListWithReservedKey, authorizedSubject); + UUID.fromString(existingSecretEntity.getSecretId()), + tagListWithReservedKey, + authorizedSubject); verify(tagRepository, times(1)).deleteBySecret(existingSecretEntity); verify(tagRepository, times(1)).saveAll(tagEntitySetArgumentCaptor.capture()); @@ -710,25 +759,32 @@ void givenNoSecretShareWithGivenIdInDatabase_whenRetrieveTags_thenThrowNotFoundE when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.empty()); NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.retrieveTags(testSecretId, authorizedSubject)); + assertThrows( + NotFoundException.class, + () -> storageService.retrieveTags(testSecretId, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @Test - void givenSubjectHasNoAccess_whenRetrieveTags_thenThrowUnauthorizedException() throws CsOpaException { + void givenSubjectHasNoAccess_whenRetrieveTags_thenThrowUnauthorizedException() + throws CsOpaException { SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); when(secretEntityRepository.findById(testSecretId.toString())) - .thenReturn(Optional.of(secretEntity)); + .thenReturn(Optional.of(secretEntity)); UnauthorizedException ue = - assertThrows(UnauthorizedException.class, () -> - storageService.retrieveTags(UUID.fromString(secretEntity.getSecretId()), authorizedSubject)); + assertThrows( + UnauthorizedException.class, + () -> + storageService.retrieveTags( + UUID.fromString(secretEntity.getSecretId()), authorizedSubject)); assertEquals("User is not authorized to read tags for this secret", ue.getMessage()); } @Test - void givenSuccessfulRequest_whenRetrieveTags_thenReturnContent() throws CsOpaException, UnauthorizedException { + void givenSuccessfulRequest_whenRetrieveTags_thenReturnContent() + throws CsOpaException, UnauthorizedException { List expectedTags = asList(testTag, testTag2); SecretEntity existingSecretEntity = new SecretEntity(testSecretId.toString(), TagEntity.setFromTagList(expectedTags)); @@ -738,7 +794,8 @@ void givenSuccessfulRequest_whenRetrieveTags_thenReturnContent() throws CsOpaExc .thenReturn(true); MatcherAssert.assertThat( - storageService.retrieveTags(UUID.fromString(existingSecretEntity.getSecretId()), authorizedSubject), + storageService.retrieveTags( + UUID.fromString(existingSecretEntity.getSecretId()), authorizedSubject), containsInAnyOrder(expectedTags.toArray())); } @@ -746,7 +803,9 @@ void givenSuccessfulRequest_whenRetrieveTags_thenReturnContent() throws CsOpaExc void givenNoSecretShareWithGivenIdInDatabase_whenRetrieveTag_thenThrowNotFoundException() { String key = testTag.getKey(); NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.retrieveTag(testSecretId, key, authorizedSubject)); + assertThrows( + NotFoundException.class, + () -> storageService.retrieveTag(testSecretId, key, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @@ -766,29 +825,33 @@ void givenNoTagWithGivenKeyInDatabaseForGivenObject_whenRetrieveTag_thenThrowNot } @Test - void givenSubjectHasNoAccess_whenRetrieveTag_thenThrowUnauthorizedException() throws CsOpaException { + void givenSubjectHasNoAccess_whenRetrieveTag_thenThrowUnauthorizedException() + throws CsOpaException { SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); when(secretEntityRepository.findById(testSecretId.toString())) - .thenReturn(Optional.of(secretEntity)); + .thenReturn(Optional.of(secretEntity)); UnauthorizedException ue = - assertThrows(UnauthorizedException.class, () -> - storageService.retrieveTag(testSecretId, testTag.getKey(), authorizedSubject)); + assertThrows( + UnauthorizedException.class, + () -> storageService.retrieveTag(testSecretId, testTag.getKey(), authorizedSubject)); assertEquals("User is not authorized to read tags for this secret", ue.getMessage()); } @Test - void givenSuccessfulRequest_whenRetrieveTag_thenReturnContent() throws CsOpaException, UnauthorizedException { + void givenSuccessfulRequest_whenRetrieveTag_thenReturnContent() + throws CsOpaException, UnauthorizedException { TagEntity existingTagEntity = TagEntity.fromTag(testTag); SecretEntity existingSecretEntity = new SecretEntity(testSecretId.toString(), singleton(existingTagEntity)); when(secretEntityRepository.findById(testSecretId.toString())) - .thenReturn(Optional.of(existingSecretEntity)); + .thenReturn(Optional.of(existingSecretEntity)); when(opaService.canReadTags(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) - .thenReturn(true); + .thenReturn(true); when(tagRepository.findBySecretAndKey(existingSecretEntity, existingTagEntity.getKey())) .thenReturn(Optional.of(existingTagEntity)); - assertEquals(testTag, storageService.retrieveTag(testSecretId, testTag.getKey(), authorizedSubject)); + assertEquals( + testTag, storageService.retrieveTag(testSecretId, testTag.getKey(), authorizedSubject)); } @Test @@ -796,7 +859,9 @@ void givenTagHasReservedKey_whenUpdateTag_thenThrowIllegalArgumentException() { IllegalArgumentException iae = assertThrows( IllegalArgumentException.class, - () -> storageService.updateTag(testSecretId, testTagReservedCreationDateKey, authorizedSubject)); + () -> + storageService.updateTag( + testSecretId, testTagReservedCreationDateKey, authorizedSubject)); assertEquals( String.format(IS_RESERVED_KEY_EXCEPTION_MSG, testTagReservedCreationDateKey.getKey()), iae.getMessage()); @@ -806,7 +871,8 @@ void givenTagHasReservedKey_whenUpdateTag_thenThrowIllegalArgumentException() { void givenNoSecretShareWithGivenIdInDatabase_whenUpdateTag_thenThrowNotFoundException() { NotFoundException nfe = assertThrows( - NotFoundException.class, () -> storageService.updateTag(testSecretId, testTag, authorizedSubject)); + NotFoundException.class, + () -> storageService.updateTag(testSecretId, testTag, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @@ -825,33 +891,40 @@ void givenNoTagWithGivenKeyInDatabaseForGivenObject_whenUpdateTag_thenThrowNotFo } @Test - void givenSubjectHasNoAccess_whenUpdateTag_thenThrowUnauthorizedException() throws CsOpaException { + void givenSubjectHasNoAccess_whenUpdateTag_thenThrowUnauthorizedException() + throws CsOpaException { Tag newTag = - Tag.builder().key(testTag.getKey()).value("123").valueType(TagValueType.LONG).build(); + Tag.builder().key(testTag.getKey()).value("123").valueType(TagValueType.LONG).build(); SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); when(secretEntityRepository.findById(testSecretId.toString())) - .thenReturn(Optional.of(secretEntity)); + .thenReturn(Optional.of(secretEntity)); UnauthorizedException ue = - assertThrows(UnauthorizedException.class, () -> - storageService.updateTag(UUID.fromString(secretEntity.getSecretId()), newTag, authorizedSubject)); + assertThrows( + UnauthorizedException.class, + () -> + storageService.updateTag( + UUID.fromString(secretEntity.getSecretId()), newTag, authorizedSubject)); assertEquals("User is not authorized to update tags for this secret", ue.getMessage()); } @Test - void givenSuccessfulRequest_whenUpdateTag_thenUpdateTag() throws CsOpaException, UnauthorizedException { + void givenSuccessfulRequest_whenUpdateTag_thenUpdateTag() + throws CsOpaException, UnauthorizedException { Tag newTag = Tag.builder().key(testTag.getKey()).value("123").valueType(TagValueType.LONG).build(); TagEntity existingTagEntity = TagEntity.fromTag(testTag); SecretEntity existingSecretEntity = new SecretEntity(testSecretId.toString(), singleton(existingTagEntity)); - when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.of(existingSecretEntity)); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(existingSecretEntity)); when(tagRepository.findBySecretAndKey(existingSecretEntity, testTag.getKey())) .thenReturn(Optional.of(existingTagEntity)); when(opaService.canUpdateTags(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) - .thenReturn(true); + .thenReturn(true); - storageService.updateTag(UUID.fromString(existingSecretEntity.getSecretId()), newTag, authorizedSubject); + storageService.updateTag( + UUID.fromString(existingSecretEntity.getSecretId()), newTag, authorizedSubject); ArgumentCaptor tagEntityArgumentCaptor = ArgumentCaptor.forClass(TagEntity.class); verify(tagRepository, times(1)).save(tagEntityArgumentCaptor.capture()); @@ -876,7 +949,9 @@ void givenNoSecretShareWithGivenIdInDatabase_whenDeleteTag_thenThrowNotFoundExce String key = testTag.getKey(); NotFoundException nfe = - assertThrows(NotFoundException.class, () -> storageService.deleteTag(testSecretId, key, authorizedSubject)); + assertThrows( + NotFoundException.class, + () -> storageService.deleteTag(testSecretId, key, authorizedSubject)); assertEquals( String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, testSecretId), nfe.getMessage()); } @@ -896,33 +971,42 @@ void givenNoTagWithGivenKeyInDatabaseForGivenObject_whenDeleteTag_thenThrowNotFo } @Test - void givenSubjectHasNoAccess_whenDeleteTag_thenThrowUnauthorizedException() throws CsOpaException { + void givenSubjectHasNoAccess_whenDeleteTag_thenThrowUnauthorizedException() + throws CsOpaException { TagEntity tagEntityToDelete = TagEntity.fromTag(testTag); SecretEntity secretEntity = new SecretEntity(testSecretId.toString(), emptySet()); when(secretEntityRepository.findById(testSecretId.toString())) - .thenReturn(Optional.of(secretEntity)); + .thenReturn(Optional.of(secretEntity)); UnauthorizedException ue = - assertThrows(UnauthorizedException.class, () -> - storageService.deleteTag( - UUID.fromString(secretEntity.getSecretId()), tagEntityToDelete.getKey(), authorizedSubject)); + assertThrows( + UnauthorizedException.class, + () -> + storageService.deleteTag( + UUID.fromString(secretEntity.getSecretId()), + tagEntityToDelete.getKey(), + authorizedSubject)); assertEquals("User is not authorized to delete tags for this secret", ue.getMessage()); } @Test - void givenSuccessfulRequest_whenDeleteTag_thenDelete() throws CsOpaException, UnauthorizedException { + void givenSuccessfulRequest_whenDeleteTag_thenDelete() + throws CsOpaException, UnauthorizedException { TagEntity tagEntityToDelete = TagEntity.fromTag(testTag); SecretEntity existingSecretEntity = new SecretEntity(testSecretId.toString(), new HashSet<>(singleton(tagEntityToDelete))); - when(secretEntityRepository.findById(testSecretId.toString())).thenReturn(Optional.of(existingSecretEntity)); + when(secretEntityRepository.findById(testSecretId.toString())) + .thenReturn(Optional.of(existingSecretEntity)); when(tagRepository.findBySecretAndKey(existingSecretEntity, tagEntityToDelete.getKey())) .thenReturn(Optional.of(tagEntityToDelete)); when(opaService.canDeleteTags(authorizedSubject, setToTagList(existingSecretEntity.getTags()))) - .thenReturn(true); + .thenReturn(true); assertEquals(1, existingSecretEntity.getTags().size()); storageService.deleteTag( - UUID.fromString(existingSecretEntity.getSecretId()), tagEntityToDelete.getKey(), authorizedSubject); + UUID.fromString(existingSecretEntity.getSecretId()), + tagEntityToDelete.getKey(), + authorizedSubject); assertEquals(0, existingSecretEntity.getTags().size()); } } diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/IntraVcpControllerTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/IntraVcpControllerTest.java index 272fac8..3bbb130 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/IntraVcpControllerTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/IntraVcpControllerTest.java @@ -6,10 +6,18 @@ */ package io.carbynestack.amphora.service.rest; +import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.INTRA_VCP_OPERATIONS_SEGMENT; +import static io.carbynestack.amphora.service.util.ServletUriComponentsBuilderUtil.runInMockedHttpRequestContextForUri; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + import io.carbynestack.amphora.common.SecretShare; import io.carbynestack.amphora.service.exceptions.CsOpaException; import io.carbynestack.amphora.service.exceptions.UnauthorizedException; import io.carbynestack.amphora.service.persistence.metadata.StorageService; +import java.net.URI; +import java.util.UUID; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -18,18 +26,9 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import java.net.URI; -import java.util.UUID; - -import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.INTRA_VCP_OPERATIONS_SEGMENT; -import static io.carbynestack.amphora.service.util.ServletUriComponentsBuilderUtil.runInMockedHttpRequestContextForUri; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) class IntraVcpControllerTest { - private final String authorizedSubjectId ="afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; + private final String authorizedSubjectId = "afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; @Mock private StorageService storageService; @@ -63,11 +62,13 @@ void givenSuccessfulRequest_whenUploadSecretShare_thenReturnCreatedWithExpectedC } @Test - void givenSuccessfulRequest_whenDownloadSecretShare_thenReturnOkWithExpectedContent() throws CsOpaException, UnauthorizedException { + void givenSuccessfulRequest_whenDownloadSecretShare_thenReturnOkWithExpectedContent() + throws CsOpaException, UnauthorizedException { UUID secretShareId = UUID.fromString("3bcf8308-8f50-4d24-a37b-b0075bb5e779"); SecretShare expectedSecretShare = SecretShare.builder().secretId(secretShareId).build(); - when(storageService.useSecretShare(secretShareId, authorizedSubjectId)).thenReturn(expectedSecretShare); + when(storageService.useSecretShare(secretShareId, authorizedSubjectId)) + .thenReturn(expectedSecretShare); ResponseEntity actualResponse = intraVcpController.downloadSecretShare(secretShareId, authorizedSubjectId); diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/MaskedInputControllerTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/MaskedInputControllerTest.java index d119967..bd55671 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/MaskedInputControllerTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/MaskedInputControllerTest.java @@ -6,11 +6,21 @@ */ package io.carbynestack.amphora.service.rest; +import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.INTRA_VCP_OPERATIONS_SEGMENT; +import static io.carbynestack.amphora.service.util.ServletUriComponentsBuilderUtil.runInMockedHttpRequestContextForUri; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import io.carbynestack.amphora.common.MaskedInput; import io.carbynestack.amphora.common.MaskedInputData; import io.carbynestack.amphora.service.exceptions.UnauthorizedException; import io.carbynestack.amphora.service.opa.JwtReader; import io.carbynestack.amphora.service.persistence.metadata.StorageService; +import java.net.URI; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -18,36 +28,29 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import java.net.URI; -import java.util.UUID; - -import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.INTRA_VCP_OPERATIONS_SEGMENT; -import static io.carbynestack.amphora.service.util.ServletUriComponentsBuilderUtil.runInMockedHttpRequestContextForUri; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) class MaskedInputControllerTest { - private final String authHeader = "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5Njk0OTgyLWQzMTAtNDBkOC04ZDk4LTczOWI1ZGZjNWUyNiIsInR5cCI6IkpXVCJ9.eyJhbXIiOlsicGFzc3dvcmQiXSwiYXRfaGFzaCI6InowbjhudTNJQ19HaXN3bmFTWjgwZ2ciLCJhdWQiOlsiOGExYTIwNzUtMzY3Yi00ZGU1LTgyODgtMGMyNzQ1OTMzMmI3Il0sImF1dGhfdGltZSI6MTczMTUwMDQ0OSwiZXhwIjoxNzMxNTA0NDIyLCJpYXQiOjE3MzE1MDA4MjIsImlzcyI6Imh0dHA6Ly8xNzIuMTguMS4xMjguc3NsaXAuaW8vaWFtL29hdXRoIiwianRpIjoiZTlhMmQxYzQtZGViNy00MTgwLWE0M2YtN2QwNTZhYjNlNTk3Iiwibm9uY2UiOiJnV1JVZjhxTERaeDZpOFNhMXpMdm9IX2tSQ01OWll2WTE0WTFsLWNBU0tVIiwicmF0IjoxNzMxNTAwODIyLCJzaWQiOiJlNGVkOTc2Mi0yMmNlLTQyYzEtOTU3NC01MDVkYjAyMThhNDYiLCJzdWIiOiJhZmMwMTE3Zi1jOWNkLTRkOGMtYWNlZS1mYTE0MzNjYTBmZGQifQ.OACqa6WjpAeZbHR54b3p7saUk9plTdXlZsou41E-gfC7WxCG7ZEKfDPKXUky-r20oeIt1Ov3S2QL6Kefe5dTXEC6nhKGxeClg8ys56_FPcx_neI-p09_pSWOkMx7DHP65giaP7UubyyInVpE-2Eu1o6TpoheahNQfCahKDsJmJ-4Vvts3wA79UMfOI0WHO4vLaaG6DRAZQK_dv7ltw3p_WlncpaQAtHwY9iVhtdB3LtAI39EjplCoAF0c9uQO6W7KHWUlj24l2kc564bsJgZSrYvezw6b2-FY7YisVnicSyAORpeqhWEpLltH3D8I1NtHlSYMJhWuVZbBhAm7Iz6q1-W-Q9ttvdPchdwPSASFRkrjrdIyQf6MsFrItKzUxYck57EYL4GkyN9MWvMNxL1UTtkzGsFEczUVsJFm8OQpulYXIFZksmnPTBB0KuUUvEZ-xih8V1HsMRoHvbiCLaDJwjOFKzPevVggsSMysPKR52UAZJDZLTeHBnVCtQ3rro6T0RxNg94lXypz0AmfsGnoTF34u4FmMxzoeFZ9N5zmEpOnMRqLs7Sb3FnLL-IMitc9_2bsHuFbBJl8KbiGHBQihK5v5SIa292L7P9ChsxomWVhM29qHNFuXQMwFUr57hmveNh2Fz9mduZ5h2hLUuDf5xc6u9rSxy3_e3t_xBuUT4"; - private final String authorizedUserId ="afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; + private final String authHeader = + "Bearer" + + " eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5Njk0OTgyLWQzMTAtNDBkOC04ZDk4LTczOWI1ZGZjNWUyNiIsInR5cCI6IkpXVCJ9.eyJhbXIiOlsicGFzc3dvcmQiXSwiYXRfaGFzaCI6InowbjhudTNJQ19HaXN3bmFTWjgwZ2ciLCJhdWQiOlsiOGExYTIwNzUtMzY3Yi00ZGU1LTgyODgtMGMyNzQ1OTMzMmI3Il0sImF1dGhfdGltZSI6MTczMTUwMDQ0OSwiZXhwIjoxNzMxNTA0NDIyLCJpYXQiOjE3MzE1MDA4MjIsImlzcyI6Imh0dHA6Ly8xNzIuMTguMS4xMjguc3NsaXAuaW8vaWFtL29hdXRoIiwianRpIjoiZTlhMmQxYzQtZGViNy00MTgwLWE0M2YtN2QwNTZhYjNlNTk3Iiwibm9uY2UiOiJnV1JVZjhxTERaeDZpOFNhMXpMdm9IX2tSQ01OWll2WTE0WTFsLWNBU0tVIiwicmF0IjoxNzMxNTAwODIyLCJzaWQiOiJlNGVkOTc2Mi0yMmNlLTQyYzEtOTU3NC01MDVkYjAyMThhNDYiLCJzdWIiOiJhZmMwMTE3Zi1jOWNkLTRkOGMtYWNlZS1mYTE0MzNjYTBmZGQifQ.OACqa6WjpAeZbHR54b3p7saUk9plTdXlZsou41E-gfC7WxCG7ZEKfDPKXUky-r20oeIt1Ov3S2QL6Kefe5dTXEC6nhKGxeClg8ys56_FPcx_neI-p09_pSWOkMx7DHP65giaP7UubyyInVpE-2Eu1o6TpoheahNQfCahKDsJmJ-4Vvts3wA79UMfOI0WHO4vLaaG6DRAZQK_dv7ltw3p_WlncpaQAtHwY9iVhtdB3LtAI39EjplCoAF0c9uQO6W7KHWUlj24l2kc564bsJgZSrYvezw6b2-FY7YisVnicSyAORpeqhWEpLltH3D8I1NtHlSYMJhWuVZbBhAm7Iz6q1-W-Q9ttvdPchdwPSASFRkrjrdIyQf6MsFrItKzUxYck57EYL4GkyN9MWvMNxL1UTtkzGsFEczUVsJFm8OQpulYXIFZksmnPTBB0KuUUvEZ-xih8V1HsMRoHvbiCLaDJwjOFKzPevVggsSMysPKR52UAZJDZLTeHBnVCtQ3rro6T0RxNg94lXypz0AmfsGnoTF34u4FmMxzoeFZ9N5zmEpOnMRqLs7Sb3FnLL-IMitc9_2bsHuFbBJl8KbiGHBQihK5v5SIa292L7P9ChsxomWVhM29qHNFuXQMwFUr57hmveNh2Fz9mduZ5h2hLUuDf5xc6u9rSxy3_e3t_xBuUT4"; + private final String authorizedUserId = "afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; private final StorageService storageService = mock(StorageService.class); private final JwtReader jwtReader = mock(JwtReader.class); - private final MaskedInputController maskedInputController = new MaskedInputController(storageService, jwtReader); + private final MaskedInputController maskedInputController = + new MaskedInputController(storageService, jwtReader); @BeforeEach public void setUp() throws UnauthorizedException { - when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); + when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); } @Test void givenArgumentIsNull_whenUpload_thenThrowIllegalArgumentException() { IllegalArgumentException iae = - assertThrows(IllegalArgumentException.class, () -> maskedInputController.upload(authHeader, null)); + assertThrows( + IllegalArgumentException.class, () -> maskedInputController.upload(authHeader, null)); assertEquals("MaskedInput must not be null", iae.getMessage()); } @@ -58,7 +61,8 @@ void givenMaskedInputDataIsEmpty_whenUpload_thenThrowIllegalArgumentException() IllegalArgumentException iae = assertThrows( - IllegalArgumentException.class, () -> maskedInputController.upload(authHeader, maskedInput)); + IllegalArgumentException.class, + () -> maskedInputController.upload(authHeader, maskedInput)); assertEquals("MaskedInput data must not be empty", iae.getMessage()); } @@ -72,19 +76,20 @@ void givenSuccessfulRequest_whenUpload_thenReturnCreatedWithExpectedContent() { new MaskedInput( secretShareId, singletonList(MaskedInputData.of(new byte[16])), emptyList()); - when(storageService.createSecret(maskedInput, authorizedUserId)).thenReturn(secretShareId.toString()); + when(storageService.createSecret(maskedInput, authorizedUserId)) + .thenReturn(secretShareId.toString()); runInMockedHttpRequestContextForUri( expectedUri, () -> { - ResponseEntity actualResponse = null; - try { - actualResponse = maskedInputController.upload(authHeader, maskedInput); - } catch (UnauthorizedException e) { - fail("Unexpected exception thrown", e); - } + ResponseEntity actualResponse = null; + try { + actualResponse = maskedInputController.upload(authHeader, maskedInput); + } catch (UnauthorizedException e) { + fail("Unexpected exception thrown", e); + } - assertEquals(HttpStatus.CREATED, actualResponse.getStatusCode()); + assertEquals(HttpStatus.CREATED, actualResponse.getStatusCode()); assertEquals(expectedUri, actualResponse.getBody()); }); } diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/SecretShareControllerTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/SecretShareControllerTest.java index 367d749..2ed1ba8 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/SecretShareControllerTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/SecretShareControllerTest.java @@ -7,12 +7,23 @@ package io.carbynestack.amphora.service.rest; +import static io.carbynestack.amphora.common.TagFilterOperator.EQUALS; +import static io.carbynestack.amphora.common.TagFilterOperator.LESS_THAN; +import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.CRITERIA_SEPARATOR; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + import io.carbynestack.amphora.common.*; import io.carbynestack.amphora.service.calculation.OutputDeliveryService; import io.carbynestack.amphora.service.exceptions.CsOpaException; import io.carbynestack.amphora.service.exceptions.UnauthorizedException; import io.carbynestack.amphora.service.opa.JwtReader; import io.carbynestack.amphora.service.persistence.metadata.StorageService; +import java.util.List; +import java.util.UUID; import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -22,33 +33,24 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import java.util.List; -import java.util.UUID; - -import static io.carbynestack.amphora.common.TagFilterOperator.EQUALS; -import static io.carbynestack.amphora.common.TagFilterOperator.LESS_THAN; -import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.CRITERIA_SEPARATOR; -import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) class SecretShareControllerTest { - private final String authHeader = "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5Njk0OTgyLWQzMTAtNDBkOC04ZDk4LTczOWI1ZGZjNWUyNiIsInR5cCI6IkpXVCJ9.eyJhbXIiOlsicGFzc3dvcmQiXSwiYXRfaGFzaCI6InowbjhudTNJQ19HaXN3bmFTWjgwZ2ciLCJhdWQiOlsiOGExYTIwNzUtMzY3Yi00ZGU1LTgyODgtMGMyNzQ1OTMzMmI3Il0sImF1dGhfdGltZSI6MTczMTUwMDQ0OSwiZXhwIjoxNzMxNTA0NDIyLCJpYXQiOjE3MzE1MDA4MjIsImlzcyI6Imh0dHA6Ly8xNzIuMTguMS4xMjguc3NsaXAuaW8vaWFtL29hdXRoIiwianRpIjoiZTlhMmQxYzQtZGViNy00MTgwLWE0M2YtN2QwNTZhYjNlNTk3Iiwibm9uY2UiOiJnV1JVZjhxTERaeDZpOFNhMXpMdm9IX2tSQ01OWll2WTE0WTFsLWNBU0tVIiwicmF0IjoxNzMxNTAwODIyLCJzaWQiOiJlNGVkOTc2Mi0yMmNlLTQyYzEtOTU3NC01MDVkYjAyMThhNDYiLCJzdWIiOiJhZmMwMTE3Zi1jOWNkLTRkOGMtYWNlZS1mYTE0MzNjYTBmZGQifQ.OACqa6WjpAeZbHR54b3p7saUk9plTdXlZsou41E-gfC7WxCG7ZEKfDPKXUky-r20oeIt1Ov3S2QL6Kefe5dTXEC6nhKGxeClg8ys56_FPcx_neI-p09_pSWOkMx7DHP65giaP7UubyyInVpE-2Eu1o6TpoheahNQfCahKDsJmJ-4Vvts3wA79UMfOI0WHO4vLaaG6DRAZQK_dv7ltw3p_WlncpaQAtHwY9iVhtdB3LtAI39EjplCoAF0c9uQO6W7KHWUlj24l2kc564bsJgZSrYvezw6b2-FY7YisVnicSyAORpeqhWEpLltH3D8I1NtHlSYMJhWuVZbBhAm7Iz6q1-W-Q9ttvdPchdwPSASFRkrjrdIyQf6MsFrItKzUxYck57EYL4GkyN9MWvMNxL1UTtkzGsFEczUVsJFm8OQpulYXIFZksmnPTBB0KuUUvEZ-xih8V1HsMRoHvbiCLaDJwjOFKzPevVggsSMysPKR52UAZJDZLTeHBnVCtQ3rro6T0RxNg94lXypz0AmfsGnoTF34u4FmMxzoeFZ9N5zmEpOnMRqLs7Sb3FnLL-IMitc9_2bsHuFbBJl8KbiGHBQihK5v5SIa292L7P9ChsxomWVhM29qHNFuXQMwFUr57hmveNh2Fz9mduZ5h2hLUuDf5xc6u9rSxy3_e3t_xBuUT4"; - private final String authorizedUserId ="afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; + private final String authHeader = + "Bearer" + + " eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5Njk0OTgyLWQzMTAtNDBkOC04ZDk4LTczOWI1ZGZjNWUyNiIsInR5cCI6IkpXVCJ9.eyJhbXIiOlsicGFzc3dvcmQiXSwiYXRfaGFzaCI6InowbjhudTNJQ19HaXN3bmFTWjgwZ2ciLCJhdWQiOlsiOGExYTIwNzUtMzY3Yi00ZGU1LTgyODgtMGMyNzQ1OTMzMmI3Il0sImF1dGhfdGltZSI6MTczMTUwMDQ0OSwiZXhwIjoxNzMxNTA0NDIyLCJpYXQiOjE3MzE1MDA4MjIsImlzcyI6Imh0dHA6Ly8xNzIuMTguMS4xMjguc3NsaXAuaW8vaWFtL29hdXRoIiwianRpIjoiZTlhMmQxYzQtZGViNy00MTgwLWE0M2YtN2QwNTZhYjNlNTk3Iiwibm9uY2UiOiJnV1JVZjhxTERaeDZpOFNhMXpMdm9IX2tSQ01OWll2WTE0WTFsLWNBU0tVIiwicmF0IjoxNzMxNTAwODIyLCJzaWQiOiJlNGVkOTc2Mi0yMmNlLTQyYzEtOTU3NC01MDVkYjAyMThhNDYiLCJzdWIiOiJhZmMwMTE3Zi1jOWNkLTRkOGMtYWNlZS1mYTE0MzNjYTBmZGQifQ.OACqa6WjpAeZbHR54b3p7saUk9plTdXlZsou41E-gfC7WxCG7ZEKfDPKXUky-r20oeIt1Ov3S2QL6Kefe5dTXEC6nhKGxeClg8ys56_FPcx_neI-p09_pSWOkMx7DHP65giaP7UubyyInVpE-2Eu1o6TpoheahNQfCahKDsJmJ-4Vvts3wA79UMfOI0WHO4vLaaG6DRAZQK_dv7ltw3p_WlncpaQAtHwY9iVhtdB3LtAI39EjplCoAF0c9uQO6W7KHWUlj24l2kc564bsJgZSrYvezw6b2-FY7YisVnicSyAORpeqhWEpLltH3D8I1NtHlSYMJhWuVZbBhAm7Iz6q1-W-Q9ttvdPchdwPSASFRkrjrdIyQf6MsFrItKzUxYck57EYL4GkyN9MWvMNxL1UTtkzGsFEczUVsJFm8OQpulYXIFZksmnPTBB0KuUUvEZ-xih8V1HsMRoHvbiCLaDJwjOFKzPevVggsSMysPKR52UAZJDZLTeHBnVCtQ3rro6T0RxNg94lXypz0AmfsGnoTF34u4FmMxzoeFZ9N5zmEpOnMRqLs7Sb3FnLL-IMitc9_2bsHuFbBJl8KbiGHBQihK5v5SIa292L7P9ChsxomWVhM29qHNFuXQMwFUr57hmveNh2Fz9mduZ5h2hLUuDf5xc6u9rSxy3_e3t_xBuUT4"; + private final String authorizedUserId = "afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; private final OutputDeliveryService outputDeliveryService = mock(OutputDeliveryService.class); private final StorageService storageService = mock(StorageService.class); private final JwtReader jwtReader = mock(JwtReader.class); - private final SecretShareController secretShareController = new SecretShareController(storageService, outputDeliveryService, jwtReader); + private final SecretShareController secretShareController = + new SecretShareController(storageService, outputDeliveryService, jwtReader); @BeforeEach void setUp() throws UnauthorizedException { - when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); - reset(storageService, outputDeliveryService); + when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); + reset(storageService, outputDeliveryService); } @SneakyThrows @@ -164,7 +166,8 @@ void givenRequestIdArgumentIsNull_whenGetSecretShare_thenThrowIllegalArgumentExc } @Test - void givenSuccessfulRequest_whenGetSecretShare_thenReturnOkAndExpectedContent() throws CsOpaException, UnauthorizedException { + void givenSuccessfulRequest_whenGetSecretShare_thenReturnOkAndExpectedContent() + throws CsOpaException, UnauthorizedException { UUID secretId = UUID.fromString("3bcf8308-8f50-4d24-a37b-b0075bb5e779"); UUID requestId = UUID.fromString("d6d0f4ff-df28-4c96-b7df-95170320eaee"); SecretShare secretShare = SecretShare.builder().secretId(requestId).build(); @@ -186,9 +189,11 @@ void givenSuccessfulRequest_whenGetSecretShare_thenReturnOkAndExpectedContent() } @Test - void givenSuccessfulRequest_whenDeleteSecretShare_thenReturnOk() throws UnauthorizedException, CsOpaException { + void givenSuccessfulRequest_whenDeleteSecretShare_thenReturnOk() + throws UnauthorizedException, CsOpaException { UUID secretId = UUID.fromString("3bcf8308-8f50-4d24-a37b-b0075bb5e779"); - ResponseEntity actualResponse = secretShareController.deleteSecretShare(authHeader, secretId); + ResponseEntity actualResponse = + secretShareController.deleteSecretShare(authHeader, secretId); assertEquals(HttpStatus.OK, actualResponse.getStatusCode()); } diff --git a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/TagsControllerTest.java b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/TagsControllerTest.java index 1c74aac..f96440e 100644 --- a/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/TagsControllerTest.java +++ b/amphora-service/src/test/java/io/carbynestack/amphora/service/rest/TagsControllerTest.java @@ -23,9 +23,7 @@ import java.net.URI; import java.util.List; import java.util.UUID; - import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -36,8 +34,10 @@ @ExtendWith(MockitoExtension.class) class TagsControllerTest { - private final String authHeader = "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5Njk0OTgyLWQzMTAtNDBkOC04ZDk4LTczOWI1ZGZjNWUyNiIsInR5cCI6IkpXVCJ9.eyJhbXIiOlsicGFzc3dvcmQiXSwiYXRfaGFzaCI6InowbjhudTNJQ19HaXN3bmFTWjgwZ2ciLCJhdWQiOlsiOGExYTIwNzUtMzY3Yi00ZGU1LTgyODgtMGMyNzQ1OTMzMmI3Il0sImF1dGhfdGltZSI6MTczMTUwMDQ0OSwiZXhwIjoxNzMxNTA0NDIyLCJpYXQiOjE3MzE1MDA4MjIsImlzcyI6Imh0dHA6Ly8xNzIuMTguMS4xMjguc3NsaXAuaW8vaWFtL29hdXRoIiwianRpIjoiZTlhMmQxYzQtZGViNy00MTgwLWE0M2YtN2QwNTZhYjNlNTk3Iiwibm9uY2UiOiJnV1JVZjhxTERaeDZpOFNhMXpMdm9IX2tSQ01OWll2WTE0WTFsLWNBU0tVIiwicmF0IjoxNzMxNTAwODIyLCJzaWQiOiJlNGVkOTc2Mi0yMmNlLTQyYzEtOTU3NC01MDVkYjAyMThhNDYiLCJzdWIiOiJhZmMwMTE3Zi1jOWNkLTRkOGMtYWNlZS1mYTE0MzNjYTBmZGQifQ.OACqa6WjpAeZbHR54b3p7saUk9plTdXlZsou41E-gfC7WxCG7ZEKfDPKXUky-r20oeIt1Ov3S2QL6Kefe5dTXEC6nhKGxeClg8ys56_FPcx_neI-p09_pSWOkMx7DHP65giaP7UubyyInVpE-2Eu1o6TpoheahNQfCahKDsJmJ-4Vvts3wA79UMfOI0WHO4vLaaG6DRAZQK_dv7ltw3p_WlncpaQAtHwY9iVhtdB3LtAI39EjplCoAF0c9uQO6W7KHWUlj24l2kc564bsJgZSrYvezw6b2-FY7YisVnicSyAORpeqhWEpLltH3D8I1NtHlSYMJhWuVZbBhAm7Iz6q1-W-Q9ttvdPchdwPSASFRkrjrdIyQf6MsFrItKzUxYck57EYL4GkyN9MWvMNxL1UTtkzGsFEczUVsJFm8OQpulYXIFZksmnPTBB0KuUUvEZ-xih8V1HsMRoHvbiCLaDJwjOFKzPevVggsSMysPKR52UAZJDZLTeHBnVCtQ3rro6T0RxNg94lXypz0AmfsGnoTF34u4FmMxzoeFZ9N5zmEpOnMRqLs7Sb3FnLL-IMitc9_2bsHuFbBJl8KbiGHBQihK5v5SIa292L7P9ChsxomWVhM29qHNFuXQMwFUr57hmveNh2Fz9mduZ5h2hLUuDf5xc6u9rSxy3_e3t_xBuUT4"; - private final String authorizedUserId ="afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; + private final String authHeader = + "Bearer" + + " eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5Njk0OTgyLWQzMTAtNDBkOC04ZDk4LTczOWI1ZGZjNWUyNiIsInR5cCI6IkpXVCJ9.eyJhbXIiOlsicGFzc3dvcmQiXSwiYXRfaGFzaCI6InowbjhudTNJQ19HaXN3bmFTWjgwZ2ciLCJhdWQiOlsiOGExYTIwNzUtMzY3Yi00ZGU1LTgyODgtMGMyNzQ1OTMzMmI3Il0sImF1dGhfdGltZSI6MTczMTUwMDQ0OSwiZXhwIjoxNzMxNTA0NDIyLCJpYXQiOjE3MzE1MDA4MjIsImlzcyI6Imh0dHA6Ly8xNzIuMTguMS4xMjguc3NsaXAuaW8vaWFtL29hdXRoIiwianRpIjoiZTlhMmQxYzQtZGViNy00MTgwLWE0M2YtN2QwNTZhYjNlNTk3Iiwibm9uY2UiOiJnV1JVZjhxTERaeDZpOFNhMXpMdm9IX2tSQ01OWll2WTE0WTFsLWNBU0tVIiwicmF0IjoxNzMxNTAwODIyLCJzaWQiOiJlNGVkOTc2Mi0yMmNlLTQyYzEtOTU3NC01MDVkYjAyMThhNDYiLCJzdWIiOiJhZmMwMTE3Zi1jOWNkLTRkOGMtYWNlZS1mYTE0MzNjYTBmZGQifQ.OACqa6WjpAeZbHR54b3p7saUk9plTdXlZsou41E-gfC7WxCG7ZEKfDPKXUky-r20oeIt1Ov3S2QL6Kefe5dTXEC6nhKGxeClg8ys56_FPcx_neI-p09_pSWOkMx7DHP65giaP7UubyyInVpE-2Eu1o6TpoheahNQfCahKDsJmJ-4Vvts3wA79UMfOI0WHO4vLaaG6DRAZQK_dv7ltw3p_WlncpaQAtHwY9iVhtdB3LtAI39EjplCoAF0c9uQO6W7KHWUlj24l2kc564bsJgZSrYvezw6b2-FY7YisVnicSyAORpeqhWEpLltH3D8I1NtHlSYMJhWuVZbBhAm7Iz6q1-W-Q9ttvdPchdwPSASFRkrjrdIyQf6MsFrItKzUxYck57EYL4GkyN9MWvMNxL1UTtkzGsFEczUVsJFm8OQpulYXIFZksmnPTBB0KuUUvEZ-xih8V1HsMRoHvbiCLaDJwjOFKzPevVggsSMysPKR52UAZJDZLTeHBnVCtQ3rro6T0RxNg94lXypz0AmfsGnoTF34u4FmMxzoeFZ9N5zmEpOnMRqLs7Sb3FnLL-IMitc9_2bsHuFbBJl8KbiGHBQihK5v5SIa292L7P9ChsxomWVhM29qHNFuXQMwFUr57hmveNh2Fz9mduZ5h2hLUuDf5xc6u9rSxy3_e3t_xBuUT4"; + private final String authorizedUserId = "afc0117f-c9cd-4d8c-acee-fa1433ca0fdd"; private final UUID testSecretId = UUID.fromString("3bcf8308-8f50-4d24-a37b-b0075bb5e779"); private final Tag testTag = Tag.builder().key("key").value("value").valueType(TagValueType.STRING).build(); @@ -48,7 +48,8 @@ class TagsControllerTest { @InjectMocks private TagsController tagsController; @Test - void givenSuccessfulRequest_whenGetTags_thenReturnOkWithExpectedContent() throws UnauthorizedException, CsOpaException { + void givenSuccessfulRequest_whenGetTags_thenReturnOkWithExpectedContent() + throws UnauthorizedException, CsOpaException { List expectedList = singletonList(testTag); when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); @@ -60,16 +61,19 @@ void givenSuccessfulRequest_whenGetTags_thenReturnOkWithExpectedContent() throws } @Test - void givenTagIsNull_whenCreateTag_thenThrowIllegalArgumentException() throws CsOpaException, UnauthorizedException { + void givenTagIsNull_whenCreateTag_thenThrowIllegalArgumentException() + throws CsOpaException, UnauthorizedException { IllegalArgumentException iae = assertThrows( - IllegalArgumentException.class, () -> tagsController.createTag(authHeader, testSecretId, null)); + IllegalArgumentException.class, + () -> tagsController.createTag(authHeader, testSecretId, null)); verify(storageService, never()).storeTag(any(), any(), eq(authorizedUserId)); assertEquals("Tag must not be empty", iae.getMessage()); } @Test - void givenSuccessfulRequest_whenCreateTag_thenReturnCreatedWithExpectedContent() throws UnauthorizedException, CsOpaException { + void givenSuccessfulRequest_whenCreateTag_thenReturnCreatedWithExpectedContent() + throws UnauthorizedException, CsOpaException { URI expectedUri = URI.create( "https://amphora.carbynestack.io" + INTRA_VCP_OPERATIONS_SEGMENT + "/" + testSecretId); @@ -79,12 +83,12 @@ void givenSuccessfulRequest_whenCreateTag_thenReturnCreatedWithExpectedContent() runInMockedHttpRequestContextForUri( expectedUri, () -> { - ResponseEntity actualResponse = null; - try { - actualResponse = tagsController.createTag(authHeader, testSecretId, testTag); - } catch (UnauthorizedException | CsOpaException e) { - Assertions.fail("unexpected exception thrown: " + e); - } + ResponseEntity actualResponse = null; + try { + actualResponse = tagsController.createTag(authHeader, testSecretId, testTag); + } catch (UnauthorizedException | CsOpaException e) { + Assertions.fail("unexpected exception thrown: " + e); + } assertEquals(HttpStatus.CREATED, actualResponse.getStatusCode()); assertEquals(expectedUri, actualResponse.getBody()); }); @@ -93,7 +97,8 @@ void givenSuccessfulRequest_whenCreateTag_thenReturnCreatedWithExpectedContent() } @Test - void givenTagsAreEmpty_whenUpdateTags_thenThrowIllegalArgumentException() throws CsOpaException, UnauthorizedException { + void givenTagsAreEmpty_whenUpdateTags_thenThrowIllegalArgumentException() + throws CsOpaException, UnauthorizedException { List emptyTags = emptyList(); IllegalArgumentException iae = assertThrows( @@ -104,38 +109,46 @@ void givenTagsAreEmpty_whenUpdateTags_thenThrowIllegalArgumentException() throws } @Test - void givenSuccessfulRequest_whenUpdateTags_thenReturnCreatedWithExpectedContent() throws UnauthorizedException, CsOpaException { + void givenSuccessfulRequest_whenUpdateTags_thenReturnCreatedWithExpectedContent() + throws UnauthorizedException, CsOpaException { List newTagList = singletonList(testTag); when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); - ResponseEntity actualResponse = tagsController.updateTags(authHeader, testSecretId, newTagList); + ResponseEntity actualResponse = + tagsController.updateTags(authHeader, testSecretId, newTagList); verify(storageService, times(1)).replaceTags(testSecretId, newTagList, authorizedUserId); assertEquals(HttpStatus.OK, actualResponse.getStatusCode()); } @Test - void givenSuccessfulRequest_whenGetTag_thenReturnOkWithExpectedContent() throws UnauthorizedException, CsOpaException { + void givenSuccessfulRequest_whenGetTag_thenReturnOkWithExpectedContent() + throws UnauthorizedException, CsOpaException { when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); - when(storageService.retrieveTag(testSecretId, testTag.getKey(), authorizedUserId)).thenReturn(testTag); + when(storageService.retrieveTag(testSecretId, testTag.getKey(), authorizedUserId)) + .thenReturn(testTag); - ResponseEntity actualResponse = tagsController.getTag(authHeader, testSecretId, testTag.getKey()); + ResponseEntity actualResponse = + tagsController.getTag(authHeader, testSecretId, testTag.getKey()); assertEquals(HttpStatus.OK, actualResponse.getStatusCode()); assertEquals(testTag, actualResponse.getBody()); } @Test - void givenTagIsNull_whenPutTag_thenTrowIllegalArgumentException() throws CsOpaException, UnauthorizedException { + void givenTagIsNull_whenPutTag_thenTrowIllegalArgumentException() + throws CsOpaException, UnauthorizedException { String key = testTag.getKey(); IllegalArgumentException iae = assertThrows( - IllegalArgumentException.class, () -> tagsController.putTag(authHeader, testSecretId, key, null)); + IllegalArgumentException.class, + () -> tagsController.putTag(authHeader, testSecretId, key, null)); verify(storageService, never()).updateTag(testSecretId, testTag, authorizedUserId); assertEquals("Tag must not be empty", iae.getMessage()); } @Test - void givenTagConfigurationDoesNotMatchAddressedKey_whenPutTag_thenTrowIllegalArgumentException() throws CsOpaException, UnauthorizedException { + void givenTagConfigurationDoesNotMatchAddressedKey_whenPutTag_thenTrowIllegalArgumentException() + throws CsOpaException, UnauthorizedException { String nonMatchingKey = testTag.getKey() + "_different"; IllegalArgumentException iae = assertThrows( @@ -149,7 +162,8 @@ void givenTagConfigurationDoesNotMatchAddressedKey_whenPutTag_thenTrowIllegalArg } @Test - void givenSuccessfulRequest_whenPutTag_thenReturnOk() throws UnauthorizedException, CsOpaException { + void givenSuccessfulRequest_whenPutTag_thenReturnOk() + throws UnauthorizedException, CsOpaException { when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); ResponseEntity actualResponse = @@ -159,10 +173,12 @@ void givenSuccessfulRequest_whenPutTag_thenReturnOk() throws UnauthorizedExcepti } @Test - void givenSuccessfulRequest_whenDeleteTag_thenReturnOk() throws UnauthorizedException, CsOpaException { + void givenSuccessfulRequest_whenDeleteTag_thenReturnOk() + throws UnauthorizedException, CsOpaException { when(jwtReader.extractUserIdFromAuthHeader(authHeader)).thenReturn(authorizedUserId); - ResponseEntity actualResponse = tagsController.deleteTag(authHeader, testSecretId, testTag.getKey()); + ResponseEntity actualResponse = + tagsController.deleteTag(authHeader, testSecretId, testTag.getKey()); verify(storageService, times(1)).deleteTag(testSecretId, testTag.getKey(), authorizedUserId); assertEquals(HttpStatus.OK, actualResponse.getStatusCode()); } diff --git a/amphora-service/src/test/resources/application-test.properties b/amphora-service/src/test/resources/application-test.properties index dedf86d..5531c64 100644 --- a/amphora-service/src/test/resources/application-test.properties +++ b/amphora-service/src/test/resources/application-test.properties @@ -13,7 +13,7 @@ spring.datasource.username=${POSTGRESQL_USERNAME} spring.datasource.password=${POSTGRESQL_PASSWORD} spring.jpa.database=postgresql spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hi.hibernate.connection.autocommit=true +spring.jpa.hibernate.connection.autocommit=true spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.hbm2ddl.auto=update diff --git a/amphora-service/src/test/resources/redis.conf b/amphora-service/src/test/resources/redis.conf index ae3a97d..1dafdb1 100644 --- a/amphora-service/src/test/resources/redis.conf +++ b/amphora-service/src/test/resources/redis.conf @@ -197,4 +197,4 @@ aof-load-truncated yes # # This is currently turned off by default in order to avoid the surprise # of a format change, but will at some point be used as the default. -aof-use-rdb-preamble no \ No newline at end of file +aof-use-rdb-preamble no