diff --git a/service/build.gradle b/service/build.gradle index 5d5b5801e4..3f5203956a 100644 --- a/service/build.gradle +++ b/service/build.gradle @@ -50,7 +50,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-logging' // Infrastructure - implementation 'com.amazonaws:aws-java-sdk-s3:1.12.780' + implementation 'software.amazon.awssdk:s3:2.28.29' implementation ('com.azure:azure-storage-blob:12.29.0') implementation 'org.apache.qpid:qpid-jms-client:2.6.1' @@ -72,6 +72,7 @@ dependencies { testImplementation 'org.wiremock:wiremock-standalone:3.9.2' testImplementation 'com.squareup.okhttp3:okhttp:4.12.0' testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0' + testImplementation 'io.findify:s3mock_2.13:0.2.6' pitest 'com.arcmutate:base:1.3.1' pitest 'com.arcmutate:pitest-git-plugin:2.0.0' @@ -178,3 +179,4 @@ sonar { bootJar { exclude("**/TransformJsonToXml*") } + diff --git a/service/src/main/java/uk/nhs/adaptors/gp2gp/common/configuration/CustomTrustStore.java b/service/src/main/java/uk/nhs/adaptors/gp2gp/common/configuration/CustomTrustStore.java index 01e4230185..87fcaa85ee 100644 --- a/service/src/main/java/uk/nhs/adaptors/gp2gp/common/configuration/CustomTrustStore.java +++ b/service/src/main/java/uk/nhs/adaptors/gp2gp/common/configuration/CustomTrustStore.java @@ -1,5 +1,6 @@ package uk.nhs.adaptors.gp2gp.common.configuration; +import java.net.URI; import java.security.KeyStore; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; @@ -11,10 +12,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; - -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3URI; -import com.amazonaws.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.services.s3.S3Uri; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import lombok.NoArgsConstructor; @@ -27,12 +29,13 @@ @NoArgsConstructor public class CustomTrustStore { @Autowired(required = false) - private AmazonS3 s3Client; + private S3Client s3Client; @SneakyThrows public void addToDefault(String trustStorePath, String trustStorePassword) { final X509TrustManager defaultTrustManager = getDefaultTrustManager(); - final X509TrustManager customTrustManager = getCustomDbTrustManager(new AmazonS3URI(trustStorePath), trustStorePassword); + final var s3Uri = s3Client.utilities().parseUri(URI.create(trustStorePath)); + final X509TrustManager customTrustManager = getCustomDbTrustManager(s3Uri, trustStorePassword); X509TrustManager combinedTrustManager = new CombinedTrustManager(customTrustManager, defaultTrustManager); SSLContext sslContext = SSLContext.getInstance("TLS"); @@ -56,15 +59,15 @@ private X509TrustManager getDefaultTrustManager() { @SneakyThrows @SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE") - private X509TrustManager getCustomDbTrustManager(AmazonS3URI s3URI, String trustStorePassword) { + protected X509TrustManager getCustomDbTrustManager(S3Uri s3Uri, String trustStorePassword) { TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init((KeyStore) null); - LOGGER.info("Loading custom KeyStore from '{}'", s3URI.toString()); - try (var s3Object = s3Client.getObject(new GetObjectRequest(s3URI.getBucket(), s3URI.getKey())); - var content = s3Object.getObjectContent()) { + LOGGER.info("Loading custom KeyStore from '{}'", s3Uri.toString()); + final var getObjectRequest = GetObjectRequest.builder().bucket(s3Uri.bucket().orElseThrow()).key(s3Uri.key().orElseThrow()).build(); + try (ResponseInputStream s3Object = s3Client.getObject(getObjectRequest)) { KeyStore customKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - customKeyStore.load(content, trustStorePassword.toCharArray()); + customKeyStore.load(s3Object, trustStorePassword.toCharArray()); trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(customKeyStore); } diff --git a/service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/S3StorageConnector.java b/service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/S3StorageConnector.java index e5f736e4dc..142b300713 100644 --- a/service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/S3StorageConnector.java +++ b/service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/S3StorageConnector.java @@ -2,33 +2,30 @@ import java.io.InputStream; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.S3Object; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; public class S3StorageConnector implements StorageConnector { - private final AmazonS3 s3client; + private final S3Client s3client; private final String bucketName; - protected S3StorageConnector(StorageConnectorConfiguration configuration) { + protected S3StorageConnector(S3Client s3client, StorageConnectorConfiguration configuration) { this.bucketName = configuration.getContainerName(); - this.s3client = AmazonS3ClientBuilder - .standard() - .build(); + this.s3client = s3client; } @Override public void uploadToStorage(InputStream is, long streamLength, String filename) throws StorageConnectorException { try { - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(streamLength); + final var putObjectRequest = PutObjectRequest.builder().bucket(bucketName).key(filename).build(); s3client.putObject( - bucketName, - filename, - is, - metadata + putObjectRequest, + RequestBody.fromInputStream(is, streamLength) ); } catch (Exception exception) { throw new StorageConnectorException("Error occurred uploading to S3 Bucket", exception); @@ -36,10 +33,10 @@ public void uploadToStorage(InputStream is, long streamLength, String filename) } @Override - public InputStream downloadFromStorage(String filename) throws StorageConnectorException { + public ResponseInputStream downloadFromStorage(String filename) throws StorageConnectorException { try { - S3Object s3Object = s3client.getObject(bucketName, filename); - return s3Object.getObjectContent(); + final var request = GetObjectRequest.builder().bucket(bucketName).key(filename).build(); + return s3client.getObject(request); } catch (Exception exception) { throw new StorageConnectorException("Error occurred downloading from S3 Bucket", exception); } diff --git a/service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/StorageConnectorConfiguration.java b/service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/StorageConnectorConfiguration.java index 01e79aea8c..1a5282651b 100644 --- a/service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/StorageConnectorConfiguration.java +++ b/service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/StorageConnectorConfiguration.java @@ -5,8 +5,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import software.amazon.awssdk.services.s3.S3Client; import lombok.Getter; import lombok.Setter; @@ -16,6 +15,7 @@ @Configuration @ConfigurationProperties(prefix = "gp2gp.storage") public class StorageConnectorConfiguration { + private static final String S3_PREFIX = "s3"; private String type; @@ -25,11 +25,9 @@ public class StorageConnectorConfiguration { private String trustStorePassword; @Bean - @SuppressWarnings("unused") - public AmazonS3 getS3Client() { + public S3Client getS3Client() { if (StringUtils.isNotBlank(trustStoreUrl) && trustStoreUrl.startsWith(S3_PREFIX)) { - return AmazonS3ClientBuilder.standard() - .build(); + return S3Client.builder().build(); } return null; diff --git a/service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/StorageConnectorFactory.java b/service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/StorageConnectorFactory.java index 6aee67c615..7f817de7b5 100644 --- a/service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/StorageConnectorFactory.java +++ b/service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/StorageConnectorFactory.java @@ -1,8 +1,8 @@ package uk.nhs.adaptors.gp2gp.common.storage; import org.springframework.beans.factory.FactoryBean; - import lombok.Setter; +import software.amazon.awssdk.services.s3.S3Client; @Setter public class StorageConnectorFactory implements FactoryBean { @@ -15,7 +15,7 @@ public StorageConnector getObject() { if (storageConnector == null) { switch (StorageConnectorOptions.enumOf(configuration.getType())) { case S3: - storageConnector = new S3StorageConnector(configuration); + storageConnector = new S3StorageConnector(S3Client.builder().build(), configuration); break; case AZURE: storageConnector = new AzureStorageConnector(); diff --git a/service/src/test/java/uk/nhs/adaptors/gp2gp/common/configuration/CustomTrustStoreTest.java b/service/src/test/java/uk/nhs/adaptors/gp2gp/common/configuration/CustomTrustStoreTest.java new file mode 100644 index 0000000000..6ac2d74565 --- /dev/null +++ b/service/src/test/java/uk/nhs/adaptors/gp2gp/common/configuration/CustomTrustStoreTest.java @@ -0,0 +1,69 @@ +package uk.nhs.adaptors.gp2gp.common.configuration; + +import io.findify.s3mock.S3Mock; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3Configuration; +import software.amazon.awssdk.services.s3.model.CreateBucketRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import java.io.File; +import java.lang.reflect.Field; +import java.net.URI; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class CustomTrustStoreTest { + + public static final int PORT = 8001; + private static S3Mock s3Mock; + private static S3Client s3Client; + private static final String BUCKET_NAME = "test-bucket"; + private static final String TRUSTSTORE_PATH = "test.jks"; + private static final String TRUSTSTORE_PASSWORD = "password"; + + private final CustomTrustStore customTrustStore = new CustomTrustStore(); + + @BeforeAll + static void setUp() { + s3Mock = new S3Mock.Builder().withPort(PORT).withInMemoryBackend().build(); + s3Mock.start(); + System.out.println("S3Mock started at http://localhost:" + PORT); + + s3Client = S3Client.builder() + .endpointOverride(URI.create("http://localhost:" + PORT)) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create("accessKey", "secretKey"))) + .serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(true).build()) + .region(Region.EU_WEST_2) + .build(); + + s3Client.createBucket(CreateBucketRequest.builder().bucket(BUCKET_NAME).build()); + + File trustStoreFile = new File("src/test/resources/test.jks"); + s3Client.putObject(PutObjectRequest.builder().bucket(BUCKET_NAME).key(TRUSTSTORE_PATH).build(), + software.amazon.awssdk.core.sync.RequestBody.fromFile(trustStoreFile)); + } + + @AfterAll + static void tearDown() { + s3Mock.shutdown(); + } + + @Test + void trustManagerLoadsSuccessfullyTest() throws NoSuchFieldException, IllegalAccessException { + + Field s3ClientField = CustomTrustStore.class.getDeclaredField("s3Client"); + s3ClientField.setAccessible(true); + s3ClientField.set(customTrustStore, s3Client); + + String s3Uri = "s3://" + BUCKET_NAME + "/" + TRUSTSTORE_PATH; + + var trustManager = customTrustStore.getCustomDbTrustManager(s3Client.utilities().parseUri(URI.create(s3Uri)), TRUSTSTORE_PASSWORD); + + assertNotNull(trustManager, "Custom TrustManager wasn't loaded successfully!"); + } +} \ No newline at end of file diff --git a/service/src/test/java/uk/nhs/adaptors/gp2gp/common/storage/S3StorageConnectorTest.java b/service/src/test/java/uk/nhs/adaptors/gp2gp/common/storage/S3StorageConnectorTest.java new file mode 100644 index 0000000000..9637f273e1 --- /dev/null +++ b/service/src/test/java/uk/nhs/adaptors/gp2gp/common/storage/S3StorageConnectorTest.java @@ -0,0 +1,85 @@ +package uk.nhs.adaptors.gp2gp.common.storage; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class S3StorageConnectorTest { + + private static final String FILE_NAME = "test-file.txt"; + private S3StorageConnector s3StorageConnector; + private StorageConnectorConfiguration config; + private static final long STREAM_LENGTH = 100L; + + @Mock + private S3Client mockS3Client; + + @Mock + private InputStream is; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + config = new StorageConnectorConfiguration(); + config.setContainerName("s3Bucket"); + + s3StorageConnector = new S3StorageConnector(mockS3Client, config); + } + + + @Test + void expectExceptionWhenS3ClientCannotDeliverResponse() { + S3StorageConnector storageConnector = new S3StorageConnector(S3Client.builder().region(Region.EU_WEST_2).build(), config); + Exception exception = assertThrows(StorageConnectorException.class, + () -> storageConnector.downloadFromStorage("s3File")); + + assertEquals("Error occurred downloading from S3 Bucket", exception.getMessage()); + } + + @Test + void downloadFromStorageTest() { + var mockResponse = mock(GetObjectResponse.class); + var mockInputStream = new ByteArrayInputStream("dummy-content".getBytes()); + var mockResponseInputStream = new ResponseInputStream<>(mockResponse, mockInputStream); + final var request = GetObjectRequest.builder().bucket(config.getContainerName()).key(FILE_NAME).build(); + + when(mockS3Client.getObject(request)).thenReturn(mockResponseInputStream); + + var result = s3StorageConnector.downloadFromStorage(FILE_NAME); + + assertNotNull(result); + verify(mockS3Client).getObject(request); + } + + @Test + void uploadToStorageTest() { + final var expectedRequest = PutObjectRequest.builder().bucket(config.getContainerName()).key(FILE_NAME).build(); + + s3StorageConnector.uploadToStorage(is, STREAM_LENGTH, FILE_NAME); + + verify(mockS3Client, times(1)).putObject(eq(expectedRequest), any(RequestBody.class)); + } + +} \ No newline at end of file diff --git a/service/src/test/resources/test.jks b/service/src/test/resources/test.jks new file mode 100644 index 0000000000..4435d8acfb Binary files /dev/null and b/service/src/test/resources/test.jks differ