From a3c65689ed9a488a3a37b643327c661087823488 Mon Sep 17 00:00:00 2001 From: Sebastian Becker Date: Thu, 14 Nov 2024 15:43:42 +0100 Subject: [PATCH] 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