diff --git a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java index 3ba22929eefaaa6a335360ee6a147b8fef6a9c1e..74220c5e4e14603337fcae09b634379d33ecc966 100644 --- a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java +++ b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java @@ -29,12 +29,18 @@ import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.CredentialsProvider; import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; import org.apache.hc.core5.http.HttpHost; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.ssl.NoSuchSslBundleException; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; @@ -47,6 +53,8 @@ class SmartDocumentsConfiguration { @Autowired private SmartDocumentsProperties properties; + @Autowired + private SslBundles sslBundles; @Bean("smartDocuments") RestClient smartDocumentsRestClient() { @@ -59,17 +67,22 @@ class SmartDocumentsConfiguration { CloseableHttpClient buildHttpClient() { if (properties.getProxy() != null) { - return createProxyHttpClient(); + return createHttpClientUsingProxy(); } return createNoProxyHttpClient(); } + HttpClientBuilder createDefulatHttpClientBuilder() { + return HttpClients.custom() + .setConnectionManager(createConnectionManagerWithClientCertificateIfConfigured()); + } + CloseableHttpClient createNoProxyHttpClient() { - return HttpClients.createDefault(); + return createDefulatHttpClientBuilder().build(); } - CloseableHttpClient createProxyHttpClient() { - return HttpClients.custom() + CloseableHttpClient createHttpClientUsingProxy() { + return createDefulatHttpClientBuilder() .setRoutePlanner(createProxyRoutePlanner()) .setDefaultCredentialsProvider(createProxyCredentialsProvider()) .build(); @@ -94,4 +107,17 @@ class SmartDocumentsConfiguration { } } + HttpClientConnectionManager createConnectionManagerWithClientCertificateIfConfigured() { + var builder = PoolingHttpClientConnectionManagerBuilder.create(); + try { + var sslBundleName = properties.getSslBundleName(); + if (Objects.nonNull(sslBundleName)) { + builder.setTlsSocketStrategy(new DefaultClientTlsStrategy(sslBundles.getBundle(sslBundleName).createSslContext())); + } + return builder.build(); + } catch (NoSuchSslBundleException e) { + return builder.build(); + } + } + } diff --git a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsProperties.java b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsProperties.java index 6b0d37bfba52547a9d4de355903207ef286761fd..1c7839b2f12a7e7cb2ce5e66c9cc65228b96f7e2 100644 --- a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsProperties.java +++ b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsProperties.java @@ -52,10 +52,14 @@ public class SmartDocumentsProperties { /** * Credential for basic auth to the Smart Documents Server */ - @NotNull @Valid private UsernamePassword basicAuth; + /** + * Name of SSL bundle need for client certificate authentication. + */ + private String sslBundleName; + /** * Smart Documents Template Group */ diff --git a/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java index 7d21660e84c927efc35aeb334991316b008b0697..3d1829645fa269383ab59b983e004f0233094be1 100644 --- a/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java +++ b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java @@ -3,19 +3,28 @@ package de.ozgcloud.document.bescheid.smartdocuments; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import javax.net.ssl.SSLContext; + import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; 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.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpHost; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Spy; +import org.springframework.boot.ssl.NoSuchSslBundleException; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; import org.springframework.http.HttpHeaders; import de.ozgcloud.document.bescheid.smartdocuments.SmartDocumentsProperties.ProxyConfiguration; @@ -28,9 +37,11 @@ class SmartDocumentsConfigurationTest { @Mock private SmartDocumentsProperties properties; + @Mock + private SslBundles sslBundles; @Nested - class CreateProxyCredentialsProviderTest { + class TestCreateProxyCredentialsProvider { private final String user = "max"; private final String password = "max2"; @@ -61,7 +72,7 @@ class SmartDocumentsConfigurationTest { } @Nested - class CreateProxyRoutePlannerTest { + class TestCreateProxyRoutePlanner { private final String host = "test-proxy.local"; private final int port = 8080; @@ -88,7 +99,7 @@ class SmartDocumentsConfigurationTest { } @Nested - class BuildHttpClientTest { + class TestBuildHttpClient { private final CloseableHttpClient proxyHttpClient = HttpClients.createDefault(); private final CloseableHttpClient nonProxyHttpClient = HttpClients.createDefault(); @@ -98,7 +109,7 @@ class SmartDocumentsConfigurationTest { @Test void shouldReturnProxyHttpClient() { - doReturn(proxyHttpClient).when(smartDocumentsConfiguration).createProxyHttpClient(); + doReturn(proxyHttpClient).when(smartDocumentsConfiguration).createHttpClientUsingProxy(); when(properties.getProxy()).thenReturn(new ProxyConfiguration()); var httpClient = smartDocumentsConfiguration.buildHttpClient(); @@ -118,7 +129,7 @@ class SmartDocumentsConfigurationTest { } @Nested - class AddBasicAuthenticationIfConfiguredTest { + class TestAddBasicAuthenticationIfConfigured { @Test void shouldNotAddBasicAuthentication() { @@ -142,4 +153,92 @@ class SmartDocumentsConfigurationTest { .containsExactly(UsernamePasswordTestFactory.getAsBasicAuthenticationHeader()); } } + + @Nested + class TestCreateConnectionManagerWithClientCertificateIfConfigured { + + private MockedStatic<PoolingHttpClientConnectionManagerBuilder> staticBuilder; + @Mock + private PoolingHttpClientConnectionManagerBuilder builder; + @Mock + private SslBundle sslBundle; + @Mock + private SSLContext sslContext; + + private final String bundleName = "smartdocuments_bundle"; + private final PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + + @BeforeEach + void setUp() { + staticBuilder = mockStatic(PoolingHttpClientConnectionManagerBuilder.class); + staticBuilder.when(PoolingHttpClientConnectionManagerBuilder::create).thenReturn(builder); + when(builder.build()).thenReturn(connectionManager); + } + + @AfterEach + void tearDown() { + staticBuilder.close(); + } + + @Test + void shouldCreateWithBuilder() { + smartDocumentsConfiguration.createConnectionManagerWithClientCertificateIfConfigured(); + + staticBuilder.verify(() -> PoolingHttpClientConnectionManagerBuilder.create()); + } + + @Nested + class OnMissingClientCertificateConfiguration { + + @Test + void shouldCreateWithoutTlsStrategyOnMissingBundleName() { + when(properties.getSslBundleName()).thenReturn(null); + + var created = smartDocumentsConfiguration.createConnectionManagerWithClientCertificateIfConfigured(); + + verify(builder, never()).setTlsSocketStrategy(any()); + assertThat(created).isSameAs(connectionManager); + + } + + @Test + void shouldCreateWithoutTlsStrategyOnMissingBundleConfiguration() { + when(properties.getSslBundleName()).thenReturn(bundleName); + when(sslBundles.getBundle(anyString())).thenThrow(new NoSuchSslBundleException("some_bundle", "error message")); + + var created = smartDocumentsConfiguration.createConnectionManagerWithClientCertificateIfConfigured(); + + verify(builder, never()).setTlsSocketStrategy(any()); + assertThat(created).isSameAs(connectionManager); + } + + } + + @Nested + class OnExistingClientCertificateConfiguration { + + @BeforeEach + void setUp() { + when(properties.getSslBundleName()).thenReturn(bundleName); + when(sslBundles.getBundle(any())).thenReturn(sslBundle); + when(sslBundle.createSslContext()).thenReturn(sslContext); + } + + @Test + void shouldGetSslBundle() { + smartDocumentsConfiguration.createConnectionManagerWithClientCertificateIfConfigured(); + + verify(sslBundles).getBundle(bundleName); + } + + @Test + void shouldCreateWithTlsStrategy() { + var created = smartDocumentsConfiguration.createConnectionManagerWithClientCertificateIfConfigured(); + + verify(builder).setTlsSocketStrategy(any()); + assertThat(created).isSameAs(connectionManager); + } + } + + } } \ No newline at end of file