diff --git a/README.md b/README.md index 2b1b855d0b217e7b511e3c3d0f2bb706d9a2c420..33d526e69d0ec8e093d9556c90de812cea3f514a 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ The Partition service is responsible for creating and retrieving partition specific properties on behalf of other services whether they are secret values or not. It is a Maven multi-module project with each cloud implementation placed in its submodule. ## Running Locally - AWS +Instructions for running the AWS implementation locally can be found [here](./provider/partition-aws/README.md) ## Running Locally - Azure Instructions for running the Azure implementation locally can be found [here](./provider/partition-azure/README.md) ## Running Locally - GCP diff --git a/provider/partition-aws/README.md b/provider/partition-aws/README.md new file mode 100644 index 0000000000000000000000000000000000000000..42ae2e1478a21b717313c98a4193dc1d61c81178 --- /dev/null +++ b/provider/partition-aws/README.md @@ -0,0 +1,131 @@ +# Partition Service +The AWS Partition service is a [Spring Boot](https://spring.io/projects/spring-boot) service that creates, reads, updates, and destroys partition properties. The partition properties are stored in a [MongoDB database](https://www.mongodb.com/) and encrypted by [AWS KMS](https://aws.amazon.com/kms/) if the properties are flagged as sensitive. + +## Running Locally +The following instructions are the minimum requirements for running the AWS partition service locally. + +### Prerequisites +* [JDK 8](https://docs.aws.amazon.com/corretto/latest/corretto-8-ug/downloads-list.html) +* [Maven 3.6.0+](https://maven.apache.org/download.cgi) +* IDE ([IntelliJ](https://www.jetbrains.com/idea/download/) is preferred) +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) +* [Postman](https://www.postman.com/) +* [MongoDB](https://docs.mongodb.com/v4.0/installation/) on local machine or a MongoDB connection string for test database. +* [Mongo Compass](https://www.mongodb.com/products/compass) <Optional> +* OSDU Instance deployed on AWS + +### Service Configuration +The following environment variables need to be defined to run the service locally. + +| Name | Example Value | Description | Sensitive? | Source | +| --- | --- | --- | --- | --- | +| `LOCAL_MODE` | `true` | A required flag to indicate to the authorization service that partition service is running locally versus in the cluster | no | - | +| `AWS_REGION` | ex `us-east-1` | The region where resources needed by the service are deployed | no | - | +| `AWS_ACCESS_KEY_ID` | - | The AWS Access Key for a user with access to Backend Resources required by the service | yes | [temporary security credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html) | +| `AWS_SECRET_ACCESS_KEY` | - | The AWS Secret Key for a user with access to Backend Resources required by the service | yes | [temporary security credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html) | +| `AWS_SESSION_TOKEN` | - | AWS Session token needed if using an SSO user session to authenticate | yes | [temporary security credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html) | +| `ENVIRONMENT` | ex `osdu-dev` | The name of the environment stack name | no | Output by infrastructure deployment | +| `ENTITLEMENTS_BASE_URL` | ex `https://alias.dev.osdu.aws` | The base URL of OSDU | no | Output by infrastructure deployment | +| `MONGODB_ENDPOINT` | ex `mongodbname.robot.mongodb.net` or `localhost` | MongoDB database endpoint used for local development | yes | https://www.mongodb.com/ | +| `MONGODB_AUTH_DATABASE` | ex `partitions` | The database name within the MongoDB instance - defaults to `partitions` | no | - | +| `MONGODB_USERNAME` | ex `admin` | MongoDB username used as part of the connection string | yes | - | +| `MONGODB_PASSWORD` | - | MongoDB password used as part of the connection string | yes | - | +| `MONGODB_USE_SRV_ENDPOINT` | `false` or `true` | To run the service locally, set this to false | no | - | +| `KEY_ARN` | ex `arn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab` | A symmetric AWS KMS encryption key ARN with the appropriate key user policy that allows encrypt/decrypt access to the IAM role configured in the following step | yes | https://docs.aws.amazon.com/kms/latest/developerguide/create-keys.html | + +### MongoDB Setup Locally +* Navigate to where the mongo sever is installed and start the server using: + ``C:\Program Files\MongoDB\Server\5.0\bin>mongod`` + +The server will start on the default port 27017 +* Launch MongoDB Compass and create a new connection to localhost: 27017 + + +* Next add a db user using mongo shell using the following commands: + + +### Run Locally +Check that maven is installed: + +```bash +mvn --version +Apache Maven 3.8.3 +Maven home: C:\opt\apache-maven-3.8.3 +Java version: 1.8.0_302, vendor: Amazon.com Inc., runtime: C:\Program Files\Amazon Corretto\jdk1.8.0_302\jre +``` + +You may need to configure access to the remote maven repository that holds the OSDU dependencies. Copy one of the below files' content to your .m2 folder +* For development against the OSDU GitLab environment, leverage: `<REPO_ROOT>~/.mvn/community-maven.settings.xml` +* For development in an AWS Environment, leverage: `<REPO_ROOT>/provider/legal-aws/maven/settings.xml` + + +* Navigate to the AWS partition service's root folder and run: +```bash +mvn clean package -pl partition-core,provider/partition-aws +``` + +* If you wish to build the project without running tests +```bash +mvn clean package -pl partition-core,provider/partition-aws -DskipTests +``` + +After configuring your environment as specified above, you can follow these steps to run the application. These steps should be invoked from the *repository root.* +NOTE: If not on osx/linux: Replace `*` with version numbers as defined in the provider/partition-aws/pom.xml file + +```bash +java -jar provider/partition-aws/target/partition-aws-*.*.*-SNAPSHOT-spring-boot.jar +``` + +Alternatively, if using IntelliJ, you can configure your environment variables and run configuration by selecting Run>Edit Configurations and fill in the below information: +Main Class: org.opengroup.osdu.partition.provider.aws.PartitionApplication +Use Classpath of Module: partition-aws +Environment Variables: (As defined above) + +To run the configuration, select Run>Run and select your configuration. + +### Hitting Partitions API +The service will be accessible at [http://localhost:8080/api/partition/v1/partitions/](http://localhost:8080/api/partition/v1/partitions/). A /info endpoint is available at [http://localhost:8080/api/partition/v1/info/](http://localhost:8080/api/partition/v1/info/). + +A bearer access token is required to authorize all partitions API requests, except for /info. To generate an access token a POST request must be sent to the following URL: +{{auth_token_url}}?grant_type=client_credentials&client_id={{client_id}}&scope={{scope}}. The request must use "Basic Auth" with the client ID and secret passed in as parameters. The table below explains where to find these parameters: + +| Parameter | Value | Sensitive? | Source | +| --- | --- | --- | --- | +| `auth_token_url` | ex `https://osdu-dev-888733619319.auth.us-east-1.amazoncognito.com/oauth2/token` | no | Found in AWS SSM under resource path /osdu/{resource_prefix}/client-credentials-client-id | +| `client_id` | - | yes | Found in AWS SSM under resource path /osdu/{resource_prefix}/client-credentials-client-id | +| `client_secret` | - | yes | Found in AWS Secrets Manager under resource path /osdu/{resource_prefix}/client_credentials_secret | +| `scope` | ex `osduOnAws/osduOnAWSService` | no | Found in AWS SSM under resource path /osdu/{resource_prefix}/oauth-custom-scope | + +All partitions API requests should use Bearer Token auth using the access token returned from hitting the above endpoint. + +## Testing +### Running Unit Tests +Navigate to the partition service's root folder: +```bash +cd provider/partition-aws +``` +Install the project dependencies and run unit tests: +```bash +mvn clean install +``` + +### Running Integration Tests +Execute following command to build code and run all the integration tests from the root folder: + +```bash +mvn clean package -f testing/pom.xml -pl partition-test-core,partition-test-aws -DskipTests +mvn test -f testing/partition-test-aws/pom.xml +``` + +## Licence +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/provider/partition-aws/pom.xml b/provider/partition-aws/pom.xml index 4cef8a49bf4d9511062c66290a1e9ffbac706db1..9a6332cda42ef4771b63e2190003df4b82905390 100644 --- a/provider/partition-aws/pom.xml +++ b/provider/partition-aws/pom.xml @@ -87,6 +87,11 @@ <groupId>com.amazonaws</groupId> <artifactId>aws-java-sdk-cognitoidentity</artifactId> </dependency> + <dependency> + <groupId>com.amazonaws</groupId> + <artifactId>aws-encryption-sdk-java</artifactId> + <version>2.3.3</version> + </dependency> <!-- Third party Apache 2.0 license packages --> <dependency> @@ -104,9 +109,12 @@ <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-commons</artifactId> - <version>2.1.10.RELEASE</version> <scope>compile</scope> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-mongodb</artifactId> + </dependency> <dependency> <groupId>javax.inject</groupId> <artifactId>javax.inject</artifactId> @@ -192,6 +200,26 @@ </execution> </executions> </plugin> + <plugin> + <groupId>pl.project13.maven</groupId> + <artifactId>git-commit-id-plugin</artifactId> + <version>4.0.5</version> + <executions> + <execution> + <goals> + <goal>revision</goal> + </goals> + </execution> + </executions> + <configuration> + <verbose>true</verbose> + <dateFormat>yyyy-MM-dd'T'HH:mm:ssZ</dateFormat> + <generateGitPropertiesFile>true</generateGitPropertiesFile> + <generateGitPropertiesFilename> + ${project.build.outputDirectory}/git.properties + </generateGitPropertiesFilename> + </configuration> + </plugin> </plugins> </build> diff --git a/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/AwsServiceConfig.java b/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/model/IPartitionRepository.java similarity index 51% rename from provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/AwsServiceConfig.java rename to provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/model/IPartitionRepository.java index 005231193c09ea5f3c2bca498a4546a7a2ff1497..7b4d6c492256a46b9ec9b97cc4549dda4cf24f2c 100644 --- a/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/AwsServiceConfig.java +++ b/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/model/IPartitionRepository.java @@ -1,4 +1,4 @@ -// Copyright © 2020, Amazon Web Services +// Copyright © 2021 Amazon Web Services // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,23 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package org.opengroup.osdu.partition.provider.aws; +package org.opengroup.osdu.partition.provider.aws.model; -import javax.annotation.PostConstruct; +import org.springframework.data.mongodb.repository.MongoRepository; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; +import java.util.List; +import java.util.Optional; -@Component -public class AwsServiceConfig { - - @Value("${aws.resource.prefix}") - public String environment; +public interface IPartitionRepository extends MongoRepository<Partition, String> { - public String ssmPartitionPrefix; - - @PostConstruct - public void init() { - ssmPartitionPrefix = "/osdu/" + environment + "/partition/partitions/"; - } + public Optional<Partition> findById(String id); + public void deleteById(String id); + public List<Partition> findAll(); } diff --git a/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/model/Partition.java b/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/model/Partition.java new file mode 100644 index 0000000000000000000000000000000000000000..642f014f6316b55fdbb8206668a19b2edc786a2b --- /dev/null +++ b/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/model/Partition.java @@ -0,0 +1,39 @@ +// Copyright © 2021 Amazon Web Services +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.opengroup.osdu.partition.provider.aws.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.opengroup.osdu.partition.model.Property; +import org.springframework.data.mongodb.core.mapping.MongoId; + +import javax.validation.constraints.NotEmpty; +import java.util.HashMap; +import java.util.Map; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Partition { + + @NotEmpty + @MongoId + String id; + + Map<String, Property> properties = new HashMap<>(); + +} + diff --git a/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/security/BasicAuthSecurityConfig.java b/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/security/BasicAuthSecurityConfig.java index 7193321fcc65b1ca2790d45cf1684ca3c9b96383..ed04b17655727bcce86ad040dd93782220343a7d 100644 --- a/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/security/BasicAuthSecurityConfig.java +++ b/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/security/BasicAuthSecurityConfig.java @@ -27,4 +27,4 @@ public class BasicAuthSecurityConfig extends WebSecurityConfigurerAdapter { ((HttpSecurity)http.httpBasic().disable()) .csrf().disable(); } -} +} \ No newline at end of file diff --git a/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/service/PartitionServiceImpl.java b/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/service/PartitionServiceImpl.java index ad69c012f8c112aa29f8c94d65ede3e7f827125c..6cf6c44c81c2c99e5e53b72b6a33e4126c31a621 100644 --- a/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/service/PartitionServiceImpl.java +++ b/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/service/PartitionServiceImpl.java @@ -14,12 +14,18 @@ package org.opengroup.osdu.partition.provider.aws.service; +import java.util.*; + +import lombok.RequiredArgsConstructor; import org.apache.http.HttpStatus; +import org.bson.types.Binary; import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; import org.opengroup.osdu.core.common.model.http.AppException; import org.opengroup.osdu.partition.model.PartitionInfo; import org.opengroup.osdu.partition.model.Property; -import org.opengroup.osdu.partition.provider.aws.util.SSMHelper; +import org.opengroup.osdu.partition.provider.aws.model.Partition; +import org.opengroup.osdu.partition.provider.aws.model.IPartitionRepository; +import org.opengroup.osdu.partition.provider.aws.util.AwsKmsEncryptionClient; import org.opengroup.osdu.partition.provider.interfaces.IPartitionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -31,179 +37,158 @@ import java.util.Map; * AWS implementation doesn't use cache. */ @Service +@RequiredArgsConstructor public class PartitionServiceImpl implements IPartitionService { @Autowired private JaxRsDpsLog logger; - - @Autowired - private SSMHelper ssmHelper; - public PartitionServiceImpl() { - - } + private final IPartitionRepository repository; + + private final AwsKmsEncryptionClient awsKmsEncryptionClient; @Override public PartitionInfo createPartition(String partitionId, PartitionInfo partitionInfo) { - - if (ssmHelper.partitionExists(partitionId)) { + + // throw error if partition already exists + if (repository.findById(partitionId).isPresent()) { throw new AppException(HttpStatus.SC_CONFLICT, "partition exist", "Partition with same id exist"); } - try { - for (Map.Entry<String, Property> entry : partitionInfo.getProperties().entrySet()) { - ssmHelper.createOrUpdateSecret(partitionId, entry.getKey(), entry.getValue().getValue()); - } + Partition savedPartition = repository.save(new Partition(partitionId, encryptSensitiveProperties(partitionInfo, partitionId))); - /** - * SSM parameters are not immediately available after pushing to System Manager. - * This API is expected to return a 200 response meaning that the parameters should be available immediately. - * This logic is added to validate when the parameters become available before returning the 200 response. - * The performance hit is acceptable because partitions are only created as an early operation and shouldn't affect - * the performance of runtime workflows - */ - int retryCount = 10; - boolean partitionReady = false; - while (!partitionReady && retryCount > 0) { - retryCount--; - List<String> partitionCheck = ssmHelper.getSsmParamsPathsForPartition(partitionId); - if (partitionCheck.size() == partitionInfo.getProperties().size()) - partitionReady = true; - else - Thread.sleep(500); - } + return new PartitionInfo(savedPartition.getProperties()); + } - String rollbackSuccess = "Failed"; - if (!partitionReady) { - try { - ssmHelper.deletePartitionSecrets(partitionId); - rollbackSuccess = "Succeeded"; - } - catch (Exception e){ + @Override + public PartitionInfo updatePartition(String partitionId, PartitionInfo partitionInfo) { - } + Optional<Partition> partition = repository.findById(partitionId); - throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Partition Creation Failed", "One or more secrets couldn't be stored. Rollback " + rollbackSuccess); - - } + // throw error if search did not return a result + if (!partition.isPresent()) { + throw new AppException(HttpStatus.SC_NOT_FOUND, "Partition does not exist", "Partition does not exist"); } - catch (AppException appE) { - throw appE; + + // throw error if the client tries to update the id + if (partitionInfo.getProperties().containsKey("id")) { + throw new AppException(HttpStatus.SC_BAD_REQUEST, "Cannot update id", "the field id cannot be updated"); } - catch (Exception e) { - try { - Thread.sleep(2000); //wait for any existing ssm parameters that got added to normalize - ssmHelper.deletePartitionSecrets(partitionId); - } - catch (Exception deleteE) { - //if the partition didnt get created at all deletePartition will throw an exception. Eat it so we return the creation exception. - } + Map<String, Property> updatedProperties = partition.get().getProperties(); + Map<String, Property> encryptedPropertiesToAdd = encryptSensitiveProperties(partitionInfo, partitionId); - logger.error("Failed to create partition due to key creation failure in ssm", e.getMessage()); - throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Partition Creation Failure", e.getMessage(), e); + for (Map.Entry<String, Property> e : encryptedPropertiesToAdd.entrySet()) { + updatedProperties.put(e.getKey(), e.getValue()); } - + + // throw error if save was unsuccessful + try { + repository.save(new Partition(partitionId, updatedProperties)); + } catch (Exception e) { + logger.error("Failed to update partition", e.getMessage()); + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Partition update Failure", e.getMessage(), e); + } + return partitionInfo; } @Override - public PartitionInfo updatePartition(String partitionId, PartitionInfo partitionInfo) { + public PartitionInfo getPartition(String partitionId) { + Optional<Partition> partition = repository.findById(partitionId); - if (!ssmHelper.partitionExists(partitionId)) { - throw new AppException(HttpStatus.SC_NOT_FOUND, "partition does not exist", "Partition doesn't exist"); + // throw error if partition doesn't exist + if (!partition.isPresent()) { + throw new AppException(HttpStatus.SC_NOT_FOUND, "Partition not found", String.format("%s partition not found", partitionId)); } - if(partitionInfo.getProperties().containsKey("id")) { - throw new AppException(HttpStatus.SC_BAD_REQUEST, "Cannot update id", "the field id cannot be updated"); - } + PartitionInfo partitionInfo; try { - for (Map.Entry<String, Property> entry : partitionInfo.getProperties().entrySet()) { - ssmHelper.createOrUpdateSecret(partitionId, entry.getKey(), entry.getValue().getValue()); - } - - /** - * SSM parameters are not immediately available after pushing to System Manager. - * This API is expected to return a 200 response meaning that the parameters should be available immediately. - * This logic is added to validate when the parameters become available before returning the 200 response. - * The performance hit is acceptable because partitions are only created as an early operation and shouldn't affect - * the performance of runtime workflows - */ - int retryCount = 10; - boolean partitionReady = false; - while (!partitionReady && retryCount > 0) { - retryCount--; - List<String> partitionCheck = ssmHelper.getSsmParamsPathsForPartition(partitionId); - if (partitionCheck.size() == partitionInfo.getProperties().size()) - partitionReady = true; - else - Thread.sleep(500); - } + partitionInfo = decryptSensitiveProperties(partition.get().getProperties(), partitionId); + } catch (ClassCastException e) { + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Corrupt data", String.format("%s contains unreadable data", partitionId)); + } catch (IllegalStateException e) { + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Illegal database modification", String.format("Partition %s has been modified without permission", partitionId)); + } - String rollbackSuccess = "Failed"; - if (!partitionReady) { - try { - ssmHelper.deletePartitionSecrets(partitionId); - rollbackSuccess = "Succeeded"; - } - catch (Exception e){ + return partitionInfo; - } + } - throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Partition update Failed", "One or more secrets couldn't be stored. Rollback " + rollbackSuccess); + @Override + public boolean deletePartition(String partitionId) { - } - } - catch (AppException appE) { - throw appE; + // throw error if partition doesn't exist + if (!repository.findById(partitionId).isPresent()) { + throw new AppException(HttpStatus.SC_NOT_FOUND, "Partition not found", String.format("%s partition not found", partitionId)); } - catch (Exception e) { - try { - Thread.sleep(2000); //wait for any existing ssm parameters that got added to normalize - ssmHelper.deletePartitionSecrets(partitionId); - } - catch (Exception deleteE) { - //if the partition didnt get created at all deletePartition will throw an exception. Eat it so we return the creation exception. - } + repository.deleteById(partitionId); - logger.error("Failed to update partition due to key creation failure in ssm", e.getMessage()); - throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Partition update Failure", e.getMessage(), e); - } - - return partitionInfo; + return true; } @Override - public PartitionInfo getPartition(String partitionId) { + public List<String> getAllPartitions() { - Map<String,Property> secrets = ssmHelper.getPartitionSecrets(partitionId); + List<Partition> partitionList = repository.findAll(); + List<String> partitions = new ArrayList<>(); - //throw error if partition doesn't exist - if (secrets.size() <= 0) { - throw new AppException(HttpStatus.SC_NOT_FOUND, "partition not found", String.format("%s partition not found", partitionId)); + // populate list of ids, i.e. partition names + for (Partition p: partitionList) { + String id = p.getId(); + partitions.add(id); } - - return PartitionInfo.builder().properties(secrets).build(); + + return partitions; } - @Override - public boolean deletePartition(String partitionId) { - - if (!ssmHelper.partitionExists(partitionId)) { - throw new AppException(HttpStatus.SC_NOT_FOUND, "partition not found", String.format("%s partition not found", partitionId)); + private Map<String, Property> encryptSensitiveProperties(PartitionInfo partitionInfo, String id) { + + Map<String, Property> encryptedProperties = new HashMap<>(); + + // encrypt all properties that are flagged as sensitive + for (Map.Entry<String, Property> e : partitionInfo.getProperties().entrySet()) { + + Property encryptedProp = new Property(); + encryptedProp.setSensitive(e.getValue().isSensitive()); + + if (encryptedProp.isSensitive()) { + encryptedProp.setValue(awsKmsEncryptionClient.encrypt(e.getValue().getValue().toString(), id)); + } else { + encryptedProp.setValue(e.getValue().getValue()); + } + + encryptedProperties.put(e.getKey(), encryptedProp); } - return ssmHelper.deletePartitionSecrets(partitionId); + return encryptedProperties; } - @Override - public List<String> getAllPartitions() { + private PartitionInfo decryptSensitiveProperties(Map<String, Property> properties, String id) throws ClassCastException { + + HashMap<String,Property> decryptedProperties = new HashMap<>(); + + // decrypt all properties that are flagged as sensitive + for (Map.Entry<String, Property> e : properties.entrySet()) { + + Property decryptedProp = new Property(); + decryptedProp.setSensitive(e.getValue().isSensitive()); + + if (decryptedProp.isSensitive()) { + Binary bin = (Binary) e.getValue().getValue(); + decryptedProp.setValue(awsKmsEncryptionClient.decrypt(bin.getData(), id)); + } else { + decryptedProp.setValue(e.getValue().getValue()); + } + + decryptedProperties.put(e.getKey(), decryptedProp); + } - return ssmHelper.getPartitions(); + return new PartitionInfo(decryptedProperties); } } diff --git a/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/util/AwsKmsEncryptionClient.java b/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/util/AwsKmsEncryptionClient.java new file mode 100644 index 0000000000000000000000000000000000000000..ae2d30448e72d936cd98a3bf89579be022ea05cc --- /dev/null +++ b/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/util/AwsKmsEncryptionClient.java @@ -0,0 +1,110 @@ +// Copyright © 2021 Amazon Web Services +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.opengroup.osdu.partition.provider.aws.util; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Map; + +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.CryptoResult; +import com.amazonaws.encryptionsdk.kms.KmsMasterKey; +import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; +import com.amazonaws.encryptionsdk.CommitmentPolicy; +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.Data; + +import org.opengroup.osdu.core.aws.iam.IAMConfig; +import org.opengroup.osdu.core.aws.ssm.K8sLocalParameterProvider; +import org.opengroup.osdu.core.aws.ssm.K8sParameterNotFoundException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import javax.annotation.PostConstruct; + + +/** + * <p> + * Encrypts and then decrypts data using an AWS KMS key. + * <p> + */ + +@Data +@Component +public class AwsKmsEncryptionClient { + + @Value("${aws.kms.keyArn}") + private String keyArn; + + @Value("${spring.data.mongodb.database}") + private String authDatabase; + + private AWSCredentialsProvider amazonAWSCredentials; + + private KmsMasterKeyProvider keyProvider; + + private static final AwsCrypto crypto = AwsCrypto.builder() + .withCommitmentPolicy(CommitmentPolicy.RequireEncryptRequireDecrypt) + .build(); + + @PostConstruct + private void initializeKeyProvider() throws K8sParameterNotFoundException, JsonProcessingException { + + // grab key arn from K8s + K8sLocalParameterProvider provider = new K8sLocalParameterProvider(); + + if (!provider.getLocalMode()) { + keyArn = provider.getParameterAsStringOrDefault("KEY_ARN", keyArn); + } + + // log in with IAM credentials + amazonAWSCredentials = IAMConfig.amazonAWSCredentials(); + + // generate keyProvider + this.keyProvider = KmsMasterKeyProvider.builder() + .withCredentials(amazonAWSCredentials) + .buildStrict(keyArn); + } + + public byte[] encrypt(String plainText, String id) { + + final Map<String, String> encryptionContext = generateEncryptionContext(id); + final CryptoResult<byte[], KmsMasterKey> encryptResult = crypto.encryptData(keyProvider, plainText.getBytes(StandardCharsets.UTF_8), encryptionContext); + final byte[] ciphertext = encryptResult.getResult(); + + return ciphertext; + } + + public String decrypt(byte[] ciphertext, String id) { + + final CryptoResult<byte[], KmsMasterKey> decryptResult = crypto.decryptData(keyProvider, ciphertext); + final Map<String, String> encryptionContext = generateEncryptionContext(id); + + // throw error if context doesn't match + if (!encryptionContext.entrySet().stream() + .allMatch(e -> e.getValue().equals(decryptResult.getEncryptionContext().get(e.getKey())))) { + throw new IllegalStateException("Wrong Encryption Context!"); + } + + return new String(decryptResult.getResult(), StandardCharsets.UTF_8); + } + + private Map<String, String> generateEncryptionContext(String id) { + return Collections.singletonMap(authDatabase, id); + } + +} + + diff --git a/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/util/MongoConfig.java b/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/util/MongoConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2b6bada345918473489cab776293a57788a9650b --- /dev/null +++ b/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/util/MongoConfig.java @@ -0,0 +1,76 @@ +// Copyright © 2021 Amazon Web Services +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.opengroup.osdu.partition.provider.aws.util; + +import com.mongodb.ConnectionString; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.core.MongoClientFactoryBean; + +import javax.inject.Inject; + +@Configuration +public class MongoConfig { + + @Inject + MongoProperties props; + + public String getMongoURI() { + + Boolean useSrvEndpoint = Boolean.parseBoolean(props.getUseSrvEndpointStr()); + + if (useSrvEndpoint) { + + String srvUriFormat = "mongodb+srv://%s:%s@%s/%s?ssl=%s&retryWrites=%s&w=%s"; + + String srvUri = String.format( + srvUriFormat, + props.getUsername(), + props.getPassword(), + props.getEndpoint(), + props.getAuthDatabase(), + props.getEnableTLS(), + props.getRetryWrites(), + props.getWriteMode()); + + return srvUri; + } + else { + String uriFormat = "mongodb+srv://%s:%s@%s/%s?retryWrites=%s&w=%s"; + + String uri = String.format( + uriFormat, + props.getUsername(), + props.getPassword(), + props.getEndpoint(), + props.getAuthDatabase(), +// props.getEnableTLS(), + props.getRetryWrites(), + props.getWriteMode()); + + return uri; + } + } + + public @Bean MongoClientFactoryBean mongo() { + ConnectionString connectionString = new ConnectionString(this.getMongoURI()); + + MongoClientFactoryBean mongo = new MongoClientFactoryBean(); + + mongo.setConnectionString(connectionString); + + return mongo; + } +} diff --git a/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/util/MongoProperties.java b/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/util/MongoProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..432b5214a556d9ef6c27a576293207f2927cc5ea --- /dev/null +++ b/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/util/MongoProperties.java @@ -0,0 +1,76 @@ +// Copyright © 2021 Amazon Web Services +// Copyright MongoDB, Inc or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.opengroup.osdu.partition.provider.aws.util; + +import java.util.Map; + +import javax.annotation.PostConstruct; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import org.opengroup.osdu.core.aws.ssm.K8sLocalParameterProvider; +import org.opengroup.osdu.core.aws.ssm.K8sParameterNotFoundException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import lombok.Data; + +@Data +@Component +public class MongoProperties { + + @Value("${osdu.mongodb.username}") + private String username; + @Value("${osdu.mongodb.password}") + private String password; + @Value("${spring.data.mongodb.host}") + private String endpoint; + @Value("${osdu.mongodb.authDatabase}") + private String authDatabase; + @Value("${osdu.mongodb.port}") + private String port; + @Value("${osdu.mongodb.retryWrites}") + private String retryWrites; + @Value("${osdu.mongodb.writeMode}") + private String writeMode; + @Value("${osdu.mongodb.useSrvEndpoint}") + private String useSrvEndpointStr; + @Value("${osdu.mongodb.enableTLS}") + private String enableTLS; + + @PostConstruct + private void init() throws K8sParameterNotFoundException, JsonProcessingException { + + K8sLocalParameterProvider provider = new K8sLocalParameterProvider(); + + if (!provider.getLocalMode()) { + Map<String,String> credentials = provider.getCredentialsAsMap("mongodb_credentials"); + + if (credentials != null) { + username = credentials.get("username"); + password = credentials.get("password"); + authDatabase = credentials.get("authDB"); + } + + endpoint = provider.getParameterAsStringOrDefault("mongodb_host", endpoint); + port = provider.getParameterAsStringOrDefault("mongodb_port", port); + + } + } + + + +} diff --git a/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/util/SSMHelper.java b/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/util/SSMHelper.java deleted file mode 100644 index 52ea4560e1ab553a569d9ce1282a0eee9ed52800..0000000000000000000000000000000000000000 --- a/provider/partition-aws/src/main/java/org/opengroup/osdu/partition/provider/aws/util/SSMHelper.java +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright © 2020 Amazon Web Services -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package org.opengroup.osdu.partition.provider.aws.util; - -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement; -import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagementClientBuilder; -import com.amazonaws.services.simplesystemsmanagement.model.*; -import org.opengroup.osdu.core.aws.iam.IAMConfig; -import org.opengroup.osdu.partition.model.Property; -import org.opengroup.osdu.partition.provider.aws.AwsServiceConfig; -import org.springframework.stereotype.Component; - -import javax.inject.Inject; -import java.net.URI; -import java.util.*; -import java.util.stream.Collectors; - -@Component -public final class SSMHelper { - - @Inject - private AwsServiceConfig awsServiceConfig; - - private AWSCredentialsProvider amazonAWSCredentials; - private AWSSimpleSystemsManagement ssmManager; - - public SSMHelper() { - amazonAWSCredentials = IAMConfig.amazonAWSCredentials(); - ssmManager = AWSSimpleSystemsManagementClientBuilder.standard() - .withCredentials(amazonAWSCredentials) - .build(); - } - - // public boolean secretExists(String secretName) { - // GetParameterRequest request = new GetParameterRequest().withName(secretName); - // GetParameterResult result = ssmManager.getParameter(request); - - // return result.getParameter() != null; - // } - - private String getSsmPathForPartitition(String partitionName) { - return URI.create(getTenantPrefix() + '/' + partitionName + '/').normalize().toString(); - } - - private String getSsmPathForPartititions() { - return URI.create(getTenantPrefix() + '/').normalize().toString(); - } - - private String getSsmPathForPartititionSecret(String partitionName, String secretName) { - return URI.create(getSsmPathForPartitition(partitionName) + '/' + secretName).normalize().toString(); - } - - private List<Parameter> getSsmParamsForPartition(String partitionName) { - - String ssmPath = getSsmPathForPartitition(partitionName); - - List<Parameter> params = new ArrayList<Parameter>(); - String nextToken = null; - - do { - - GetParametersByPathRequest request = new GetParametersByPathRequest() - .withPath(ssmPath) - .withRecursive(true) - .withNextToken(nextToken) - .withWithDecryption(true); - - GetParametersByPathResult result = ssmManager.getParametersByPath(request); - nextToken = result.getNextToken(); - - if (result.getParameters().size() > 0) - params.addAll(result.getParameters()); - } - while (nextToken != null); - - return params; - } - - public List<String> getSsmParamsPathsForPartition(String partitionName) { - - List<Parameter> paramsToDelete = getSsmParamsForPartition(partitionName); - - List<String> ssmParamNames = paramsToDelete.stream().map(Parameter::getName).collect(Collectors.toList()); - - return ssmParamNames; - } - - public boolean partitionExists(String partitionName) { - - String ssmPath = getSsmPathForPartitition(partitionName); - String nextToken = null; - - do { - - GetParametersByPathRequest request = new GetParametersByPathRequest() - .withPath(ssmPath) - .withRecursive(true) - .withNextToken(nextToken); - - GetParametersByPathResult result = ssmManager.getParametersByPath(request); - nextToken = result.getNextToken(); - - if (result.getParameters().size() > 0) - return true; - } - while (nextToken != null); - - return false; - } - - public Map<String, Property> getPartitionSecrets(String partitionName) { - - List<Parameter> partitionSsmParameters = getSsmParamsForPartition(partitionName); - - String ssmPath = getSsmPathForPartitition(partitionName); - - Map<String, Property> kvMap = new HashMap<>(); - - for (Parameter parameter : partitionSsmParameters) { - - String shortName = parameter.getName().substring(ssmPath.length()); - - kvMap.put(shortName, Property.builder().value(parameter.getValue()).build()); - } - - return kvMap; - } - - public boolean createOrUpdateSecret(String partitionName, String secretName, Object secretValue) { - - String ssmPath = getSsmPathForPartititionSecret(partitionName, secretName); - - PutParameterRequest request = new PutParameterRequest() - .withName(ssmPath) - .withType(ParameterType.SecureString) - .withOverwrite(true) - .withValue(String.valueOf(secretValue)); - - - PutParameterResult result = ssmManager.putParameter(request); - - //secret creation throws an exception if there's an error so we wont hit here - return true; - - } - - public boolean deletePartitionSecrets(String partitionName) { - - List<String> ssmParamPaths = getSsmParamsPathsForPartition(partitionName); - - int expectedNumOfDeletedParams = ssmParamPaths.size(); - int totalDeletedParams = 0; - - while (ssmParamPaths.size() > 0) { - int subListCount = ssmParamPaths.size(); - if (subListCount > 10) - subListCount = 10; - - List<String> paramsToDelete = ssmParamPaths.subList(0, subListCount); - ssmParamPaths = ssmParamPaths.subList(subListCount, ssmParamPaths.size()); - - DeleteParametersRequest request = new DeleteParametersRequest() - .withNames(paramsToDelete); - - DeleteParametersResult result = ssmManager.deleteParameters(request); - - totalDeletedParams += result.getDeletedParameters().size(); - } - - - - return totalDeletedParams == expectedNumOfDeletedParams; - - } - - public List<String> getPartitions() { - - List<String> partitions = new ArrayList<>(); - Set<String> uniquePartitions = new HashSet<String>(); - String ssmPath = getSsmPathForPartititions(); - - String nextToken = null; - GetParametersByPathResult result=null; - do { - - GetParametersByPathRequest request = new GetParametersByPathRequest() - .withPath(ssmPath) - .withRecursive(true) - .withNextToken(nextToken); - - result = ssmManager.getParametersByPath(request); - for(Parameter p: result.getParameters()) - { - - String dp = (p.getName().substring(ssmPath.length()).split("/")[0]); - - uniquePartitions.add(dp); - } - nextToken = result.getNextToken(); - - - } - while (nextToken != null); - - - partitions.addAll(uniquePartitions); - - - return partitions; - } - - private String getTenantPrefix() { - return awsServiceConfig.ssmPartitionPrefix; - } -} diff --git a/provider/partition-aws/src/main/resources/application.properties b/provider/partition-aws/src/main/resources/application.properties index a5f1e4feed00c8c2111306ef303b0f96c8ca7e75..ce127bc10b7eb81c98eb1f96bed13148c37e0346 100644 --- a/provider/partition-aws/src/main/resources/application.properties +++ b/provider/partition-aws/src/main/resources/application.properties @@ -43,6 +43,21 @@ server.ssl.key-alias=${SSL_KEY_ALIAS:osduonaws} server.ssl.key-password=${SSL_KEY_PASSWORD:} server.ssl.key-store-password=${SSL_KEY_STORE_PASSWORD:} +#MongoDB config +spring.data.mongodb.database=${ENVIRONMENT}_osdu_partitions +osdu.mongodb.authDatabase=${MONGODB_AUTH_DATABASE:admin} +spring.data.mongodb.host=${MONGODB_ENDPOINT:} +osdu.mongodb.port=${MONGODB_PORT:27017} +osdu.mongodb.username=${MONGODB_USERNAME:empty} +osdu.mongodb.password=${MONGODB_PASSWORD:empty} +osdu.mongodb.retryWrites=${MONGODB_RETRY_WRITES:true} +osdu.mongodb.writeMode=${MONGODB_WRITE_MODE:majority} +osdu.mongodb.useSrvEndpoint=${MONGODB_USE_SRV_ENDPOINT:true} +osdu.mongodb.enableTLS=${MONGODB_ENABLE_TLS:false} + +#AWS KMS Config +aws.kms.keyArn=${KEY_ARN:empty} + spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration ## AWS ElastiCache configuration aws.elasticache.cluster.endpoint=${CACHE_CLUSTER_ENDPOINT:null} diff --git a/provider/partition-aws/src/test/java/org/opengroup/osdu/partition/provider/aws/service/PartitionServiceImplTest.java b/provider/partition-aws/src/test/java/org/opengroup/osdu/partition/provider/aws/service/PartitionServiceImplTest.java index 9fb4d644ff98c6f05057a8c38ff2fafb80823f23..5a7e8f50d101c41f697ed3c046a337ffce48c1d0 100644 --- a/provider/partition-aws/src/test/java/org/opengroup/osdu/partition/provider/aws/service/PartitionServiceImplTest.java +++ b/provider/partition-aws/src/test/java/org/opengroup/osdu/partition/provider/aws/service/PartitionServiceImplTest.java @@ -15,64 +15,86 @@ package org.opengroup.osdu.partition.provider.aws.service; +import org.bson.types.Binary; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; -import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.mockito.*; +import org.mockito.junit.MockitoJUnitRunner; import org.opengroup.osdu.core.common.model.http.AppException; import org.opengroup.osdu.partition.model.PartitionInfo; import org.opengroup.osdu.partition.model.Property; -import org.opengroup.osdu.partition.provider.aws.util.SSMHelper; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; +import org.opengroup.osdu.partition.provider.aws.model.Partition; +import org.opengroup.osdu.partition.provider.aws.model.IPartitionRepository; +import org.opengroup.osdu.partition.provider.aws.util.AwsKmsEncryptionClient; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.Executors; +import java.nio.charset.StandardCharsets; +import java.util.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.mockito.Mockito.verify; -@RunWith(PowerMockRunner.class) + +@RunWith(MockitoJUnitRunner.class) public class PartitionServiceImplTest { @Mock - private SSMHelper ssmHelper; + private IPartitionRepository repository; + + @Mock + private AwsKmsEncryptionClient awsKmsEncryptionClient; + + @Captor + private ArgumentCaptor<Partition> partitionArgumentCaptor; @InjectMocks private PartitionServiceImpl partService; - private PartitionInfo partitionInfo = new PartitionInfo(); - - private Map<String, Property> partitionSecretMap = new HashMap<>(); + private String id; + private PartitionInfo partitionInfoDummy = new PartitionInfo(); + private Partition partitionDummy = new Partition(); + private Partition encryptedPartitionDummy = new Partition(); @Before public void setup() { - partitionSecretMap.put("id", Property.builder().value("my-tenant").build()); - partitionSecretMap.put("storageAccount", Property.builder().value("storage-account").build()); - partitionSecretMap.put("complianceRuleSet", Property.builder().value("compliance-rule-set").build()); - partitionInfo.setProperties(partitionSecretMap); + id = "id"; + + Map<String, Property> partitionSecretMap = new HashMap<>(); + partitionSecretMap.put("storageAccount", Property.builder() + .value("storage-account") + .sensitive(true).build()); + partitionSecretMap.put("complianceRuleSet", Property.builder() + .value("compliance-rule-set") + .sensitive(false).build()); + + partitionInfoDummy.setProperties(partitionSecretMap); + + partitionDummy.setId(id); + partitionDummy.setProperties(partitionSecretMap); + + Map<String, Property> encryptedPartitionSecretMap = new HashMap<>(); + encryptedPartitionSecretMap.put("storageAccount", Property.builder() + .value(new Binary("storage-account".getBytes(StandardCharsets.UTF_8))) + .sensitive(true).build()); + encryptedPartitionSecretMap.put("complianceRuleSet", Property.builder() + .value("compliance-rule-set") + .sensitive(false).build()); + + encryptedPartitionDummy.setId(id); + encryptedPartitionDummy.setProperties(encryptedPartitionSecretMap); } @Test public void should_ThrowConflictError_when_createPartition_whenPartitionExists() { - when(ssmHelper.partitionExists(any())).thenReturn(true); + when(repository.findById(any())).thenReturn(Optional.of(partitionDummy)); try { - partService.createPartition(this.partitionInfo.getProperties().get("id").toString(), this.partitionInfo); + partService.createPartition(id, partitionInfoDummy); //we should never hit this code because create partition should end in an error assertTrue("Expected partService.createPartition to throw an exception, but passed", false); } catch (AppException e) { @@ -85,43 +107,75 @@ public class PartitionServiceImplTest { @Test public void should_returnPartitionInfo_when_createPartition_whenPartitionDoesntExist() { - when(ssmHelper.partitionExists(any())).thenReturn(false); - when(ssmHelper.createOrUpdateSecret(any(), any(), any())).thenReturn(true); - when(ssmHelper.getSsmParamsPathsForPartition(any())).thenReturn(new ArrayList<String>(this.partitionInfo.getProperties().keySet())); + when(repository.findById(any())).thenReturn(Optional.empty()); + when(repository.save(any())).thenReturn(partitionDummy); + when(awsKmsEncryptionClient.encrypt(any(), any())).thenReturn("ENCRYPTED".getBytes(StandardCharsets.UTF_8)); + + PartitionInfo partInfo = partService.createPartition(partitionDummy.getId(), partitionInfoDummy); + assertTrue(partInfo.getProperties().size() == 2); - PartitionInfo partInfo = partService.createPartition(this.partitionInfo.getProperties().get("id").toString(), this.partitionInfo); - assertTrue(partInfo.getProperties().size() == 3); - assertTrue(partInfo.getProperties().containsKey("id")); - assertTrue(partInfo.getProperties().containsKey("complianceRuleSet")); - assertTrue(partInfo.getProperties().containsKey("storageAccount")); + for (Map.Entry<String, Property> e : partitionInfoDummy.getProperties().entrySet()) { + assertTrue(partInfo.getProperties().containsKey(e.getKey())); + } } @Test public void should_returnPartition_when_partitionExists() { - String Key1 = "my-tenant-id"; - String Key2 = "my-tenant-groups"; - String Key3 = "my-tenant-complianceRuleSet"; + when(repository.findById(any())).thenReturn(Optional.of(encryptedPartitionDummy)); + when(awsKmsEncryptionClient.decrypt(any(), any())).thenReturn("DECRYPTED"); + + PartitionInfo partitionInfo = partService.getPartition(id); + + assertTrue(partitionInfo.getProperties().containsKey("complianceRuleSet")); + assertTrue(partitionInfo.getProperties().containsKey("storageAccount")); + } + + @Test + public void should_call_awsEncryptionClient_encrypt_when_isSensitive() { + + when(repository.findById(any())).thenReturn(Optional.empty()); + when(repository.save(any())).thenReturn(partitionDummy); + when(awsKmsEncryptionClient.encrypt(any(), any())).thenReturn("ENCRYPTED".getBytes(StandardCharsets.UTF_8)); + + partService.createPartition(partitionDummy.getId(), partitionInfoDummy); + + verify(repository).save(partitionArgumentCaptor.capture()); + Map<String, Property> encryptedProps = partitionArgumentCaptor.getValue().getProperties(); + + for (Map.Entry<String, Property> e : partitionInfoDummy.getProperties().entrySet()) { + if (e.getValue().isSensitive()) { + // just check to see if the sensitive value has been modified + assertTrue(encryptedProps.get(e.getKey()).getValue() != e.getValue().getValue()); + } else { + assertTrue(encryptedProps.get(e.getKey()).getValue() == e.getValue().getValue()); + } + } + } - HashMap<String, Property> propertiesMap = new HashMap<>(); - propertiesMap.put("id", Property.builder().value("my-tenant").build()); - propertiesMap.put(Key1, null); - propertiesMap.put(Key2, null); - propertiesMap.put(Key3, null); + @Test + public void should_call_awsEncryptionClient_decrypt_when_isSensitive() { + when(repository.findById(any())).thenReturn(Optional.of(encryptedPartitionDummy)); + when(awsKmsEncryptionClient.decrypt(any(), any())).thenReturn("DECRYPTED"); - when(ssmHelper.getPartitionSecrets(any())).thenReturn(propertiesMap); + PartitionInfo partitionInfo = partService.getPartition(id); + Map<String, Property> encryptedProps = partitionInfo.getProperties(); - PartitionInfo partitionInfo = this.partService.getPartition(this.partitionInfo.getProperties().get("id").toString()); - assertTrue(partitionInfo.getProperties().containsKey(Key1)); - assertTrue(partitionInfo.getProperties().containsKey(Key2)); - assertTrue(partitionInfo.getProperties().containsKey(Key3)); - assertTrue(partitionInfo.getProperties().containsKey("id")); + for (Map.Entry<String, Property> e : partitionInfoDummy.getProperties().entrySet()) { + if (e.getValue().isSensitive()) { + assertTrue(encryptedProps.get(e.getKey()).getValue().equals("DECRYPTED")); + } else { + assertTrue(encryptedProps.get(e.getKey()).getValue() == e.getValue().getValue()); + } + } + + assertTrue(partitionInfo.getProperties().containsKey("storageAccount")); } @Test public void should_throwNotFoundException_when_partitionDoesntExist() { - when(this.ssmHelper.getPartitionSecrets("my-tenant")).thenReturn(new HashMap<>()); + when(repository.findById(any())).thenReturn(Optional.empty()); try { partService.getPartition("my-tenant"); @@ -137,17 +191,14 @@ public class PartitionServiceImplTest { @Test public void should_returnTrue_when_successfullyDeletingSecretes() { - when(ssmHelper.partitionExists(any())).thenReturn(true); - when(ssmHelper.getSsmParamsPathsForPartition(any())).thenReturn(Arrays.asList("/my-tenant/partition/partitions/dummy-param")); - when(ssmHelper.deletePartitionSecrets(any())).thenReturn(true); + when(repository.findById(any())).thenReturn(Optional.of(partitionDummy)); - assertTrue(this.partService.deletePartition("test-partition")); + assertTrue(partService.deletePartition("test-partition")); } @Test public void should_throwException_when_deletingNonExistentPartition() { - try { this.partService.deletePartition("some-invalid-partition"); //we should never hit this code because delete partition should end in an error @@ -163,4 +214,5 @@ public class PartitionServiceImplTest { this.partService.deletePartition(null); } + } \ No newline at end of file