Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion service/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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'
Expand Down Expand Up @@ -178,3 +179,4 @@ sonar {
bootJar {
exclude("**/TransformJsonToXml*")
}

Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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");
Expand All @@ -56,15 +59,15 @@

@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<GetObjectResponse> s3Object = s3Client.getObject(getObjectRequest)) {
KeyStore customKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
customKeyStore.load(content, trustStorePassword.toCharArray());
customKeyStore.load(s3Object, trustStorePassword.toCharArray());

Check warning on line 70 in service/src/main/java/uk/nhs/adaptors/gp2gp/common/configuration/CustomTrustStore.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 70 without causing a test to fail

removed call to java/security/KeyStore::load (covered by 1 tests VoidMethodCallMutator)
trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(customKeyStore);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,41 @@

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;
Copy link
Contributor Author

@adrianclay adrianclay Jan 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this use the Bean defined as getS3Client inside of StorageConnectorConfiguration.java ? Because if so, then that bean only gets constructed when a truststore is defined. Whereas previously this was being constructed all the time.

It will come from the StorageConnectorFactory which is fine.

}

@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);
}
}

@Override
public InputStream downloadFromStorage(String filename) throws StorageConnectorException {
public ResponseInputStream<GetObjectResponse> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,6 +15,7 @@
@Configuration
@ConfigurationProperties(prefix = "gp2gp.storage")
public class StorageConnectorConfiguration {

private static final String S3_PREFIX = "s3";

private String type;
Expand All @@ -25,11 +25,9 @@
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();

Check warning on line 30 in service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/StorageConnectorConfiguration.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 30 without causing a test to fail

replaced return value with null for getS3Client (no tests cover this line NullReturnValsMutator)
}

return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<StorageConnector> {
Expand All @@ -15,7 +15,7 @@
if (storageConnector == null) {
switch (StorageConnectorOptions.enumOf(configuration.getType())) {
case S3:
storageConnector = new S3StorageConnector(configuration);
storageConnector = new S3StorageConnector(S3Client.builder().build(), configuration);

Check warning on line 18 in service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/StorageConnectorFactory.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 18 without causing a test to fail

removed write to storageConnector (no tests cover this line RemoveFieldWriteMutator)
break;
case AZURE:
storageConnector = new AzureStorageConnector();
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Comment on lines +59 to +61
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally not a fan of messing around with reflection to create tests. Any way to make the s3Client a constructor argument?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it, I'd probably make the AppInitializer construct it's own CustomTrustStore instead of relying on dependency injection. Also moving into AppInitializer the logic to optionally construct the S3Client that is currently inside of StorageConnectorConfiguration.

AppInitializer can instead depend on StorageConnectorConfiguration.


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!");
}
}
Original file line number Diff line number Diff line change
@@ -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));
}

}
Binary file added service/src/test/resources/test.jks
Binary file not shown.
Loading