From 853572b40045e4466bb8df3bbd66b9d0d2eaa580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20R=C3=BChle?= Date: Thu, 16 Nov 2023 16:18:40 +0100 Subject: [PATCH 1/6] Update Flare Server Version in Test --- .../client/flare/FlareWebserviceClientImplIT.java | 2 +- .../client/flare/valid-structured-query.json | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplIT.java index f75711b..a61f91a 100644 --- a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplIT.java +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplIT.java @@ -38,7 +38,7 @@ public class FlareWebserviceClientImplIT { .withEnv("LOG_LEVEL", "debug"); @Container - public static GenericContainer flare = new GenericContainer<>(DockerImageName.parse("ghcr.io/medizininformatik-initiative/flare:0.2.3")) + public static GenericContainer flare = new GenericContainer<>(DockerImageName.parse("ghcr.io/medizininformatik-initiative/flare:2.1.0")) .withExposedPorts(8080) .withNetwork(DEFAULT_CONTAINER_NETWORK) .withNetworkAliases("flare") diff --git a/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/valid-structured-query.json b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/valid-structured-query.json index 6d31eeb..80bff13 100644 --- a/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/valid-structured-query.json +++ b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/valid-structured-query.json @@ -1,5 +1,6 @@ { "version": "http://to_be_decided.com/draft-1/schema#", + "display": "", "inclusionCriteria": [ [ { @@ -10,15 +11,21 @@ "display": "Geschlecht" } ], + "context": { + "code": "Patient", + "system": "fdpg.mii.cds", + "version": "1.0.0", + "display": "Patient" + }, "valueFilter": { - "type": "concept", "selectedConcepts": [ { "code": "female", - "system": "http://hl7.org/fhir/administrative-gender", - "display": "Female" + "display": "Female", + "system": "http://hl7.org/fhir/administrative-gender" } - ] + ], + "type": "concept" } } ] From 225f57fab6e355532cffce531220ba87735e5937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20R=C3=BChle?= Date: Thu, 16 Nov 2023 16:20:36 +0100 Subject: [PATCH 2/6] Update Blaze FHIR Server Version in Test --- .../client/flare/FlareWebserviceClientImplIT.java | 2 +- .../feasibility_dsf_process/client/store/StoreClientIT.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplIT.java index a61f91a..4830660 100644 --- a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplIT.java +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplIT.java @@ -31,7 +31,7 @@ public class FlareWebserviceClientImplIT { private static final Network DEFAULT_CONTAINER_NETWORK = Network.newNetwork(); @Container - public static GenericContainer fhirServer = new GenericContainer<>(DockerImageName.parse("samply/blaze:0.22.2")) + public static GenericContainer fhirServer = new GenericContainer<>(DockerImageName.parse("samply/blaze:0.23.0")) .withExposedPorts(8080) .withNetwork(DEFAULT_CONTAINER_NETWORK) .withNetworkAliases("fhir-server") diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/StoreClientIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/StoreClientIT.java index d548b0d..31995f1 100644 --- a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/StoreClientIT.java +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/StoreClientIT.java @@ -52,7 +52,7 @@ public class StoreClientIT { private static final Network DEFAULT_CONTAINER_NETWORK = Network.newNetwork(); @Container - public GenericContainer fhirServer = new GenericContainer<>(DockerImageName.parse("samply/blaze:0.22.2")) + public GenericContainer fhirServer = new GenericContainer<>(DockerImageName.parse("samply/blaze:0.23.0")) .withExposedPorts(8080) .withNetwork(DEFAULT_CONTAINER_NETWORK) .withNetworkAliases("fhir-server") From 5416efd25addaaee7e64a2760321a5389a83fd49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20R=C3=BChle?= Date: Wed, 22 Nov 2023 18:48:35 +0100 Subject: [PATCH 3/6] Use Existing SSL Context in Flare Store Client --- feasibility-dsf-process/pom.xml | 2 +- ...ts.sh => create_certs_for_client_tests.sh} | 8 +- .../flare/FlareWebserviceClientImpl.java | 28 +++---- .../FlareWebserviceClientSpringConfig.java | 16 ++-- .../client/store/StoreClientSpringConfig.java | 73 +----------------- .../client/store/TlsClientFactory.java | 56 +++++++------- .../spring/config/BaseConfig.java | 71 ++++++++++++++++++ .../config}/DefaultTrustStoreUtils.java | 8 +- .../FlareWebserviceClientImplBaseIT.java | 36 +++++++++ ... FlareWebserviceClientImplNonProxyIT.java} | 32 +------- ...viceClientImplRevProxyTlsClientCertIT.java | 74 +++++++++++++++++++ ...lareWebserviceClientImplRevProxyTlsIT.java | 69 +++++++++++++++++ .../flare/FlareWebserviceClientImplTest.java | 73 +++--------------- .../client/store/StoreClientIT.java | 13 ++-- .../client/flare/index.html | 8 ++ .../client/flare/nginx.conf | 25 +++++++ .../flare/reverse_proxy_tls.conf.template | 17 +++++ ...everse_proxy_tls_client_cert.conf.template | 23 ++++++ 18 files changed, 412 insertions(+), 220 deletions(-) rename feasibility-dsf-process/scripts/{create_certs_for_store_client_tests.sh => create_certs_for_client_tests.sh} (89%) rename feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/{client/store => spring/config}/DefaultTrustStoreUtils.java (95%) create mode 100644 feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplBaseIT.java rename feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/{FlareWebserviceClientImplIT.java => FlareWebserviceClientImplNonProxyIT.java} (51%) create mode 100644 feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyTlsClientCertIT.java create mode 100644 feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyTlsIT.java create mode 100644 feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/index.html create mode 100644 feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/nginx.conf create mode 100644 feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_tls.conf.template create mode 100644 feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_tls_client_cert.conf.template diff --git a/feasibility-dsf-process/pom.xml b/feasibility-dsf-process/pom.xml index 5dda620..3cf4aba 100755 --- a/feasibility-dsf-process/pom.xml +++ b/feasibility-dsf-process/pom.xml @@ -159,7 +159,7 @@ exec - ${basedir}/scripts/create_certs_for_store_client_tests.sh + ${basedir}/scripts/create_certs_for_client_tests.sh diff --git a/feasibility-dsf-process/scripts/create_certs_for_store_client_tests.sh b/feasibility-dsf-process/scripts/create_certs_for_client_tests.sh similarity index 89% rename from feasibility-dsf-process/scripts/create_certs_for_store_client_tests.sh rename to feasibility-dsf-process/scripts/create_certs_for_client_tests.sh index e271d14..99dfbd6 100755 --- a/feasibility-dsf-process/scripts/create_certs_for_store_client_tests.sh +++ b/feasibility-dsf-process/scripts/create_certs_for_client_tests.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash BASE_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" -TARGET_DIR=$(readlink -f "${BASE_DIR}/../src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/store/certs") +TARGET_DIR=$(readlink -f "${BASE_DIR}/../src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/certs") mkdir -p "${TARGET_DIR}" @@ -19,12 +19,14 @@ openssl pkcs12 -export -out ${TARGET_DIR}/ca.p12 \ # Issue server certificate using said self signed CA openssl req -nodes -sha256 -new -newkey rsa:2048 -keyout ${TARGET_DIR}/server_cert_key.pem \ -out ${TARGET_DIR}/server_cert_csr.pem \ - -subj "/C=DE/ST=Berlin/L=Berlin/O=Bar/CN=localhost" + -subj "/C=DE/ST=Berlin/L=Berlin/O=Bar/CN=localhost" \ + -addext "subjectAltName = DNS:localhost, DNS:proxy" openssl x509 -req -days 7 -sha256 -in ${TARGET_DIR}/server_cert_csr.pem \ -CA ${TARGET_DIR}/ca.pem \ -CAkey ${TARGET_DIR}/ca_key.pem \ -CAcreateserial \ + -copy_extensions copyall \ -out ${TARGET_DIR}/server_cert.pem # Server cert chain @@ -56,4 +58,4 @@ rm -f ${TARGET_DIR}/server_cert_csr.pem rm -f ${TARGET_DIR}/server_cert.pem rm -f ${TARGET_DIR}/client_cert_csr.pem rm -f ${TARGET_DIR}/client_cert_key.pem -rm -f ${TARGET_DIR}/client_cert.pem \ No newline at end of file +rm -f ${TARGET_DIR}/client_cert.pem diff --git a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImpl.java b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImpl.java index 4464317..5646011 100644 --- a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImpl.java +++ b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImpl.java @@ -1,19 +1,22 @@ package de.medizininformatik_initiative.feasibility_dsf_process.client.flare; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.client.BasicResponseHandler; +import org.apache.http.message.BasicHeader; + import java.io.IOException; import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import static java.net.http.HttpRequest.BodyPublishers.ofByteArray; -import static java.net.http.HttpResponse.BodyHandlers.ofString; +import static ca.uhn.fhir.rest.api.Constants.HEADER_CONTENT_TYPE; /** * Client for communicating with a Flare instance. */ public class FlareWebserviceClientImpl implements FlareWebserviceClient { - private final HttpClient httpClient; + private final org.apache.http.client.HttpClient httpClient; private final URI flareBaseUrl; public FlareWebserviceClientImpl(HttpClient httpClient, URI flareBaseUrl) { @@ -23,13 +26,12 @@ public FlareWebserviceClientImpl(HttpClient httpClient, URI flareBaseUrl) { @Override public int requestFeasibility(byte[] structuredQuery) throws IOException, InterruptedException { - var req = HttpRequest.newBuilder() - .POST(ofByteArray(structuredQuery)) - .setHeader("Content-Type", "application/sq+json") - .uri(flareBaseUrl.resolve("/query/execute")) - .build(); - - var res = httpClient.send(req, ofString()); - return Integer.parseInt(res.body()); + var req = new HttpPost(flareBaseUrl.resolve("/query/execute")); + req.setEntity(new ByteArrayEntity(structuredQuery)); + req.setHeader(new BasicHeader(HEADER_CONTENT_TYPE, "application/sq+json")); + + var response = httpClient.execute(req, new BasicResponseHandler()); + + return Integer.parseInt(response); } } diff --git a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java index 4383061..637effa 100644 --- a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java +++ b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java @@ -1,14 +1,20 @@ package de.medizininformatik_initiative.feasibility_dsf_process.client.flare; +import de.medizininformatik_initiative.feasibility_dsf_process.client.store.TlsClientFactory; +import de.medizininformatik_initiative.feasibility_dsf_process.spring.config.BaseConfig; +import org.apache.http.client.HttpClient; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import java.net.URI; -import java.net.http.HttpClient; -import java.time.Duration; + +import javax.net.ssl.SSLContext; @Configuration +@Import(BaseConfig.class) public class FlareWebserviceClientSpringConfig { @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.flare.base_url:}") @@ -23,9 +29,9 @@ public FlareWebserviceClient flareWebserviceClient(HttpClient httpClient) { } @Bean - public HttpClient flareHttpClient() { - return HttpClient.newBuilder() - .connectTimeout(Duration.ofMillis(connectTimeout)) + public HttpClient flareHttpClient(@Qualifier("base-client") SSLContext sslContext) { + return new TlsClientFactory(null, sslContext) + .getNativeHttpClientBuilder() .build(); } } diff --git a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/StoreClientSpringConfig.java b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/StoreClientSpringConfig.java index 8e43ffa..9eb9ea1 100644 --- a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/StoreClientSpringConfig.java +++ b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/StoreClientSpringConfig.java @@ -6,20 +6,17 @@ import ca.uhn.fhir.rest.client.impl.RestfulClientFactory; import ca.uhn.fhir.rest.client.interceptor.BasicAuthInterceptor; import ca.uhn.fhir.rest.client.interceptor.BearerTokenAuthInterceptor; -import org.apache.http.ssl.SSLContexts; +import de.medizininformatik_initiative.feasibility_dsf_process.spring.config.BaseConfig; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.lang.Nullable; +import org.springframework.context.annotation.Import; import javax.net.ssl.SSLContext; -import java.io.FileInputStream; -import java.io.IOException; -import java.security.*; -import java.security.cert.CertificateException; @Configuration +@Import(BaseConfig.class) public class StoreClientSpringConfig { @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.host:#{null}}") private String proxyHost; @@ -51,18 +48,6 @@ public class StoreClientSpringConfig { @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.timeout.socket:20000}") private Integer socketTimeout; - @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.trust_store_path:#{null}}") - private String trustStorePath; - - @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.trust_store_password:#{null}}") - private String trustStorePassword; - - @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.key_store_path:#{null}}") - private String keyStorePath; - - @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.key_store_password:#{null}}") - private String keyStorePassword; - @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.base_url}") private String storeBaseUrl; @@ -108,57 +93,7 @@ FhirContext fhirContext() { @Bean @Qualifier("store-client") RestfulClientFactory clientFactory(@Qualifier("store-client") FhirContext fhirContext, - @Qualifier("store-client") SSLContext sslContext) { + @Qualifier("base-client") SSLContext sslContext) { return new TlsClientFactory(fhirContext, sslContext); } - - @Bean - @Qualifier("store-client-trust") - KeyStore loadTrustStore() throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException { - if (trustStorePath == null || trustStorePath.isBlank()) { - return DefaultTrustStoreUtils.loadDefaultTrustStore(); - } - - var trustStoreInputStream = new FileInputStream(trustStorePath); - - var trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); - trustStore.load(trustStoreInputStream, (trustStorePassword == null) ? null : trustStorePassword.toCharArray()); - trustStoreInputStream.close(); - - return trustStore; - } - - - @Bean - @Qualifier("store-client-key") - @Nullable - KeyStore loadKeyStore() throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { - if (keyStorePath == null) { - return null; - } - - var keyStoreInputStream = new FileInputStream(keyStorePath); - - var keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load(keyStoreInputStream, (keyStorePassword == null) ? null : keyStorePassword.toCharArray()); - keyStoreInputStream.close(); - - return keyStore; - } - - @Bean - @Qualifier("store-client") - SSLContext createSslContext(@Qualifier("store-client-trust") KeyStore trustStore, - @Nullable @Qualifier("store-client-key") KeyStore keyStore) - throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, UnrecoverableKeyException { - var sslContextBuilder = SSLContexts.custom() - .loadTrustMaterial(trustStore, null); - - if (keyStore != null) { - sslContextBuilder.loadKeyMaterial(keyStore, (keyStorePassword == null) ? null : - keyStorePassword.toCharArray()); - } - - return sslContextBuilder.build(); - } } diff --git a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/TlsClientFactory.java b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/TlsClientFactory.java index 8f28ad7..af89b8b 100644 --- a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/TlsClientFactory.java +++ b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/TlsClientFactory.java @@ -26,13 +26,14 @@ import org.apache.http.impl.client.ProxyAuthenticationStrategy; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import javax.net.ssl.SSLContext; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; +import javax.net.ssl.SSLContext; + // TODO: doc @Slf4j @RequiredArgsConstructor @@ -74,39 +75,42 @@ public synchronized IHttpClient getHttpClient(StringBuilder theUrl, Map socketFactoryRegistry = RegistryBuilder.create() - .register("http", PlainConnectionSocketFactory.getSocketFactory()) - .register("https", new SSLConnectionSocketFactory(sslContext)).build(); + return myHttpClient; + } - PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager( - socketFactoryRegistry, null, null, null, 5000, - TimeUnit.MILLISECONDS); + public HttpClientBuilder getNativeHttpClientBuilder() { + SSLContext sslContext = getSslContext(); - connectionManager.setMaxTotal(getPoolMaxTotal()); - connectionManager.setDefaultMaxPerRoute(getPoolMaxPerRoute()); + Registry socketFactoryRegistry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.getSocketFactory()) + .register("https", new SSLConnectionSocketFactory(sslContext)).build(); - RequestConfig defaultRequestConfig = RequestConfig.custom().setSocketTimeout(getSocketTimeout()) - .setConnectTimeout(getConnectTimeout()).setConnectionRequestTimeout(getConnectionRequestTimeout()) - .setProxy(myProxy).build(); + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager( + socketFactoryRegistry, null, null, null, 5000, + TimeUnit.MILLISECONDS); - HttpClientBuilder builder = HttpClients.custom().setConnectionManager(connectionManager) - .setSSLContext(sslContext).setDefaultRequestConfig(defaultRequestConfig).disableCookieManagement(); + connectionManager.setMaxTotal(getPoolMaxTotal()); + connectionManager.setDefaultMaxPerRoute(getPoolMaxPerRoute()); - if (myProxy != null && StringUtils.isNotBlank(getProxyUsername()) - && StringUtils.isNotBlank(getProxyPassword())) { - CredentialsProvider credsProvider = new BasicCredentialsProvider(); - credsProvider.setCredentials(new AuthScope(myProxy.getHostName(), myProxy.getPort()), - new UsernamePasswordCredentials(getProxyUsername(), getProxyPassword())); - builder.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy()); - builder.setDefaultCredentialsProvider(credsProvider); - } + RequestConfig defaultRequestConfig = RequestConfig.custom().setSocketTimeout(getSocketTimeout()) + .setConnectTimeout(getConnectTimeout()).setConnectionRequestTimeout(getConnectionRequestTimeout()) + .setProxy(myProxy).build(); - myHttpClient = builder.build(); - } + HttpClientBuilder builder = HttpClients.custom().setConnectionManager(connectionManager) + .setSSLContext(sslContext).setDefaultRequestConfig(defaultRequestConfig).disableCookieManagement(); - return myHttpClient; + if (myProxy != null && StringUtils.isNotBlank(getProxyUsername()) + && StringUtils.isNotBlank(getProxyPassword())) { + CredentialsProvider credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials(new AuthScope(myProxy.getHostName(), myProxy.getPort()), + new UsernamePasswordCredentials(getProxyUsername(), getProxyPassword())); + builder.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy()); + builder.setDefaultCredentialsProvider(credsProvider); + } + return builder; } protected SSLContext getSslContext() { diff --git a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/spring/config/BaseConfig.java b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/spring/config/BaseConfig.java index 9745f74..5cf7105 100644 --- a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/spring/config/BaseConfig.java +++ b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/spring/config/BaseConfig.java @@ -1,17 +1,88 @@ package de.medizininformatik_initiative.feasibility_dsf_process.spring.config; import ca.uhn.fhir.context.FhirContext; +import org.apache.http.ssl.SSLContexts; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.lang.Nullable; + +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +import javax.net.ssl.SSLContext; @Configuration public class BaseConfig { + @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.trust_store_path:#{null}}") private String trustStorePath; + + @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.trust_store_password:#{null}}") private String trustStorePassword; + + @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.key_store_path:#{null}}") private String keyStorePath; + + @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.key_store_password:#{null}}") private String keyStorePassword; + @Bean @Qualifier("base") FhirContext fhirContext() { return FhirContext.forR4(); } + @Bean + @Qualifier("base-client-trust") + KeyStore loadTrustStore() throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException { + if (trustStorePath == null || trustStorePath.isBlank()) { + return DefaultTrustStoreUtils.loadDefaultTrustStore(); + } + + var trustStoreInputStream = new FileInputStream(trustStorePath); + + var trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(trustStoreInputStream, (trustStorePassword == null) ? null : trustStorePassword.toCharArray()); + trustStoreInputStream.close(); + + return trustStore; + } + + @Bean + @Qualifier("base-client-key") + @Nullable + KeyStore loadKeyStore() throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { + if (keyStorePath == null) { + return null; + } + + var keyStoreInputStream = new FileInputStream(keyStorePath); + + var keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(keyStoreInputStream, (keyStorePassword == null) ? null : keyStorePassword.toCharArray()); + keyStoreInputStream.close(); + + return keyStore; + } + + @Bean + @Qualifier("base-client") + SSLContext createSslContext(@Qualifier("base-client-trust") KeyStore trustStore, + @Nullable @Qualifier("base-client-key") KeyStore keyStore) + throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, UnrecoverableKeyException { + var sslContextBuilder = SSLContexts.custom() + .loadTrustMaterial(trustStore, null); + + if (keyStore != null) { + sslContextBuilder.loadKeyMaterial(keyStore, (keyStorePassword == null) ? null : + keyStorePassword.toCharArray()); + } + + return sslContextBuilder.build(); + } + } diff --git a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/DefaultTrustStoreUtils.java b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/spring/config/DefaultTrustStoreUtils.java similarity index 95% rename from feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/DefaultTrustStoreUtils.java rename to feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/spring/config/DefaultTrustStoreUtils.java index a933266..0e5b7fd 100644 --- a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/DefaultTrustStoreUtils.java +++ b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/spring/config/DefaultTrustStoreUtils.java @@ -1,4 +1,4 @@ -package de.medizininformatik_initiative.feasibility_dsf_process.client.store; +package de.medizininformatik_initiative.feasibility_dsf_process.spring.config; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -12,11 +12,11 @@ import java.security.cert.CertificateException; // TODO: doc -final class DefaultTrustStoreUtils { +public final class DefaultTrustStoreUtils { private DefaultTrustStoreUtils() { } - static KeyStore loadDefaultTrustStore() { + public static KeyStore loadDefaultTrustStore() { Path location = null; String type = null; String password = null; @@ -67,4 +67,4 @@ static KeyStore loadDefaultTrustStore() { return trustStore; } -} \ No newline at end of file +} diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplBaseIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplBaseIT.java new file mode 100644 index 0000000..024c8d7 --- /dev/null +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplBaseIT.java @@ -0,0 +1,36 @@ +package de.medizininformatik_initiative.feasibility_dsf_process.client.flare; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.utility.DockerImageName; + +import java.net.URL; +import java.time.Duration; +import java.util.Map; + +public abstract class FlareWebserviceClientImplBaseIT { + + protected static final Network DEFAULT_CONTAINER_NETWORK = Network.newNetwork(); + @Container public static GenericContainer fhirServer = new GenericContainer<>( + DockerImageName.parse("samply/blaze:0.23.0")) + .withExposedPorts(8080) + .withNetwork(DEFAULT_CONTAINER_NETWORK) + .withNetworkAliases("fhir-server") + .withEnv("LOG_LEVEL", "debug"); + @Container public static GenericContainer flare = new GenericContainer<>( + DockerImageName.parse("ghcr.io/medizininformatik-initiative/flare:2.1.0")) + .withExposedPorts(8080) + .withNetwork(DEFAULT_CONTAINER_NETWORK) + .withNetworkAliases("flare") + .withEnv(Map.of( + "FLARE_FHIR_SERVER", "http://fhir-server:8080/fhir/" + )) + .withStartupTimeout(Duration.ofMinutes(5)) + .dependsOn(fhirServer); + + protected static URL getResource(final String name) { + return FlareWebserviceClientImplBaseIT.class.getResource(name); + } + +} diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplNonProxyIT.java similarity index 51% rename from feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplIT.java rename to feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplNonProxyIT.java index 4830660..64d1446 100644 --- a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplIT.java +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplNonProxyIT.java @@ -6,15 +6,9 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.Network; -import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; import java.io.IOException; -import java.time.Duration; -import java.util.Map; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -23,30 +17,10 @@ @Tag("flare") @SpringBootTest(classes = FlareWebserviceClientSpringConfig.class) @Testcontainers -public class FlareWebserviceClientImplIT { +public class FlareWebserviceClientImplNonProxyIT extends FlareWebserviceClientImplBaseIT { @Autowired - private FlareWebserviceClient flareClient; - - private static final Network DEFAULT_CONTAINER_NETWORK = Network.newNetwork(); - - @Container - public static GenericContainer fhirServer = new GenericContainer<>(DockerImageName.parse("samply/blaze:0.23.0")) - .withExposedPorts(8080) - .withNetwork(DEFAULT_CONTAINER_NETWORK) - .withNetworkAliases("fhir-server") - .withEnv("LOG_LEVEL", "debug"); - - @Container - public static GenericContainer flare = new GenericContainer<>(DockerImageName.parse("ghcr.io/medizininformatik-initiative/flare:2.1.0")) - .withExposedPorts(8080) - .withNetwork(DEFAULT_CONTAINER_NETWORK) - .withNetworkAliases("flare") - .withEnv(Map.of( - "FLARE_FHIR_SERVER", "http://fhir-server:8080/fhir/" - )) - .withStartupTimeout(Duration.ofMinutes(5)) - .dependsOn(fhirServer); + protected FlareWebserviceClient flareClient; @DynamicPropertySource static void dynamicProperties(DynamicPropertyRegistry registry) { @@ -58,7 +32,7 @@ static void dynamicProperties(DynamicPropertyRegistry registry) { } @Test - public void testRequestToFlareWithEmptyFhirServer() throws IOException { + public void sendQuery() throws IOException { var rawStructuredQuery = this.getClass().getResource("valid-structured-query.json") .openStream().readAllBytes(); diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyTlsClientCertIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyTlsClientCertIT.java new file mode 100644 index 0000000..c628a25 --- /dev/null +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyTlsClientCertIT.java @@ -0,0 +1,74 @@ +package de.medizininformatik_initiative.feasibility_dsf_process.client.flare; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.testcontainers.containers.BindMode.READ_ONLY; + +@Tag("client") +@Tag("flare") +@SpringBootTest(classes = FlareWebserviceClientSpringConfig.class) +@Testcontainers +public class FlareWebserviceClientImplRevProxyTlsClientCertIT extends FlareWebserviceClientImplBaseIT { + + @Autowired + protected FlareWebserviceClient flareClient; + + private static URL nginxConf = getResource("nginx.conf"); + private static URL nginxTestProxyConfTemplate = getResource("reverse_proxy_tls.conf.template"); + private static URL indexFile = getResource("index.html"); + private static URL serverCertChain = getResource("../certs/server_cert_chain.pem"); + private static URL serverCertKey = getResource("../certs/server_cert_key.pem"); + private static URL trustStoreFile = getResource("../certs/ca.p12"); + private static URL keyStoreFile = getResource("../certs/client_key_store.p12"); + + @Container + public static GenericContainer proxy = new GenericContainer<>( + DockerImageName.parse("nginx:1.25.1")) + .withExposedPorts(8443) + .withFileSystemBind(nginxConf.getPath(), "/etc/nginx/nginx.conf", READ_ONLY) + .withFileSystemBind(indexFile.getPath(), "/usr/share/nginx/html/index.html", READ_ONLY) + .withFileSystemBind(nginxTestProxyConfTemplate.getPath(), + "/etc/nginx/templates/default.conf.template", + READ_ONLY) + .withFileSystemBind(serverCertChain.getPath(), "/etc/nginx/certs/server_cert.pem", READ_ONLY) + .withFileSystemBind(serverCertKey.getPath(), "/etc/nginx/certs/server_cert_key.pem", READ_ONLY) + .withNetwork(DEFAULT_CONTAINER_NETWORK) + .dependsOn(flare); + + @DynamicPropertySource + static void dynamicProperties(DynamicPropertyRegistry registry) { + var proxyHost = proxy.getHost(); + var proxyPort = proxy.getFirstMappedPort(); + + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.flare.base_url", + () -> String.format("https://%s:%s/", proxyHost, proxyPort)); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.trust_store_path", + () -> trustStoreFile.getPath()); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.trust_store_password", + () -> "changeit"); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.key_store_path", + () -> keyStoreFile.getPath()); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.key_store_password", + () -> "changeit"); + } + + @Test + void sendQuery() throws Exception { + var rawStructuredQuery = this.getClass().getResource("valid-structured-query.json").openStream().readAllBytes(); + var feasibility = assertDoesNotThrow(() -> flareClient.requestFeasibility(rawStructuredQuery)); + assertEquals(0, feasibility); + } +} diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyTlsIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyTlsIT.java new file mode 100644 index 0000000..5f57059 --- /dev/null +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyTlsIT.java @@ -0,0 +1,69 @@ +package de.medizininformatik_initiative.feasibility_dsf_process.client.flare; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.testcontainers.containers.BindMode.READ_ONLY; + +@Tag("client") +@Tag("flare") +@SpringBootTest(classes = FlareWebserviceClientSpringConfig.class) +@Testcontainers +public class FlareWebserviceClientImplRevProxyTlsIT extends FlareWebserviceClientImplBaseIT { + + @Autowired + protected FlareWebserviceClient flareClient; + + private static URL nginxConf = getResource("nginx.conf"); + private static URL nginxTestProxyConfTemplate = getResource("reverse_proxy_tls.conf.template"); + private static URL indexFile = getResource("index.html"); + private static URL serverCertChain = getResource("../certs/server_cert_chain.pem"); + private static URL serverCertKey = getResource("../certs/server_cert_key.pem"); + private static URL trustStoreFile = getResource("../certs/ca.p12"); + + @Container + public static GenericContainer proxy = new GenericContainer<>( + DockerImageName.parse("nginx:1.25.1")) + .withExposedPorts(8443) + .withFileSystemBind(nginxConf.getPath(), "/etc/nginx/nginx.conf", READ_ONLY) + .withFileSystemBind(indexFile.getPath(), "/usr/share/nginx/html/index.html", READ_ONLY) + .withFileSystemBind(nginxTestProxyConfTemplate.getPath(), + "/etc/nginx/templates/default.conf.template", + READ_ONLY) + .withFileSystemBind(serverCertChain.getPath(), "/etc/nginx/certs/server_cert.pem", READ_ONLY) + .withFileSystemBind(serverCertKey.getPath(), "/etc/nginx/certs/server_cert_key.pem", READ_ONLY) + .withNetwork(DEFAULT_CONTAINER_NETWORK) + .dependsOn(flare); + + @DynamicPropertySource + static void dynamicProperties(DynamicPropertyRegistry registry) { + var proxyHost = proxy.getHost(); + var proxyPort = proxy.getFirstMappedPort(); + + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.flare.base_url", + () -> String.format("https://%s:%s/", proxyHost, proxyPort)); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.trust_store_path", + () -> trustStoreFile.getPath()); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.trust_store_password", + () -> "changeit"); + } + + @Test + void sendQuery() throws Exception { + var rawStructuredQuery = this.getClass().getResource("valid-structured-query.json").openStream().readAllBytes(); + var feasibility = assertDoesNotThrow(() -> flareClient.requestFeasibility(rawStructuredQuery)); + assertEquals(0, feasibility); + } +} diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplTest.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplTest.java index 7d44c62..bbe32f4 100644 --- a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplTest.java +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplTest.java @@ -1,5 +1,8 @@ package de.medizininformatik_initiative.feasibility_dsf_process.client.flare; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.BasicResponseHandler; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -8,25 +11,17 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import java.net.http.HttpClient; -import java.net.http.HttpHeaders; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.Optional; - -import javax.net.ssl.SSLSession; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class FlareWebserviceClientImplTest { - private HttpClient httpClient; + private org.apache.http.client.HttpClient httpClient; private FlareWebserviceClient flareWebserviceClient; @BeforeEach @@ -37,7 +32,7 @@ public void setUp() throws URISyntaxException { @Test public void testRequestFeasibility_FailsOnCommunicationError() throws IOException, InterruptedException { - when(httpClient.send(any(HttpRequest.class), eq(HttpResponse.BodyHandlers.ofString()))) + when(httpClient.execute(any(HttpPost.class), any(BasicResponseHandler.class))) .thenThrow(IOException.class); var structuredQuery = "foo".getBytes(); @@ -46,8 +41,8 @@ public void testRequestFeasibility_FailsOnCommunicationError() throws IOExceptio @Test public void testRequestFeasibility_FailsOnWrongBodyContent() throws IOException, InterruptedException { - var response = new StringHttpResponse("{\"invalid\": true}"); - when(httpClient.send(any(HttpRequest.class), eq(HttpResponse.BodyHandlers.ofString()))) + var response = "{\"invalid\": true}"; + when(httpClient.execute(any(HttpPost.class), any(BasicResponseHandler.class))) .thenReturn(response); var structuredQuery = "foo".getBytes(); @@ -56,8 +51,8 @@ public void testRequestFeasibility_FailsOnWrongBodyContent() throws IOException, @Test public void testRequestFeasibility() throws IOException, InterruptedException { - var response = new StringHttpResponse("15"); - when(httpClient.send(any(HttpRequest.class), eq(HttpResponse.BodyHandlers.ofString()))) + var response = "15"; + when(httpClient.execute(any(HttpPost.class), any(BasicResponseHandler.class))) .thenReturn(response); var structuredQuery = "foo".getBytes(); @@ -65,54 +60,4 @@ public void testRequestFeasibility() throws IOException, InterruptedException { assertEquals(15, feasibility); } - - - private class StringHttpResponse implements HttpResponse { - - private final String body; - - public StringHttpResponse(String body) { - this.body = body; - } - - @Override - public int statusCode() { - return 200; - } - - @Override - public HttpRequest request() { - return null; - } - - @Override - public Optional> previousResponse() { - return Optional.empty(); - } - - @Override - public HttpHeaders headers() { - return null; - } - - @Override - public String body() { - return body; - } - - @Override - public Optional sslSession() { - return Optional.empty(); - } - - @Override - public URI uri() { - return null; - } - - @Override - public HttpClient.Version version() { - return null; - } - } } diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/StoreClientIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/StoreClientIT.java index 31995f1..bca6a64 100644 --- a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/StoreClientIT.java +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/store/StoreClientIT.java @@ -4,6 +4,7 @@ import de.medizininformatik_initiative.feasibility_dsf_process.client.store.StoreClientConfiguration.ConnectionConfiguration; import de.medizininformatik_initiative.feasibility_dsf_process.client.store.StoreClientConfiguration.ProxyConfiguration; import de.medizininformatik_initiative.feasibility_dsf_process.client.store.StoreClientConfiguration.StoreAuthenticationConfiguration; +import de.medizininformatik_initiative.feasibility_dsf_process.spring.config.DefaultTrustStoreUtils; import lombok.NonNull; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; @@ -189,16 +190,16 @@ public void testRequestToReverseProxyWithSelfSignedCertificate() throws IOExcept @Test public void testRequestToReverseProxyWithClientCert() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException { - // Make sure to run `create_certs_for_store_client_tests.sh` from the `scripts` directory first. This will be + // Make sure to run `create_certs_for_client_tests.sh` from the `scripts` directory first. This will be // automatically triggered by maven when trying to run integration tests as it is coupled with a phase called // `pre-integration-test`. var nginxConf = getResource("nginx.conf"); var nginxTestProxyConfTemplate = getResource("reverse_proxy_client_cert.conf.template"); var staticFhirMetadata = getResource("fhir_metadata.json"); var indexFile = getResource("index.html"); - var trustedCerts = getResource("./certs/ca.pem"); - var serverCertChain = getResource("./certs/server_cert_chain.pem"); - var serverCertKey = getResource("./certs/server_cert_key.pem"); + var trustedCerts = getResource("../certs/ca.pem"); + var serverCertChain = getResource("../certs/server_cert_chain.pem"); + var serverCertKey = getResource("../certs/server_cert_key.pem"); NginxContainer nginx = new NginxContainer<>("nginx:1.25.1") .withExposedPorts(80) @@ -212,12 +213,12 @@ public void testRequestToReverseProxyWithClientCert() throws KeyStoreException, .withNetwork(DEFAULT_CONTAINER_NETWORK); nginx.start(); - var serverTrustStoreStream = getResourceAsStream("./certs/ca.p12"); + var serverTrustStoreStream = getResourceAsStream("../certs/ca.p12"); var trustStore = KeyStore.getInstance("PKCS12"); trustStore.load(serverTrustStoreStream, "changeit".toCharArray()); serverTrustStoreStream.close(); - var clientCertStream = getResourceAsStream("./certs/client_key_store.p12"); + var clientCertStream = getResourceAsStream("../certs/client_key_store.p12"); var keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(clientCertStream, "changeit".toCharArray()); clientCertStream.close(); diff --git a/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/index.html b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/index.html new file mode 100644 index 0000000..07bee1c --- /dev/null +++ b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/index.html @@ -0,0 +1,8 @@ + + + Test-Proxy + + + Test-Proxy + + \ No newline at end of file diff --git a/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/nginx.conf b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/nginx.conf new file mode 100644 index 0000000..b2e5fe2 --- /dev/null +++ b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/nginx.conf @@ -0,0 +1,25 @@ +user nginx; +worker_processes 1; + +error_log /var/log/nginx/error.log debug; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /var/log/nginx/access.log main; + + sendfile on; + keepalive_timeout 65; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_tls.conf.template b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_tls.conf.template new file mode 100644 index 0000000..be16007 --- /dev/null +++ b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_tls.conf.template @@ -0,0 +1,17 @@ +server { + listen 8443 ssl; + listen [::]:8443 ssl; + + ssl_certificate /etc/nginx/certs/server_cert.pem; + ssl_certificate_key /etc/nginx/certs/server_cert_key.pem; + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + add_header Strict-Transport-Security "max-age=63072000" always; + + location /query/execute { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_http_version 1.1; + proxy_read_timeout 43200s; + proxy_pass http://flare:8080; + } +} diff --git a/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_tls_client_cert.conf.template b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_tls_client_cert.conf.template new file mode 100644 index 0000000..d50435b --- /dev/null +++ b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_tls_client_cert.conf.template @@ -0,0 +1,23 @@ +server { + listen 8443 ssl; + listen [::]:8443 ssl; + + ssl_certificate /etc/nginx/certs/server_cert.pem; + ssl_certificate_key /etc/nginx/certs/server_cert_key.pem; + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + add_header Strict-Transport-Security "max-age=63072000" always; + ssl_client_certificate /etc/nginx/certificates/clientCA.pem; + ssl_verify_client on; + ssl_verify_depth 2; + + location /query/execute { + if ($ssl_client_verify != SUCCESS) { + return 403; + } + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_http_version 1.1; + proxy_read_timeout 43200s; + proxy_pass http://flare:8080; + } +} From 4a6e313986ba3c7cf6629f58668db6bc31b492b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20R=C3=BChle?= Date: Wed, 22 Nov 2023 18:59:10 +0100 Subject: [PATCH 4/6] Allow Basic Authentication for Flare Store Client --- .../FlareWebserviceClientSpringConfig.java | 26 ++++++- ...bserviceClientImplRevProxyBasicAuthIT.java | 68 ++++++++++++++++ ...rviceClientImplRevProxyTlsBasicAuthIT.java | 77 +++++++++++++++++++ .../client/flare/reverse_proxy.htpasswd | 1 + .../reverse_proxy_basic_auth.conf.template | 17 ++++ ...reverse_proxy_tls_basic_auth.conf.template | 20 +++++ 6 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyBasicAuthIT.java create mode 100644 feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyTlsBasicAuthIT.java create mode 100644 feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy.htpasswd create mode 100644 feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_basic_auth.conf.template create mode 100644 feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_tls_basic_auth.conf.template diff --git a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java index 637effa..7c142ca 100644 --- a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java +++ b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java @@ -2,7 +2,12 @@ import de.medizininformatik_initiative.feasibility_dsf_process.client.store.TlsClientFactory; import de.medizininformatik_initiative.feasibility_dsf_process.spring.config.BaseConfig; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.HttpClient; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.HttpClientBuilder; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -13,6 +18,8 @@ import javax.net.ssl.SSLContext; +import static com.google.common.base.Strings.isNullOrEmpty; + @Configuration @Import(BaseConfig.class) public class FlareWebserviceClientSpringConfig { @@ -23,6 +30,12 @@ public class FlareWebserviceClientSpringConfig { @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.flare.timeout.connect:2000}") private int connectTimeout; + @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.auth.basic.username:#{null}}") + private String basicAuthUsername; + + @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.auth.basic.password:#{null}}") + private String basicAuthPassword; + @Bean public FlareWebserviceClient flareWebserviceClient(HttpClient httpClient) { return new FlareWebserviceClientImpl(httpClient, URI.create(flareBaseUrl)); @@ -30,8 +43,15 @@ public FlareWebserviceClient flareWebserviceClient(HttpClient httpClient) { @Bean public HttpClient flareHttpClient(@Qualifier("base-client") SSLContext sslContext) { - return new TlsClientFactory(null, sslContext) - .getNativeHttpClientBuilder() - .build(); + HttpClientBuilder builder = new TlsClientFactory(null, sslContext).getNativeHttpClientBuilder(); + + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + + if (!isNullOrEmpty(basicAuthUsername) && !isNullOrEmpty(basicAuthPassword)) { + URI flareUri = URI.create(flareBaseUrl); + credentialsProvider.setCredentials(new AuthScope(new HttpHost(flareUri.getHost(), flareUri.getPort())), + new UsernamePasswordCredentials(basicAuthUsername, basicAuthPassword)); + } + return builder.setDefaultCredentialsProvider(credentialsProvider).build(); } } diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyBasicAuthIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyBasicAuthIT.java new file mode 100644 index 0000000..c00262b --- /dev/null +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyBasicAuthIT.java @@ -0,0 +1,68 @@ +package de.medizininformatik_initiative.feasibility_dsf_process.client.flare; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.testcontainers.containers.BindMode.READ_ONLY; + +@Tag("client") +@Tag("flare") +@SpringBootTest(classes = FlareWebserviceClientSpringConfig.class) +@Testcontainers +public class FlareWebserviceClientImplRevProxyBasicAuthIT extends FlareWebserviceClientImplBaseIT { + + @Autowired + protected FlareWebserviceClient flareClient; + + private static URL nginxConf = getResource("nginx.conf"); + private static URL nginxTestProxyConfTemplate = getResource("reverse_proxy_basic_auth.conf.template"); + private static URL indexFile = getResource("index.html"); + private static URL passwordFile = getResource("reverse_proxy.htpasswd"); + private static String basicAuthUsername = "test"; + private static String basicAuthPassword = "foo"; + + @Container + public static GenericContainer proxy = new GenericContainer<>( + DockerImageName.parse("nginx:1.25.1")) + .withExposedPorts(8080) + .withFileSystemBind(nginxConf.getPath(), "/etc/nginx/nginx.conf", READ_ONLY) + .withFileSystemBind(indexFile.getPath(), "/usr/share/nginx/html/index.html", READ_ONLY) + .withFileSystemBind(nginxTestProxyConfTemplate.getPath(), + "/etc/nginx/templates/default.conf.template", + READ_ONLY) + .withFileSystemBind(passwordFile.getPath(), "/etc/auth/.htpasswd", READ_ONLY) + .withNetwork(DEFAULT_CONTAINER_NETWORK) + .dependsOn(flare); + + @DynamicPropertySource + static void dynamicProperties(DynamicPropertyRegistry registry) { + var proxyHost = proxy.getHost(); + var proxyPort = proxy.getFirstMappedPort(); + + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.flare.base_url", + () -> String.format("http://%s:%s/", proxyHost, proxyPort)); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.auth.basic.username", + () -> basicAuthUsername); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.auth.basic.password", + () -> basicAuthPassword); + } + + @Test + void sendQuery() throws Exception { + var rawStructuredQuery = this.getClass().getResource("valid-structured-query.json").openStream().readAllBytes(); + var feasibility = assertDoesNotThrow(() -> flareClient.requestFeasibility(rawStructuredQuery)); + assertEquals(0, feasibility); + } +} diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyTlsBasicAuthIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyTlsBasicAuthIT.java new file mode 100644 index 0000000..12b7ba4 --- /dev/null +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyTlsBasicAuthIT.java @@ -0,0 +1,77 @@ +package de.medizininformatik_initiative.feasibility_dsf_process.client.flare; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.testcontainers.containers.BindMode.READ_ONLY; + +@Tag("client") +@Tag("flare") +@SpringBootTest(classes = FlareWebserviceClientSpringConfig.class) +@Testcontainers +public class FlareWebserviceClientImplRevProxyTlsBasicAuthIT extends FlareWebserviceClientImplBaseIT { + + @Autowired + protected FlareWebserviceClient flareClient; + + private static URL nginxConf = getResource("nginx.conf"); + private static URL nginxTestProxyConfTemplate = getResource("reverse_proxy_tls.conf.template"); + private static URL indexFile = getResource("index.html"); + private static URL serverCertChain = getResource("../certs/server_cert_chain.pem"); + private static URL serverCertKey = getResource("../certs/server_cert_key.pem"); + private static URL trustStoreFile = getResource("../certs/ca.p12"); + private static URL passwordFile = getResource("reverse_proxy.htpasswd"); + private static String basicAuthUsername = "test"; + private static String basicAuthPassword = "foo"; + + @Container + public static GenericContainer proxy = new GenericContainer<>( + DockerImageName.parse("nginx:1.25.1")) + .withExposedPorts(8443) + .withFileSystemBind(nginxConf.getPath(), "/etc/nginx/nginx.conf", READ_ONLY) + .withFileSystemBind(indexFile.getPath(), "/usr/share/nginx/html/index.html", READ_ONLY) + .withFileSystemBind(nginxTestProxyConfTemplate.getPath(), + "/etc/nginx/templates/default.conf.template", + READ_ONLY) + .withFileSystemBind(serverCertChain.getPath(), "/etc/nginx/certs/server_cert.pem", READ_ONLY) + .withFileSystemBind(serverCertKey.getPath(), "/etc/nginx/certs/server_cert_key.pem", READ_ONLY) + .withFileSystemBind(passwordFile.getPath(), "/etc/auth/.htpasswd", READ_ONLY) + .withNetwork(DEFAULT_CONTAINER_NETWORK) + .dependsOn(flare); + + @DynamicPropertySource + static void dynamicProperties(DynamicPropertyRegistry registry) { + var proxyHost = proxy.getHost(); + var proxyPort = proxy.getFirstMappedPort(); + + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.flare.base_url", + () -> String.format("https://%s:%s/", proxyHost, proxyPort)); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.trust_store_path", + () -> trustStoreFile.getPath()); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.trust_store_password", + () -> "changeit"); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.auth.basic.username", + () -> basicAuthUsername); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.auth.basic.password", + () -> basicAuthPassword); + } + + @Test + void sendQuery() throws Exception { + var rawStructuredQuery = this.getClass().getResource("valid-structured-query.json").openStream().readAllBytes(); + var feasibility = assertDoesNotThrow(() -> flareClient.requestFeasibility(rawStructuredQuery)); + assertEquals(0, feasibility); + } +} diff --git a/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy.htpasswd b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy.htpasswd new file mode 100644 index 0000000..0e38cbd --- /dev/null +++ b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy.htpasswd @@ -0,0 +1 @@ +test:$2y$10$i6wzZb/8uwpaex4fp66cKeqprGQfzsJcac4EKHuwT98d58K2E3Q5a diff --git a/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_basic_auth.conf.template b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_basic_auth.conf.template new file mode 100644 index 0000000..16af4da --- /dev/null +++ b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_basic_auth.conf.template @@ -0,0 +1,17 @@ +server { + listen 8080; + listen [::]:8080; + + location / { + root /usr/share/nginx/html; + index index.html; + } + + location /query/execute { + auth_basic "Test Area"; + auth_basic_user_file /etc/auth/.htpasswd; + + proxy_pass http://flare:8080; + proxy_read_timeout 43200s; + } +} diff --git a/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_tls_basic_auth.conf.template b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_tls_basic_auth.conf.template new file mode 100644 index 0000000..9889594 --- /dev/null +++ b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_tls_basic_auth.conf.template @@ -0,0 +1,20 @@ +server { + listen 8443 ssl; + listen [::]:8443 ssl; + + ssl_certificate /etc/nginx/certs/server_cert.pem; + ssl_certificate_key /etc/nginx/certs/server_cert_key.pem; + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + add_header Strict-Transport-Security "max-age=63072000" always; + + location /query/execute { + auth_basic "Test Area"; + auth_basic_user_file /etc/auth/.htpasswd; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_http_version 1.1; + proxy_read_timeout 43200s; + proxy_pass http://flare:8080; + } +} From 986e2d55e46c9574370f696dd30eb4cafd24f2a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20R=C3=BChle?= Date: Wed, 22 Nov 2023 19:04:29 +0100 Subject: [PATCH 5/6] Allow Forward Proxy for Flare Client --- .../FlareWebserviceClientSpringConfig.java | 22 +++++ ...bserviceClientImplFwdProxyBasicAuthIT.java | 64 +++++++++++++ ...entImplFwdProxyBasicAuthRevProxyTlsIT.java | 89 +++++++++++++++++++ .../FlareWebserviceClientImplFwdProxyIT.java | 58 ++++++++++++ ...rviceClientImplFwdRevProxyBasicAuthIT.java | 85 ++++++++++++++++++ .../client/flare/forward_proxy.conf | 2 + .../client/flare/forward_proxy.conf.template | 10 +++ .../client/flare/forward_proxy.htpasswd | 1 + .../flare/forward_proxy_basic_auth.conf | 9 ++ 9 files changed, 340 insertions(+) create mode 100644 feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdProxyBasicAuthIT.java create mode 100644 feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdProxyBasicAuthRevProxyTlsIT.java create mode 100644 feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdProxyIT.java create mode 100644 feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdRevProxyBasicAuthIT.java create mode 100644 feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/forward_proxy.conf create mode 100644 feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/forward_proxy.conf.template create mode 100644 feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/forward_proxy.htpasswd create mode 100644 feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/forward_proxy_basic_auth.conf diff --git a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java index 7c142ca..ef625f5 100644 --- a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java +++ b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java @@ -8,6 +8,7 @@ import org.apache.http.client.HttpClient; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.ProxyAuthenticationStrategy; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -30,6 +31,18 @@ public class FlareWebserviceClientSpringConfig { @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.flare.timeout.connect:2000}") private int connectTimeout; + @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.host:#{null}}") + private String proxyHost; + + @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.port:}") + private Integer proxyPort; + + @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.username:#{null}}") + private String proxyUsername; + + @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.password:#{null}}") + private String proxyPassword; + @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.auth.basic.username:#{null}}") private String basicAuthUsername; @@ -47,6 +60,15 @@ public HttpClient flareHttpClient(@Qualifier("base-client") SSLContext sslContex BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + if (!isNullOrEmpty(proxyHost) && proxyPort != null) { + HttpHost proxy = new HttpHost(proxyHost, proxyPort); + builder.setProxy(proxy); + if (!isNullOrEmpty(proxyUsername) && !isNullOrEmpty(proxyPassword)) { + builder.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy()); + credentialsProvider.setCredentials(new AuthScope(proxy), + new UsernamePasswordCredentials(proxyUsername, proxyPassword)); + } + } if (!isNullOrEmpty(basicAuthUsername) && !isNullOrEmpty(basicAuthPassword)) { URI flareUri = URI.create(flareBaseUrl); credentialsProvider.setCredentials(new AuthScope(new HttpHost(flareUri.getHost(), flareUri.getPort())), diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdProxyBasicAuthIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdProxyBasicAuthIT.java new file mode 100644 index 0000000..a5140df --- /dev/null +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdProxyBasicAuthIT.java @@ -0,0 +1,64 @@ +package de.medizininformatik_initiative.feasibility_dsf_process.client.flare; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.testcontainers.containers.BindMode.READ_ONLY; + +@Tag("client") +@Tag("flare") +@SpringBootTest(classes = FlareWebserviceClientSpringConfig.class) +@Testcontainers +public class FlareWebserviceClientImplFwdProxyBasicAuthIT extends FlareWebserviceClientImplBaseIT { + + @Autowired + protected FlareWebserviceClient flareClient; + + private static URL squidProxyConf = getResource("forward_proxy_basic_auth.conf"); + private static URL passwordFile = getResource("forward_proxy.htpasswd"); + + @Container + public static GenericContainer forwardProxy = new GenericContainer<>( + DockerImageName.parse("ubuntu/squid:6.1-23.10_edge")) + .withExposedPorts(8080) + .withFileSystemBind(squidProxyConf.getPath(), "/etc/squid/squid.conf", READ_ONLY) + .withFileSystemBind(passwordFile.getPath(), "/etc/squid/passwd", READ_ONLY) + .withNetwork(DEFAULT_CONTAINER_NETWORK) + .dependsOn(flare); + + @DynamicPropertySource + static void dynamicProperties(DynamicPropertyRegistry registry) { + var proxyHost = forwardProxy.getHost(); + var proxyPort = forwardProxy.getFirstMappedPort(); + + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.flare.base_url", + () -> "http://flare:8080/"); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.host", + () -> proxyHost); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.port", + () -> proxyPort); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.username", + () -> "test"); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.password", + () -> "bar"); + } + + @Test + void sendQuery() throws Exception { + var rawStructuredQuery = this.getClass().getResource("valid-structured-query.json").openStream().readAllBytes(); + var feasibility = assertDoesNotThrow(() -> flareClient.requestFeasibility(rawStructuredQuery)); + assertEquals(0, feasibility); + } +} diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdProxyBasicAuthRevProxyTlsIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdProxyBasicAuthRevProxyTlsIT.java new file mode 100644 index 0000000..73fdb62 --- /dev/null +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdProxyBasicAuthRevProxyTlsIT.java @@ -0,0 +1,89 @@ +package de.medizininformatik_initiative.feasibility_dsf_process.client.flare; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.testcontainers.containers.BindMode.READ_ONLY; + +@Tag("client") +@Tag("flare") +@SpringBootTest(classes = FlareWebserviceClientSpringConfig.class) +@Testcontainers +public class FlareWebserviceClientImplFwdProxyBasicAuthRevProxyTlsIT extends FlareWebserviceClientImplBaseIT { + + @Autowired + protected FlareWebserviceClient flareClient; + + private static URL squidProxyConf = getResource("forward_proxy_basic_auth.conf"); + private static URL passwordFile = getResource("forward_proxy.htpasswd"); + private static URL nginxConf = getResource("nginx.conf"); + private static URL nginxTestProxyConfTemplate = getResource("reverse_proxy_tls.conf.template"); + private static URL indexFile = getResource("index.html"); + private static URL serverCertChain = getResource("../certs/server_cert_chain.pem"); + private static URL serverCertKey = getResource("../certs/server_cert_key.pem"); + private static URL trustStoreFile = getResource("../certs/ca.p12"); + + @Container + public static GenericContainer proxy = new GenericContainer<>( + DockerImageName.parse("nginx:1.25.1")) + .withExposedPorts(8443) + .withFileSystemBind(nginxConf.getPath(), "/etc/nginx/nginx.conf", READ_ONLY) + .withFileSystemBind(indexFile.getPath(), "/usr/share/nginx/html/index.html", READ_ONLY) + .withFileSystemBind(nginxTestProxyConfTemplate.getPath(), + "/etc/nginx/templates/default.conf.template", + READ_ONLY) + .withFileSystemBind(serverCertChain.getPath(), "/etc/nginx/certs/server_cert.pem", READ_ONLY) + .withFileSystemBind(serverCertKey.getPath(), "/etc/nginx/certs/server_cert_key.pem", READ_ONLY) + .withNetwork(DEFAULT_CONTAINER_NETWORK) + .withNetworkAliases("proxy") + .dependsOn(flare); + + @Container + public static GenericContainer forwardProxy = new GenericContainer<>( + DockerImageName.parse("ubuntu/squid:6.1-23.10_edge")) + .withExposedPorts(8080) + .withFileSystemBind(squidProxyConf.getPath(), "/etc/squid/squid.conf", READ_ONLY) + .withFileSystemBind(passwordFile.getPath(), "/etc/squid/passwd", READ_ONLY) + .withNetwork(DEFAULT_CONTAINER_NETWORK) + .dependsOn(proxy); + + @DynamicPropertySource + static void dynamicProperties(DynamicPropertyRegistry registry) { + var proxyHost = forwardProxy.getHost(); + var proxyPort = forwardProxy.getFirstMappedPort(); + + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.flare.base_url", + () -> "https://proxy:8443/"); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.trust_store_path", + () -> trustStoreFile.getPath()); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.trust_store_password", + () -> "changeit"); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.host", + () -> proxyHost); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.port", + () -> proxyPort); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.username", + () -> "test"); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.password", + () -> "bar"); + } + + @Test + void sendQuery() throws Exception { + var rawStructuredQuery = this.getClass().getResource("valid-structured-query.json").openStream().readAllBytes(); + var feasibility = assertDoesNotThrow(() -> flareClient.requestFeasibility(rawStructuredQuery)); + assertEquals(0, feasibility); + } +} diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdProxyIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdProxyIT.java new file mode 100644 index 0000000..2d8f828 --- /dev/null +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdProxyIT.java @@ -0,0 +1,58 @@ +package de.medizininformatik_initiative.feasibility_dsf_process.client.flare; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.testcontainers.containers.BindMode.READ_ONLY; + +@Tag("client") +@Tag("flare") +@SpringBootTest(classes = FlareWebserviceClientSpringConfig.class) +@Testcontainers +public class FlareWebserviceClientImplFwdProxyIT extends FlareWebserviceClientImplBaseIT { + + @Autowired + protected FlareWebserviceClient flareClient; + + private static URL squidProxyConf = getResource("forward_proxy.conf"); + + @Container + public static GenericContainer forwardProxy = new GenericContainer<>( + DockerImageName.parse("ubuntu/squid:6.1-23.10_edge")) + .withExposedPorts(8080) + .withFileSystemBind(squidProxyConf.getPath(), "/etc/squid/squid.conf", READ_ONLY) + .withNetwork(DEFAULT_CONTAINER_NETWORK) + .dependsOn(flare); + + @DynamicPropertySource + static void dynamicProperties(DynamicPropertyRegistry registry) { + var proxyHost = forwardProxy.getHost(); + var proxyPort = forwardProxy.getFirstMappedPort(); + + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.flare.base_url", + () -> "http://flare:8080/"); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.host", + () -> proxyHost); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.port", + () -> proxyPort); + } + + @Test + void sendQuery() throws Exception { + var rawStructuredQuery = this.getClass().getResource("valid-structured-query.json").openStream().readAllBytes(); + var feasibility = assertDoesNotThrow(() -> flareClient.requestFeasibility(rawStructuredQuery)); + assertEquals(0, feasibility); + } +} diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdRevProxyBasicAuthIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdRevProxyBasicAuthIT.java new file mode 100644 index 0000000..fe88bfa --- /dev/null +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdRevProxyBasicAuthIT.java @@ -0,0 +1,85 @@ +package de.medizininformatik_initiative.feasibility_dsf_process.client.flare; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.testcontainers.containers.BindMode.READ_ONLY; + +@Tag("client") +@Tag("flare") +@SpringBootTest(classes = FlareWebserviceClientSpringConfig.class) +@Testcontainers +public class FlareWebserviceClientImplFwdRevProxyBasicAuthIT extends FlareWebserviceClientImplBaseIT { + + @Autowired + protected FlareWebserviceClient flareClient; + + private static URL nginxConf = getResource("nginx.conf"); + private static URL nginxTestProxyConfTemplate = getResource("reverse_proxy_basic_auth.conf.template"); + private static URL indexFile = getResource("index.html"); + private static URL reverseProxyPasswordFile = getResource("reverse_proxy.htpasswd"); + private static URL squidProxyConf = getResource("forward_proxy_basic_auth.conf"); + private static URL forwardProxyPasswordFile = getResource("forward_proxy.htpasswd"); + + @Container + public static GenericContainer proxy = new GenericContainer<>( + DockerImageName.parse("nginx:1.25.1")) + .withExposedPorts(8080) + .withFileSystemBind(nginxConf.getPath(), "/etc/nginx/nginx.conf", READ_ONLY) + .withFileSystemBind(indexFile.getPath(), "/usr/share/nginx/html/index.html", READ_ONLY) + .withFileSystemBind(nginxTestProxyConfTemplate.getPath(), + "/etc/nginx/templates/default.conf.template", + READ_ONLY) + .withFileSystemBind(reverseProxyPasswordFile.getPath(), "/etc/auth/.htpasswd", READ_ONLY) + .withNetwork(DEFAULT_CONTAINER_NETWORK) + .withNetworkAliases("proxy") + .dependsOn(flare); + @Container + public static GenericContainer forwardProxy = new GenericContainer<>( + DockerImageName.parse("ubuntu/squid:6.1-23.10_edge")) + .withExposedPorts(8080) + .withFileSystemBind(squidProxyConf.getPath(), "/etc/squid/squid.conf", READ_ONLY) + .withFileSystemBind(forwardProxyPasswordFile.getPath(), "/etc/squid/passwd", READ_ONLY) + .withNetwork(DEFAULT_CONTAINER_NETWORK) + .dependsOn(proxy); + + @DynamicPropertySource + static void dynamicProperties(DynamicPropertyRegistry registry) { + var proxyHost = forwardProxy.getHost(); + var proxyPort = forwardProxy.getFirstMappedPort(); + + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.flare.base_url", + () -> "http://proxy:8080/"); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.host", + () -> proxyHost); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.port", + () -> proxyPort); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.username", + () -> "test"); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.password", + () -> "bar"); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.auth.basic.username", + () -> "test"); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.auth.basic.password", + () -> "foo"); + } + + @Test + void sendQuery() throws Exception { + var rawStructuredQuery = this.getClass().getResource("valid-structured-query.json").openStream().readAllBytes(); + var feasibility = assertDoesNotThrow(() -> flareClient.requestFeasibility(rawStructuredQuery)); + assertEquals(0, feasibility); + } +} diff --git a/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/forward_proxy.conf b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/forward_proxy.conf new file mode 100644 index 0000000..6839ffc --- /dev/null +++ b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/forward_proxy.conf @@ -0,0 +1,2 @@ +http_port 8080 +http_access allow all diff --git a/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/forward_proxy.conf.template b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/forward_proxy.conf.template new file mode 100644 index 0000000..6c9ca92 --- /dev/null +++ b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/forward_proxy.conf.template @@ -0,0 +1,10 @@ +server { + listen 8080; + listen [::]:8080; + + location / { + # Using docker default resolver - this is intended to be used within an integration test running docker anyway. + resolver 127.0.0.11; + proxy_pass http://$http_host$uri$is_args$args; + } +} diff --git a/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/forward_proxy.htpasswd b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/forward_proxy.htpasswd new file mode 100644 index 0000000..ba294cf --- /dev/null +++ b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/forward_proxy.htpasswd @@ -0,0 +1 @@ +test:$apr1$4ihrtZR3$7fCQXazNsjIaSbt6CuWi/1 diff --git a/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/forward_proxy_basic_auth.conf b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/forward_proxy_basic_auth.conf new file mode 100644 index 0000000..42e40e4 --- /dev/null +++ b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/forward_proxy_basic_auth.conf @@ -0,0 +1,9 @@ +http_port 8080 +auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/passwd +auth_param basic children 1 startup=1 +auth_param basic casesensitive off +auth_param basic utf8 on +auth_param basic realm Test Forward Proxy Basic Authentication +acl auth_users proxy_auth REQUIRED +http_access allow auth_users +http_access deny all From 08c1ab6a207dda5b6747a5bd652f29ad44790e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20R=C3=BChle?= Date: Wed, 22 Nov 2023 19:06:34 +0100 Subject: [PATCH 6/6] Allow Bearer Token Authentication for Flare Store Client --- .../FlareWebserviceClientSpringConfig.java | 55 +++++++++++++ ...oxyBasicAuthRevProxyBearerTokenAuthIT.java | 82 +++++++++++++++++++ ...ceClientImplRevProxyBearerTokenAuthIT.java | 65 +++++++++++++++ ...erse_proxy_bearer_token_auth.conf.template | 19 +++++ 4 files changed, 221 insertions(+) create mode 100644 feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdProxyBasicAuthRevProxyBearerTokenAuthIT.java create mode 100644 feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyBearerTokenAuthIT.java create mode 100644 feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_bearer_token_auth.conf.template diff --git a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java index ef625f5..222a121 100644 --- a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java +++ b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java @@ -3,22 +3,35 @@ import de.medizininformatik_initiative.feasibility_dsf_process.client.store.TlsClientFactory; import de.medizininformatik_initiative.feasibility_dsf_process.spring.config.BaseConfig; import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.conn.ClientConnectionManager; import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.ProxyAuthenticationStrategy; +import org.apache.http.message.BasicHeader; +import org.apache.http.params.HttpParams; +import org.apache.http.protocol.HttpContext; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import java.io.IOException; import java.net.URI; import javax.net.ssl.SSLContext; +import static ca.uhn.fhir.rest.api.Constants.HEADER_AUTHORIZATION; +import static ca.uhn.fhir.rest.api.Constants.HEADER_AUTHORIZATION_VALPREFIX_BEARER; import static com.google.common.base.Strings.isNullOrEmpty; @Configuration @@ -49,6 +62,9 @@ public class FlareWebserviceClientSpringConfig { @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.auth.basic.password:#{null}}") private String basicAuthPassword; + @Value("${de.medizininformatik_initiative.feasibility_dsf_process.client.store.auth.bearer.token:#{null}}") + private String bearerAuthToken; + @Bean public FlareWebserviceClient flareWebserviceClient(HttpClient httpClient) { return new FlareWebserviceClientImpl(httpClient, URI.create(flareBaseUrl)); @@ -73,7 +89,46 @@ public HttpClient flareHttpClient(@Qualifier("base-client") SSLContext sslContex URI flareUri = URI.create(flareBaseUrl); credentialsProvider.setCredentials(new AuthScope(new HttpHost(flareUri.getHost(), flareUri.getPort())), new UsernamePasswordCredentials(basicAuthUsername, basicAuthPassword)); + } else if (!isNullOrEmpty(bearerAuthToken)) { + return new BearerHttpClient(builder.setDefaultCredentialsProvider(credentialsProvider).build()); } return builder.setDefaultCredentialsProvider(credentialsProvider).build(); } + + private final class BearerHttpClient extends CloseableHttpClient { + private CloseableHttpClient client; + + public BearerHttpClient(CloseableHttpClient client) { + this.client = client; + } + + @Override + public HttpParams getParams() { + return client.getParams(); + } + + @Override + public ClientConnectionManager getConnectionManager() { + return client.getConnectionManager(); + } + + @Override + public void close() throws IOException { + client.close(); + } + + @Override + protected CloseableHttpResponse doExecute(HttpHost target, HttpRequest request, HttpContext context) + throws IOException, ClientProtocolException { + return client.execute(target, request, context); + } + + @Override + public T execute(HttpUriRequest request, ResponseHandler responseHandler) + throws IOException, ClientProtocolException { + request.setHeader(new BasicHeader(HEADER_AUTHORIZATION, + HEADER_AUTHORIZATION_VALPREFIX_BEARER + "1234")); + return super.execute(request, responseHandler); + } + } } diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdProxyBasicAuthRevProxyBearerTokenAuthIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdProxyBasicAuthRevProxyBearerTokenAuthIT.java new file mode 100644 index 0000000..3e89e22 --- /dev/null +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplFwdProxyBasicAuthRevProxyBearerTokenAuthIT.java @@ -0,0 +1,82 @@ +package de.medizininformatik_initiative.feasibility_dsf_process.client.flare; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.testcontainers.containers.BindMode.READ_ONLY; + +@Tag("client") +@Tag("flare") +@SpringBootTest(classes = FlareWebserviceClientSpringConfig.class) +@Testcontainers +public class FlareWebserviceClientImplFwdProxyBasicAuthRevProxyBearerTokenAuthIT extends FlareWebserviceClientImplBaseIT { + + @Autowired + protected FlareWebserviceClient flareClient; + + private static URL nginxConf = getResource("nginx.conf"); + private static URL nginxTestProxyConfTemplate = getResource("reverse_proxy_bearer_token_auth.conf.template"); + private static URL indexFile = getResource("index.html"); + private static URL squidProxyConf = getResource("forward_proxy_basic_auth.conf"); + private static URL forwardProxyPasswordFile = getResource("forward_proxy.htpasswd"); + private static String bearerToken = "1234"; + + @Container + public static GenericContainer proxy = new GenericContainer<>( + DockerImageName.parse("nginx:1.25.1")) + .withExposedPorts(8080) + .withFileSystemBind(nginxConf.getPath(), "/etc/nginx/nginx.conf", READ_ONLY) + .withFileSystemBind(indexFile.getPath(), "/usr/share/nginx/html/index.html", READ_ONLY) + .withFileSystemBind(nginxTestProxyConfTemplate.getPath(), + "/etc/nginx/templates/default.conf.template", + READ_ONLY) + .withNetwork(DEFAULT_CONTAINER_NETWORK) + .withNetworkAliases("proxy") + .dependsOn(flare); + @Container + public static GenericContainer forwardProxy = new GenericContainer<>( + DockerImageName.parse("ubuntu/squid:6.1-23.10_edge")) + .withExposedPorts(8080) + .withFileSystemBind(squidProxyConf.getPath(), "/etc/squid/squid.conf", READ_ONLY) + .withFileSystemBind(forwardProxyPasswordFile.getPath(), "/etc/squid/passwd", READ_ONLY) + .withNetwork(DEFAULT_CONTAINER_NETWORK) + .dependsOn(proxy); + + @DynamicPropertySource + static void dynamicProperties(DynamicPropertyRegistry registry) { + var proxyHost = forwardProxy.getHost(); + var proxyPort = forwardProxy.getFirstMappedPort(); + + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.flare.base_url", + () -> "http://proxy:8080/"); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.host", + () -> proxyHost); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.port", + () -> proxyPort); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.username", + () -> "test"); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.proxy.password", + () -> "bar"); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.auth.bearer.token", + () -> bearerToken); + } + + @Test + void sendQuery() throws Exception { + var rawStructuredQuery = this.getClass().getResource("valid-structured-query.json").openStream().readAllBytes(); + var feasibility = assertDoesNotThrow(() -> flareClient.requestFeasibility(rawStructuredQuery)); + assertEquals(0, feasibility); + } +} diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyBearerTokenAuthIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyBearerTokenAuthIT.java new file mode 100644 index 0000000..9646393 --- /dev/null +++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplRevProxyBearerTokenAuthIT.java @@ -0,0 +1,65 @@ +package de.medizininformatik_initiative.feasibility_dsf_process.client.flare; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.testcontainers.containers.BindMode.READ_ONLY; + +@Tag("client") +@Tag("flare") +@SpringBootTest(classes = FlareWebserviceClientSpringConfig.class) +@Testcontainers +public class FlareWebserviceClientImplRevProxyBearerTokenAuthIT extends FlareWebserviceClientImplBaseIT { + + @Autowired + protected FlareWebserviceClient flareClient; + + private static URL nginxConf = getResource("nginx.conf"); + private static URL nginxTestProxyConfTemplate = getResource("reverse_proxy_bearer_token_auth.conf.template"); + private static URL indexFile = getResource("index.html"); + private static URL passwordFile = getResource("reverse_proxy.htpasswd"); + private static String bearerToken = "1234"; + + @Container + public static GenericContainer proxy = new GenericContainer<>( + DockerImageName.parse("nginx:1.25.1")) + .withExposedPorts(8080) + .withFileSystemBind(nginxConf.getPath(), "/etc/nginx/nginx.conf", READ_ONLY) + .withFileSystemBind(indexFile.getPath(), "/usr/share/nginx/html/index.html", READ_ONLY) + .withFileSystemBind(nginxTestProxyConfTemplate.getPath(), + "/etc/nginx/templates/default.conf.template", + READ_ONLY) + .withFileSystemBind(passwordFile.getPath(), "/etc/auth/.htpasswd", READ_ONLY) + .withNetwork(DEFAULT_CONTAINER_NETWORK) + .dependsOn(flare); + + @DynamicPropertySource + static void dynamicProperties(DynamicPropertyRegistry registry) { + var proxyHost = proxy.getHost(); + var proxyPort = proxy.getFirstMappedPort(); + + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.flare.base_url", + () -> String.format("http://%s:%s/", proxyHost, proxyPort)); + registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.store.auth.bearer.token", + () -> bearerToken); + } + + @Test + void sendQuery() throws Exception { + var rawStructuredQuery = this.getClass().getResource("valid-structured-query.json").openStream().readAllBytes(); + var feasibility = assertDoesNotThrow(() -> flareClient.requestFeasibility(rawStructuredQuery)); + assertEquals(0, feasibility); + } +} diff --git a/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_bearer_token_auth.conf.template b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_bearer_token_auth.conf.template new file mode 100644 index 0000000..1fb91c8 --- /dev/null +++ b/feasibility-dsf-process/src/test/resources/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/reverse_proxy_bearer_token_auth.conf.template @@ -0,0 +1,19 @@ +server { + listen 8080; + listen [::]:8080; + + location / { + root /usr/share/nginx/html; + index index.html; + } + + location /query/execute { + if ($http_authorization != "Bearer 1234") { + add_header WWW-Authenticate Bearer always; + return 401; + } + + proxy_pass http://flare:8080; + proxy_read_timeout 43200s; + } +}