From b809a2da0258b5df45398bb46c552c8fd1364536 Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Wed, 24 Apr 2024 08:22:13 +0200 Subject: [PATCH] refactor: move custom rest template init to config class --- .../pseudonym/GpasPseudonymGenerator.java | 112 ++---------------- .../etl/processor/config/AppConfiguration.kt | 79 +++++++++++- 2 files changed, 88 insertions(+), 103 deletions(-) diff --git a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java index c7c07f2..77caa77 100644 --- a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java +++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java @@ -23,40 +23,17 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; import dev.dnpm.etl.processor.config.GPasConfigProperties; import org.apache.commons.lang3.StringUtils; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager; -import org.apache.hc.client5.http.socket.ConnectionSocketFactory; -import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory; -import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; -import org.apache.hc.core5.http.config.Registry; -import org.apache.hc.core5.http.config.RegistryBuilder; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.r4.model.StringType; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.*; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.retry.support.RetryTemplate; import org.springframework.web.client.RestTemplate; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManagerFactory; -import java.io.BufferedInputStream; -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.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; - public class GpasPseudonymGenerator implements Generator { private final static FhirContext r4Context = FhirContext.forR4(); @@ -68,33 +45,13 @@ public class GpasPseudonymGenerator implements Generator { private final RestTemplate restTemplate; - private SSLContext customSslContext; - public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate, RestTemplate restTemplate) { this.retryTemplate = retryTemplate; + this.restTemplate = restTemplate; this.gPasUrl = gpasCfg.getUri(); this.psnTargetDomain = gpasCfg.getTarget(); httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword()); - try { - if (StringUtils.isNotBlank(gpasCfg.getSslCaLocation())) { - customSslContext = getSslContext(gpasCfg.getSslCaLocation()); - log.warn(String.format("%s has been initialized with SSL certificate %s. This is deprecated in favor of including Root CA.", - this.getClass().getName(), gpasCfg.getSslCaLocation())); - - if (customSslContext == null) { - this.restTemplate = restTemplate; - } else { - this.restTemplate = getCustomRestTemplate(); - } - } else { - this.restTemplate = restTemplate; - } - } catch (IOException | KeyManagementException | KeyStoreException | CertificateException | - NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - log.debug(String.format("%s has been initialized", this.getClass().getName())); } @@ -104,7 +61,7 @@ public class GpasPseudonymGenerator implements Generator { var gPasRequestBody = getGpasRequestBody(id); var responseEntity = getGpasPseudonym(gPasRequestBody); var gPasPseudonymResult = (Parameters) r4Context.newJsonParser() - .parseResource(responseEntity.getBody()); + .parseResource(responseEntity.getBody()); return unwrapPseudonym(gPasPseudonymResult); } @@ -118,9 +75,9 @@ public class GpasPseudonymGenerator implements Generator { } final var identifier = (Identifier) parameters.get().getPart().stream() - .filter(a -> a.getName().equals("pseudonym")) - .findFirst() - .orElseGet(ParametersParameterComponent::new).getValue(); + .filter(a -> a.getName().equals("pseudonym")) + .findFirst() + .orElseGet(ParametersParameterComponent::new).getValue(); // pseudonym return sanitizeValue(identifier.getValue()); @@ -149,8 +106,8 @@ public class GpasPseudonymGenerator implements Generator { try { responseEntity = retryTemplate.execute( - ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity, - String.class)); + ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity, + String.class)); if (responseEntity.getStatusCode().is2xxSuccessful()) { log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode()); @@ -162,16 +119,16 @@ public class GpasPseudonymGenerator implements Generator { return responseEntity; } catch (Exception unexpected) { throw new PseudonymRequestFailed( - "API request due unexpected error unsuccessful gPas unsuccessful.", unexpected); + "API request due unexpected error unsuccessful gPas unsuccessful.", unexpected); } } protected String getGpasRequestBody(String id) { var requestParameters = new Parameters(); requestParameters.addParameter().setName("target") - .setValue(new StringType().setValue(psnTargetDomain)); + .setValue(new StringType().setValue(psnTargetDomain)); requestParameters.addParameter().setName("original") - .setValue(new StringType().setValue(id)); + .setValue(new StringType().setValue(id)); final IParser iParser = r4Context.newJsonParser(); return iParser.encodeResourceToString(requestParameters); } @@ -188,53 +145,4 @@ public class GpasPseudonymGenerator implements Generator { headers.setBasicAuth(gPasUserName, gPasPassword); return headers; } - - /** - * Read SSL root certificate and return SSLContext - * - * @param certificateLocation file location to root certificate (PEM) - * @return initialized SSLContext - * @throws IOException file cannot be read - * @throws CertificateException in case we have an invalid certificate of type X.509 - * @throws KeyStoreException keystore cannot be initialized - * @throws NoSuchAlgorithmException missing trust manager algorithmus - * @throws KeyManagementException key management failed at init SSLContext - */ - @Nullable - protected SSLContext getSslContext(String certificateLocation) - throws IOException, CertificateException, KeyStoreException, KeyManagementException, NoSuchAlgorithmException { - - KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); - - FileInputStream fis = new FileInputStream(certificateLocation); - X509Certificate ca = (X509Certificate) CertificateFactory.getInstance("X.509") - .generateCertificate(new BufferedInputStream(fis)); - - ks.load(null, null); - ks.setCertificateEntry(Integer.toString(1), ca); - - TrustManagerFactory tmf = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(ks); - - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, tmf.getTrustManagers(), null); - - return sslContext; - } - - protected RestTemplate getCustomRestTemplate() { - final var sslsf = new SSLConnectionSocketFactory(customSslContext); - final Registry socketFactoryRegistry = RegistryBuilder.create() - .register("https", sslsf).register("http", new PlainConnectionSocketFactory()).build(); - - final BasicHttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager( - socketFactoryRegistry); - final CloseableHttpClient httpClient = HttpClients.custom() - .setConnectionManager(connectionManager).build(); - - final HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( - httpClient); - return new RestTemplate(requestFactory); - } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt index 4fa3bf0..2970b4d 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt @@ -20,7 +20,10 @@ package dev.dnpm.etl.processor.config import com.fasterxml.jackson.databind.ObjectMapper -import dev.dnpm.etl.processor.monitoring.* +import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult +import dev.dnpm.etl.processor.monitoring.ConnectionCheckService +import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService +import dev.dnpm.etl.processor.monitoring.ReportService import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator import dev.dnpm.etl.processor.pseudonym.Generator import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator @@ -29,12 +32,19 @@ import dev.dnpm.etl.processor.services.TokenRepository import dev.dnpm.etl.processor.services.TokenService import dev.dnpm.etl.processor.services.Transformation import dev.dnpm.etl.processor.services.TransformationService +import org.apache.hc.client5.http.impl.classic.HttpClients +import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager +import org.apache.hc.client5.http.socket.ConnectionSocketFactory +import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory +import org.apache.hc.core5.http.config.RegistryBuilder import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory import org.springframework.retry.RetryCallback import org.springframework.retry.RetryContext import org.springframework.retry.RetryListener @@ -46,6 +56,13 @@ import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.provisioning.InMemoryUserDetailsManager import org.springframework.web.client.RestTemplate import reactor.core.publisher.Sinks +import java.io.BufferedInputStream +import java.io.FileInputStream +import java.security.KeyStore +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory import kotlin.time.Duration.Companion.seconds import kotlin.time.toJavaDuration @@ -84,6 +101,66 @@ class AppConfiguration { @ConditionalOnMissingBean @Bean fun gpasPseudonymGeneratorOnDeprecatedProperty(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator { + fun getSslContext(certificateLocation: String): SSLContext? { + val ks = KeyStore.getInstance(KeyStore.getDefaultType()) + + val fis = FileInputStream(certificateLocation) + val ca = CertificateFactory.getInstance("X.509") + .generateCertificate(BufferedInputStream(fis)) as X509Certificate + + ks.load(null, null) + ks.setCertificateEntry(1.toString(), ca) + + val tmf = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm() + ) + tmf.init(ks) + + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, tmf.trustManagers, null) + + return sslContext + } + + fun getCustomRestTemplate(customSslContext: SSLContext): RestTemplate { + val sslsf = SSLConnectionSocketFactory(customSslContext) + val socketFactoryRegistry = RegistryBuilder.create() + .register("https", sslsf).register("http", PlainConnectionSocketFactory()).build() + + val connectionManager = BasicHttpClientConnectionManager( + socketFactoryRegistry + ) + val httpClient = HttpClients.custom() + .setConnectionManager(connectionManager).build() + + val requestFactory = HttpComponentsClientHttpRequestFactory( + httpClient + ) + return RestTemplate(requestFactory) + } + + try { + if (!configProperties.sslCaLocation.isNullOrBlank()) { + val customSslContext = getSslContext(configProperties.sslCaLocation) + logger.warn( + String.format( + "%s has been initialized with SSL certificate %s. This is deprecated in favor of including Root CA.", + this.javaClass.name, configProperties.sslCaLocation + ) + ) + + if (customSslContext != null) { + return GpasPseudonymGenerator( + configProperties, + retryTemplate, + getCustomRestTemplate(customSslContext) + ) + } + } + } catch (e: Exception) { + throw RuntimeException(e) + } + return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate) }