diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72c6e85067..c16b9a8ac1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -313,6 +313,10 @@ jobs: - testSuite: SearchTestSuite compose-profile: default mvn-options: '-Dindex.subsystem.name=solr6' + - testSuite: MTLSTestSuite + compose-profile: with-mtls-transform-core-aio + mtls: true + mvn-options: '-Dencryption.ssl.keystore.location=${GITHUB_WORKSPACE}/keystores/alfresco/alfresco.keystore -Dencryption.ssl.truststore.location=${GITHUB_WORKSPACE}/keystores/alfresco/alfresco.truststore' steps: - uses: actions/checkout@v3 - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0 @@ -321,6 +325,11 @@ jobs: run: bash ./scripts/ci/init.sh - name: "Set transformers tag" run: echo "TRANSFORMERS_TAG=$(mvn help:evaluate -Dexpression=dependency.alfresco-transform-core.version -q -DforceStdout)" >> $GITHUB_ENV + - name: "Generate Keystores and Truststores for Mutual TLS configuration" + if: ${{ matrix.mtls }} + run: | + git clone -b "master" --depth=1 "https://${{ secrets.BOT_GITHUB_USERNAME }}:${{ secrets.BOT_GITHUB_TOKEN }}@github.com/Alfresco/alfresco-ssl-generator.git" + bash ./scripts/ci/generate_keystores.sh - name: "Set up the environment" run: | if [ -e ./scripts/ci/tests/${{ matrix.testSuite }}-setup.sh ]; then diff --git a/core/pom.xml b/core/pom.xml index 8c9e8218bf..5459094493 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -137,6 +137,10 @@ commons-dbcp2 test + + org.apache.httpcomponents + httpclient + diff --git a/core/src/main/java/org/alfresco/httpclient/HttpClient4Factory.java b/core/src/main/java/org/alfresco/httpclient/HttpClient4Factory.java new file mode 100644 index 0000000000..115ffdeee0 --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/HttpClient4Factory.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2023 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.httpclient; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.client.StandardHttpRequestRetryHandler; + + +public class HttpClient4Factory +{ + protected static final String TLS_PROTOCOL = "TLS"; + protected static final String HTTPS_PROTOCOL = "https"; + protected static final String HTTP_TARGET_HOST = "http.target_host"; + protected static final String TLS_V_1_2 = "TLSv1.2"; + protected static final String TLS_V_1_3 = "TLSv1.3"; + + private static SSLContext createSSLContext(HttpClientConfig config) + { + KeyManager[] keyManagers = config.getKeyStore().createKeyManagers(); + TrustManager[] trustManagers = config.getTrustStore().createTrustManagers(); + + try + { + SSLContext sslcontext = SSLContext.getInstance(TLS_PROTOCOL); + sslcontext.init(keyManagers, trustManagers, null); + return sslcontext; + } + catch(Throwable e) + { + throw new AlfrescoRuntimeException("Unable to create SSL context", e); + } + } + + public static CloseableHttpClient createHttpClient(HttpClientConfig config) + { + return createHttpClient(config, null); + } + + public static CloseableHttpClient createHttpClient(HttpClientConfig config, HttpClientConnectionManager connectionManager) + { + HttpClientBuilder clientBuilder = HttpClients.custom(); + + if(config.isMTLSEnabled()) + { + clientBuilder.addInterceptorFirst((HttpRequestInterceptor) (request, context) -> { + if (!((HttpHost) context.getAttribute(HTTP_TARGET_HOST)).getSchemeName().equals(HTTPS_PROTOCOL)) + { + String msg = "mTLS is enabled but provided URL does not use a secured protocol"; + throw new HttpClientException(msg); + } + }); + + SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory( + createSSLContext(config), + new String[] { TLS_V_1_2, TLS_V_1_3 }, + null, + config.isHostnameVerificationDisabled() ? new NoopHostnameVerifier() : SSLConnectionSocketFactory.getDefaultHostnameVerifier()); + clientBuilder.setSSLSocketFactory(sslConnectionSocketFactory); + } + + if(connectionManager != null) + { + clientBuilder.setConnectionManager(connectionManager); + } + + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(config.getConnectionTimeout()) + .setSocketTimeout(config.getSocketTimeout()) + .setConnectionRequestTimeout(config.getConnectionRequestTimeout()) + .build(); + + clientBuilder.setDefaultRequestConfig(requestConfig); + clientBuilder.setMaxConnTotal(config.getMaxTotalConnections()); + clientBuilder.setMaxConnPerRoute(config.getMaxHostConnections()); + clientBuilder.setRetryHandler(new StandardHttpRequestRetryHandler(5, false)); + + return clientBuilder.build(); + } + +} diff --git a/core/src/main/java/org/alfresco/httpclient/HttpClientConfig.java b/core/src/main/java/org/alfresco/httpclient/HttpClientConfig.java new file mode 100644 index 0000000000..7700c3382c --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/HttpClientConfig.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2023 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.httpclient; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.stream.Collectors; + +import org.alfresco.encryption.AlfrescoKeyStore; +import org.alfresco.encryption.AlfrescoKeyStoreImpl; +import org.alfresco.encryption.KeyResourceLoader; +import org.alfresco.encryption.ssl.SSLEncryptionParameters; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class HttpClientConfig +{ + private static final String HTTPCLIENT_CONFIG = "httpclient.config."; + + protected static final Log LOGGER = LogFactory.getLog(HttpClientConfig.class); + + private Properties properties; + private String serviceName; + + private SSLEncryptionParameters sslEncryptionParameters; + private KeyResourceLoader keyResourceLoader; + + private AlfrescoKeyStore keyStore; + private AlfrescoKeyStore trustStore; + + private Map config; + + public void setProperties(Properties properties) + { + this.properties = properties; + } + + public void setServiceName(String serviceName) + { + this.serviceName = serviceName; + } + + public void setSslEncryptionParameters(SSLEncryptionParameters sslEncryptionParameters) + { + this.sslEncryptionParameters = sslEncryptionParameters; + } + + public void setKeyResourceLoader(KeyResourceLoader keyResourceLoader) + { + this.keyResourceLoader = keyResourceLoader; + } + + public AlfrescoKeyStore getKeyStore() + { + return keyStore; + } + + public AlfrescoKeyStore getTrustStore() + { + return trustStore; + } + + public void init() + { + this.keyStore = new AlfrescoKeyStoreImpl(sslEncryptionParameters.getKeyStoreParameters(), keyResourceLoader); + this.trustStore = new AlfrescoKeyStoreImpl(sslEncryptionParameters.getTrustStoreParameters(), keyResourceLoader); + + config = retrieveConfig(serviceName); + checkUnsupportedProperties(config); + } + + /** + * Method used for retrieving HttpClient config from Global Properties + * @param serviceName name of used service + * @return map of properties + */ + private Map retrieveConfig(String serviceName) + { + return properties.keySet().stream() + .filter(key -> key instanceof String) + .map(Object::toString) + .filter(key -> key.startsWith(HTTPCLIENT_CONFIG + serviceName)) + .collect(Collectors.toMap( + key -> key.replace(HTTPCLIENT_CONFIG + serviceName + ".", ""), + key -> properties.getProperty(key, null))); + } + + private void checkUnsupportedProperties(Map config) + { + config.keySet().stream() + .filter(propertyName -> !HttpClientPropertiesEnum.isPropertyNameSupported(propertyName)) + .forEach(propertyName -> LOGGER.warn(String.format("For service [%s], an unsupported property [%s] is set", serviceName, propertyName))); + } + + private Integer getIntegerProperty(HttpClientPropertiesEnum property) + { + return Integer.parseInt(extractValueFromConfig(property).orElse("0")); + } + + private Boolean getBooleanProperty(HttpClientPropertiesEnum property) + { + return Boolean.parseBoolean(extractValueFromConfig(property).orElse("false")); + } + + private Optional extractValueFromConfig(HttpClientPropertiesEnum property) + { + Optional optionalProperty = Optional.ofNullable(config.get(property.name)); + if(property.isRequired && optionalProperty.isEmpty()) + { + String msg = String.format("Required property: '%s' is empty.", property.name); + throw new HttpClientException(msg); + } + return optionalProperty; + } + + public Integer getConnectionTimeout() + { + return getIntegerProperty(HttpClientPropertiesEnum.CONNECTION_REQUEST_TIMEOUT); + } + + public Integer getSocketTimeout() + { + return getIntegerProperty(HttpClientPropertiesEnum.SOCKET_TIMEOUT); + } + + public Integer getConnectionRequestTimeout() + { + return getIntegerProperty(HttpClientPropertiesEnum.CONNECTION_REQUEST_TIMEOUT); + } + + public Integer getMaxTotalConnections() + { + return getIntegerProperty(HttpClientPropertiesEnum.MAX_TOTAL_CONNECTIONS); + } + + public Integer getMaxHostConnections() + { + return getIntegerProperty(HttpClientPropertiesEnum.MAX_HOST_CONNECTIONS); + } + + public Boolean isMTLSEnabled() + { + return getBooleanProperty(HttpClientPropertiesEnum.MTLS_ENABLED); + } + + public boolean isHostnameVerificationDisabled() + { + return getBooleanProperty(HttpClientPropertiesEnum.HOSTNAME_VERIFICATION_DISABLED); + } + + private enum HttpClientPropertiesEnum + { + CONNECTION_TIMEOUT("connectionTimeout", true), + SOCKET_TIMEOUT("socketTimeout", true), + CONNECTION_REQUEST_TIMEOUT("connectionRequestTimeout", true), + MAX_TOTAL_CONNECTIONS("maxTotalConnections", true), + MAX_HOST_CONNECTIONS("maxHostConnections", true), + HOSTNAME_VERIFICATION_DISABLED("hostnameVerificationDisabled", false), + MTLS_ENABLED("mTLSEnabled", true); + + private final String name; + private final Boolean isRequired; + + HttpClientPropertiesEnum(String propertyName, Boolean isRequired) + { + this.name = propertyName; + this.isRequired = isRequired; + } + + private static final List supportedProperties = new ArrayList<>(); + + static { + for (HttpClientPropertiesEnum property : HttpClientPropertiesEnum.values()) { + supportedProperties.add(property.name); + } + } + + public static boolean isPropertyNameSupported(String propertyName) { + return supportedProperties.contains(propertyName); + } + } +} diff --git a/core/src/main/java/org/alfresco/httpclient/HttpClientException.java b/core/src/main/java/org/alfresco/httpclient/HttpClientException.java new file mode 100644 index 0000000000..9dc0f38784 --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/HttpClientException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.httpclient; + +import org.alfresco.error.AlfrescoRuntimeException; + +public class HttpClientException extends AlfrescoRuntimeException +{ + public HttpClientException(String msgId) + { + super(msgId); + } +} diff --git a/repository/src/main/java/org/alfresco/repo/content/transform/LocalTransformImpl.java b/repository/src/main/java/org/alfresco/repo/content/transform/LocalTransformImpl.java index 942afac149..afcc0a16f7 100644 --- a/repository/src/main/java/org/alfresco/repo/content/transform/LocalTransformImpl.java +++ b/repository/src/main/java/org/alfresco/repo/content/transform/LocalTransformImpl.java @@ -25,6 +25,7 @@ */ package org.alfresco.repo.content.transform; +import org.alfresco.httpclient.HttpClientConfig; import org.alfresco.repo.content.metadata.AsynchronousExtractor; import org.alfresco.repo.rendition2.RenditionDefinition2; import org.alfresco.service.cmr.repository.ContentReader; @@ -60,11 +61,12 @@ public class LocalTransformImpl extends AbstractLocalTransform boolean retryTransformOnDifferentMimeType, Set transformsTransformOptions, LocalTransformServiceRegistry localTransformServiceRegistry, String baseUrl, + HttpClientConfig httpClientConfig, int startupRetryPeriodSeconds) { super(name, transformerDebug, mimetypeService, strictMimeTypeCheck, strictMimetypeExceptions, retryTransformOnDifferentMimeType, transformsTransformOptions, localTransformServiceRegistry); - remoteTransformerClient = new RemoteTransformerClient(name, baseUrl); + remoteTransformerClient = new RemoteTransformerClient(name, baseUrl, httpClientConfig); remoteTransformerClient.setStartupRetryPeriodSeconds(startupRetryPeriodSeconds); checkAvailability(); diff --git a/repository/src/main/java/org/alfresco/repo/content/transform/LocalTransformServiceRegistry.java b/repository/src/main/java/org/alfresco/repo/content/transform/LocalTransformServiceRegistry.java index 74447ad753..34ed4058c0 100644 --- a/repository/src/main/java/org/alfresco/repo/content/transform/LocalTransformServiceRegistry.java +++ b/repository/src/main/java/org/alfresco/repo/content/transform/LocalTransformServiceRegistry.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2019 - 2022 Alfresco Software Limited + * Copyright (C) 2019 - 2023 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -35,6 +35,8 @@ import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; +import org.alfresco.httpclient.HttpClient4Factory; +import org.alfresco.httpclient.HttpClientConfig; import org.alfresco.service.cmr.repository.MimetypeService; import org.alfresco.transform.config.CoreFunction; import org.alfresco.transform.config.TransformOptionGroup; @@ -77,6 +79,17 @@ public class LocalTransformServiceRegistry extends TransformServiceRegistryImpl private boolean strictMimeTypeCheck; private Map> strictMimetypeExceptions; private boolean retryTransformOnDifferentMimeType; + private HttpClientConfig httpClientConfig; + + public HttpClientConfig getHttpClientConfig() + { + return httpClientConfig; + } + + public void setHttpClientConfig(HttpClientConfig httpClientConfig) + { + this.httpClientConfig = httpClientConfig; + } public void setPipelineConfigDir(String pipelineConfigDir) { @@ -134,7 +147,7 @@ public class LocalTransformServiceRegistry extends TransformServiceRegistryImpl @Override public boolean readConfig() throws IOException { - CombinedConfig combinedConfig = new CombinedConfig(getLog(), this); + CombinedConfig combinedConfig = new CombinedConfig(getLog(), this, httpClientConfig); List urls = getTEngineUrlsSortedByName(); boolean successReadingConfig = combinedConfig.addRemoteConfig(urls, "T-Engine"); successReadingConfig &= combinedConfig.addLocalConfig("alfresco/transforms"); @@ -188,7 +201,8 @@ public class LocalTransformServiceRegistry extends TransformServiceRegistryImpl int startupRetryPeriodSeconds = getStartupRetryPeriodSeconds(name); localTransform = new LocalTransformImpl(name, transformerDebug, mimetypeService, strictMimeTypeCheck, strictMimetypeExceptions, retryTransformOnDifferentMimeType, - transformsTransformOptions, this, baseUrl, startupRetryPeriodSeconds); + transformsTransformOptions, this, baseUrl, httpClientConfig, + startupRetryPeriodSeconds); } else if (isPipeline) { diff --git a/repository/src/main/java/org/alfresco/repo/content/transform/RemoteTransformerClient.java b/repository/src/main/java/org/alfresco/repo/content/transform/RemoteTransformerClient.java index a3713f7cb9..c03f9270b4 100644 --- a/repository/src/main/java/org/alfresco/repo/content/transform/RemoteTransformerClient.java +++ b/repository/src/main/java/org/alfresco/repo/content/transform/RemoteTransformerClient.java @@ -30,6 +30,8 @@ import java.io.InputStream; import java.util.StringJoiner; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.httpclient.HttpClient4Factory; +import org.alfresco.httpclient.HttpClientConfig; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.util.Pair; @@ -57,6 +59,8 @@ import org.apache.http.util.EntityUtils; */ public class RemoteTransformerClient { + private final HttpClientConfig httpClientConfig; + private final String name; private final String baseUrl; @@ -70,10 +74,11 @@ public class RemoteTransformerClient // Only changed once on success. This is stored so it can always be returned. private Pair checkResult = new Pair<>(null, null); - public RemoteTransformerClient(String name, String baseUrl) + public RemoteTransformerClient(String name, String baseUrl, HttpClientConfig httpClientConfig) { this.name = name; this.baseUrl = baseUrl == null || baseUrl.trim().isEmpty() ? null : baseUrl.trim(); + this.httpClientConfig = httpClientConfig; } public void setStartupRetryPeriodSeconds(int startupRetryPeriodSeconds) @@ -129,7 +134,7 @@ public class RemoteTransformerClient try { - try (CloseableHttpClient httpclient = HttpClients.createDefault()) + try (CloseableHttpClient httpclient = HttpClient4Factory.createHttpClient(httpClientConfig)) { try (CloseableHttpResponse response = execute(httpclient, httppost)) { @@ -232,7 +237,7 @@ public class RemoteTransformerClient try { - try (CloseableHttpClient httpclient = HttpClients.createDefault()) + try (CloseableHttpClient httpclient = HttpClient4Factory.createHttpClient(httpClientConfig)) { try (CloseableHttpResponse response = execute(httpclient, httpGet)) { diff --git a/repository/src/main/java/org/alfresco/transform/registry/CombinedConfig.java b/repository/src/main/java/org/alfresco/transform/registry/CombinedConfig.java index dfcd8fb466..414d15312f 100644 --- a/repository/src/main/java/org/alfresco/transform/registry/CombinedConfig.java +++ b/repository/src/main/java/org/alfresco/transform/registry/CombinedConfig.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2022 Alfresco Software Limited + * Copyright (C) 2005 - 2023 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -28,6 +28,8 @@ package org.alfresco.transform.registry; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.httpclient.HttpClient4Factory; +import org.alfresco.httpclient.HttpClientConfig; import org.alfresco.repo.content.transform.LocalPassThroughTransform; import org.alfresco.service.cmr.repository.MimetypeService; import org.alfresco.transform.config.TransformConfig; @@ -39,7 +41,6 @@ import org.apache.http.StatusLine; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import java.io.IOException; @@ -67,8 +68,11 @@ public class CombinedConfig extends CombinedTransformConfig private ConfigFileFinder configFileFinder; private int tEngineCount; - public CombinedConfig(Log log, AbstractTransformRegistry registry) + private final HttpClientConfig httpClientConfig; + + public CombinedConfig(Log log, AbstractTransformRegistry registry, HttpClientConfig httpClientConfig) { + this.httpClientConfig = httpClientConfig; this.log = log; configFileFinder = new ConfigFileFinder(jsonObjectMapper) @@ -87,88 +91,84 @@ public class CombinedConfig extends CombinedTransformConfig return configFileFinder.readFiles(path, log); } - public boolean addRemoteConfig(List urls, String remoteType) + public boolean addRemoteConfig(List urls, String remoteType) throws IOException { - boolean successReadingConfig = true; - for (String url : urls) + try(CloseableHttpClient httpclient = HttpClient4Factory.createHttpClient(httpClientConfig)) { - if (addRemoteConfig(url, remoteType)) + boolean successReadingConfig = true; + for (String url : urls) { - tEngineCount++ ; - } - else - { - successReadingConfig = false; + if (addRemoteConfig(httpclient, url, remoteType)) + { + tEngineCount++; + } else + { + successReadingConfig = false; + } } + return successReadingConfig; } - return successReadingConfig; } - private boolean addRemoteConfig(String baseUrl, String remoteType) + private boolean addRemoteConfig(CloseableHttpClient httpclient, String baseUrl, String remoteType) { String url = baseUrl + (baseUrl.endsWith("/") ? "" : "/") + ENDPOINT_TRANSFORM_CONFIG_LATEST; HttpGet httpGet = new HttpGet(url); boolean successReadingConfig = true; try { - try (CloseableHttpClient httpclient = HttpClients.createDefault()) + try (CloseableHttpResponse response = execute(httpclient, httpGet)) { - try (CloseableHttpResponse response = execute(httpclient, httpGet)) + StatusLine statusLine = response.getStatusLine(); + if (statusLine == null) { - StatusLine statusLine = response.getStatusLine(); - if (statusLine == null) + throw new AlfrescoRuntimeException(remoteType+" on " + url+" returned no status "); + } + HttpEntity resEntity = response.getEntity(); + if (resEntity != null) + { + int statusCode = statusLine.getStatusCode(); + if (statusCode == 200) { - throw new AlfrescoRuntimeException(remoteType+" on " + url+" returned no status "); - } - HttpEntity resEntity = response.getEntity(); - if (resEntity != null) - { - int statusCode = statusLine.getStatusCode(); - if (statusCode == 200) + try { - try + String content = getContent(resEntity); + try (StringReader reader = new StringReader(content)) { - String content = getContent(resEntity); - try (StringReader reader = new StringReader(content)) + int transformCount = transformerCount(); + configFileFinder.readFile(reader, remoteType+" on "+baseUrl, "json", baseUrl, log); + if (transformCount == transformerCount()) { - int transformCount = transformerCount(); - configFileFinder.readFile(reader, remoteType+" on "+baseUrl, "json", baseUrl, log); - if (transformCount == transformerCount()) - { - successReadingConfig = false; - } + successReadingConfig = false; } + } - EntityUtils.consume(resEntity); - } - catch (IOException e) - { - throw new AlfrescoRuntimeException("Failed to read the returned content from "+ - remoteType+" on " + url, e); - } + EntityUtils.consume(resEntity); } - else + catch (IOException e) { - String message = getErrorMessage(resEntity); - throw new AlfrescoRuntimeException(remoteType+" on " + url+" returned a " + statusCode + - " status " + message); + throw new AlfrescoRuntimeException("Failed to read the returned content from "+ + remoteType+" on " + url, e); } } else { - throw new AlfrescoRuntimeException(remoteType+" on " + url+" did not return an entity " + url); + String message = getErrorMessage(resEntity); + throw new AlfrescoRuntimeException(remoteType+" on " + url+" returned a " + statusCode + + " status " + message); } } - catch (IOException e) + else { - throw new AlfrescoRuntimeException("Failed to connect or to read the response from "+remoteType+ - " on " + url, e); + throw new AlfrescoRuntimeException(remoteType+" on " + url+" did not return an entity " + url); } } catch (IOException e) { - throw new AlfrescoRuntimeException(remoteType+" on " + url+" failed to create an HttpClient", e); + throw new AlfrescoRuntimeException("Failed to connect or to read the response from "+remoteType+ + " on " + url, e); } + } catch (AlfrescoRuntimeException e) { diff --git a/repository/src/main/resources/alfresco/rendition-services2-context.xml b/repository/src/main/resources/alfresco/rendition-services2-context.xml index 6581c6aeb5..a24f3c8c7e 100644 --- a/repository/src/main/resources/alfresco/rendition-services2-context.xml +++ b/repository/src/main/resources/alfresco/rendition-services2-context.xml @@ -107,6 +107,13 @@ + + + + + + + @@ -118,7 +125,8 @@ - + + diff --git a/repository/src/main/resources/alfresco/repository.properties b/repository/src/main/resources/alfresco/repository.properties index adf594204c..ae82113d9e 100644 --- a/repository/src/main/resources/alfresco/repository.properties +++ b/repository/src/main/resources/alfresco/repository.properties @@ -749,6 +749,15 @@ encryption.ssl.truststore.type=JCEKS # configuration via metadata is deprecated encryption.ssl.truststore.keyMetaData.location= +## HttpClient config for Transform Service +#enable mtls in HttpClientFactory for Transform Service. +httpclient.config.transform.mTLSEnabled=false +httpclient.config.transform.maxTotalConnections=40 +httpclient.config.transform.maxHostConnections=40 +httpclient.config.transform.socketTimeout=5000 +httpclient.config.transform.connectionRequestTimeout=5000 +httpclient.config.transform.connectionTimeout=5000 + # Re-encryptor properties encryption.reencryptor.chunkSize=100 encryption.reencryptor.numThreads=2 diff --git a/repository/src/test/java/org/alfresco/MTLSTestSuite.java b/repository/src/test/java/org/alfresco/MTLSTestSuite.java new file mode 100644 index 0000000000..a66438aa65 --- /dev/null +++ b/repository/src/test/java/org/alfresco/MTLSTestSuite.java @@ -0,0 +1,39 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco; + +import org.alfresco.repo.security.mtls.LocalTransformClientWithMTLSIntegrationTest; +import org.junit.experimental.categories.Categories; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith (Categories.class) +@Suite.SuiteClasses({ + LocalTransformClientWithMTLSIntegrationTest.class +}) +public class MTLSTestSuite +{ +} diff --git a/repository/src/test/java/org/alfresco/repo/content/transform/RemoteTransformerClientTest.java b/repository/src/test/java/org/alfresco/repo/content/transform/RemoteTransformerClientTest.java index c346ad4007..d9834b2015 100644 --- a/repository/src/test/java/org/alfresco/repo/content/transform/RemoteTransformerClientTest.java +++ b/repository/src/test/java/org/alfresco/repo/content/transform/RemoteTransformerClientTest.java @@ -25,6 +25,7 @@ */ package org.alfresco.repo.content.transform; +import org.alfresco.httpclient.HttpClientConfig; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.util.Pair; @@ -79,7 +80,7 @@ public class RemoteTransformerClientTest @Mock private StatusLine mockStatusLine; @Mock private HttpEntity mockReqEntity; - @Spy private RemoteTransformerClient remoteTransformerClient = new RemoteTransformerClient("TRANSFORMER", "http://localhost:1234/test"); + @Spy private RemoteTransformerClient remoteTransformerClient = new RemoteTransformerClient("TRANSFORMER", "http://localhost:1234/test", new HttpClientConfig()); private String sourceMimetype = "application/msword"; private String sourceExtension = "doc"; diff --git a/repository/src/test/java/org/alfresco/repo/security/mtls/LocalTransformClientWithMTLSIntegrationTest.java b/repository/src/test/java/org/alfresco/repo/security/mtls/LocalTransformClientWithMTLSIntegrationTest.java new file mode 100644 index 0000000000..32aa5a820b --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/security/mtls/LocalTransformClientWithMTLSIntegrationTest.java @@ -0,0 +1,53 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.repo.security.mtls; + + +import org.alfresco.repo.rendition2.LocalTransformClientIntegrationTest; +import org.alfresco.repo.rendition2.RenditionService2; +import org.junit.BeforeClass; + +/** + * Integration tests for {@link RenditionService2} with mtls enabled + */ +public class LocalTransformClientWithMTLSIntegrationTest extends LocalTransformClientIntegrationTest +{ + @BeforeClass + public static void before() + { + local(); + + System.setProperty("localTransform.core-aio.url", "https://localhost:8090/"); + System.setProperty("httpclient.config.transform.mTLSEnabled", "true"); + System.setProperty("ssl-keystore.password", "password"); + System.setProperty("ssl-truststore.password", "password"); + System.setProperty("metadata-keystore.password", "password"); + System.setProperty("metadata-keystore.aliases", "metadata"); + System.setProperty("metadata-keystore.metadata.password", "password"); + } + +} diff --git a/repository/src/test/java/org/alfresco/transform/registry/LocalTransformServiceRegistryConfigTest.java b/repository/src/test/java/org/alfresco/transform/registry/LocalTransformServiceRegistryConfigTest.java index 52a2e4d373..14ed9b8e95 100644 --- a/repository/src/test/java/org/alfresco/transform/registry/LocalTransformServiceRegistryConfigTest.java +++ b/repository/src/test/java/org/alfresco/transform/registry/LocalTransformServiceRegistryConfigTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2022 Alfresco Software Limited + * Copyright (C) 2005 - 2023 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -26,6 +26,12 @@ package org.alfresco.transform.registry; import com.fasterxml.jackson.databind.ObjectMapper; + +import org.alfresco.encryption.KeyResourceLoader; +import org.alfresco.encryption.ssl.SSLEncryptionParameters; +import org.alfresco.httpclient.GetRequest; +import org.alfresco.httpclient.HttpClient4Factory; +import org.alfresco.httpclient.HttpClientConfig; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.content.transform.AbstractLocalTransform; import org.alfresco.repo.content.transform.LocalPipelineTransform; @@ -38,15 +44,23 @@ import org.alfresco.transform.config.TransformOption; import org.alfresco.transform.config.TransformOptionGroup; import org.alfresco.transform.config.TransformOptionValue; import org.alfresco.transform.config.Transformer; +import org.alfresco.util.ApplicationContextHelper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.config.Configurator; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.quartz.CronExpression; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; import java.io.IOException; import java.util.ArrayList; @@ -265,7 +279,7 @@ public class LocalTransformServiceRegistryConfigTest extends TransformRegistryMo */ private void retrieveLocalTransformList(String path) { - CombinedConfig combinedConfig = new CombinedConfig(log, registry); + CombinedConfig combinedConfig = new CombinedConfig(log, registry, registry.getHttpClientConfig()); combinedConfig.addLocalConfig(path); combinedConfig.register(registry); @@ -388,7 +402,7 @@ public class LocalTransformServiceRegistryConfigTest extends TransformRegistryMo private void register(String path) throws IOException { - CombinedConfig combinedConfig = new CombinedConfig(log, registry); + CombinedConfig combinedConfig = new CombinedConfig(log, registry, registry.getHttpClientConfig()); combinedConfig.addLocalConfig(path); combinedConfig.register((TransformServiceRegistryImpl)registry); } @@ -928,4 +942,5 @@ public class LocalTransformServiceRegistryConfigTest extends TransformRegistryMo -1,"image/png", Collections.emptyMap(), null); assertNotNull("Should supported csv to png", pipelineTransform); } + } diff --git a/repository/src/test/resources/test/alfresco/test-renditions-context.xml b/repository/src/test/resources/test/alfresco/test-renditions-context.xml index 855d93a1e7..990b71bf97 100644 --- a/repository/src/test/resources/test/alfresco/test-renditions-context.xml +++ b/repository/src/test/resources/test/alfresco/test-renditions-context.xml @@ -8,6 +8,13 @@ parent="baseTransformationRenderingEngine"> + + + + + + + @@ -20,6 +27,7 @@ + \ No newline at end of file diff --git a/scripts/ci/docker-compose/docker-compose.yaml b/scripts/ci/docker-compose/docker-compose.yaml index c14fc91734..053a0bb320 100644 --- a/scripts/ci/docker-compose/docker-compose.yaml +++ b/scripts/ci/docker-compose/docker-compose.yaml @@ -10,7 +10,7 @@ services: - "8090:8090" postgres: image: postgres:14.4 - profiles: ["default", "with-transform-core-aio", "postgres"] + profiles: ["default", "with-transform-core-aio", "postgres", "with-mtls-transform-core-aio"] environment: - POSTGRES_PASSWORD=alfresco - POSTGRES_USER=alfresco @@ -19,8 +19,41 @@ services: ports: - "5433:5432" activemq: - profiles: ["default", "with-transform-core-aio", "activemq"] + profiles: ["default", "with-transform-core-aio", "activemq", "with-mtls-transform-core-aio"] image: alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 ports: - "5672:5672" # AMQP - - "61616:61616" # OpenWire \ No newline at end of file + - "61616:61616" # OpenWire + mtls-transform-core-aio: + profiles: ["with-mtls-transform-core-aio"] + image: quay.io/alfresco/alfresco-transform-core-aio:${TRANSFORMERS_TAG} + hostname: transform-core-aio + ports: + - 8090:8090 + volumes: + - ${GITHUB_WORKSPACE}/keystores/tengineAIO/tengineAIO.truststore:/tengineAIO.truststore + - ${GITHUB_WORKSPACE}/keystores/tengineAIO/tengineAIO.keystore:/tengineAIO.keystore + environment: + ACTIVEMQ_URL: "nio://activemq:61616" + ACTIVEMQ_USER: "admin" + ACTIVEMQ_PASSWORD: "admin" + LOG_LEVEL: debug + + SERVER_SSL_ENABLED: "true" + SERVER_SSL_KEY_PASSWORD: "password" + SERVER_SSL_KEY_STORE: "file:/tengineAIO.keystore" + SERVER_SSL_KEY_STORE_PASSWORD: "password" + SERVER_SSL_KEY_STORE_TYPE: "JCEKS" + + SERVER_SSL_CLIENT_AUTH: "need" + SERVER_SSL_TRUST_STORE: "file:/tengineAIO.truststore" + SERVER_SSL_TRUST_STORE_PASSWORD: "password" + SERVER_SSL_TRUST_STORE_TYPE: "JCEKS" + + CLIENT_SSL_KEY_STORE: "file:/tengineAIO.keystore" + CLIENT_SSL_KEY_STORE_PASSWORD: "password" + CLIENT_SSL_KEY_STORE_TYPE: "JCEKS" + + CLIENT_SSL_TRUST_STORE: "file:/tengineAIO.truststore" + CLIENT_SSL_TRUST_STORE_PASSWORD: "password" + CLIENT_SSL_TRUST_STORE_TYPE: "JCEKS" \ No newline at end of file diff --git a/scripts/ci/generate_keystores.sh b/scripts/ci/generate_keystores.sh new file mode 100644 index 0000000000..cf37ea857e --- /dev/null +++ b/scripts/ci/generate_keystores.sh @@ -0,0 +1,27 @@ +#! /bin/bash +#! /bin/bash + +# SETTINGS +# Alfresco Format: "classic" / "current" is supported only from 7.0 +ALFRESCO_FORMAT=current + +#Contains directory settings +source ${GITHUB_WORKSPACE}/alfresco-ssl-generator/ssl-tool/utils.sh + +# Cleanup previous output of script +rm -rd $CA_DIR +rm -rd $KEYSTORES_DIR +rm -rd $CERTIFICATES_DIR + +# SETTINGS +# Alfresco Format: "classic" / "current" is supported only from 7.0 +ALFRESCO_FORMAT=current + +#CA +${GITHUB_WORKSPACE}/alfresco-ssl-generator/ssl-tool/run_ca.sh -keysize 2048 -keystorepass password -certdname "/C=GB/ST=UK/L=Maidenhead/O=Alfresco Software Ltd./OU=Unknown/CN=Custom Alfresco CA" -servername localhost -validityduration 1 +#Alfresco +${GITHUB_WORKSPACE}/alfresco-ssl-generator/ssl-tool/run_additional.sh -servicename alfresco -rootcapass password -keysize 2048 -keystoretype JCEKS -keystorepass password -truststoretype JCEKS -truststorepass password -certdname "/C=GB/ST=UK/L=Maidenhead/O=Alfresco Software Ltd./OU=Unknown/CN=Custom Alfresco Repository" -servername localhost -alfrescoformat $ALFRESCO_FORMAT +#Alfresco Metadata encryption +${GITHUB_WORKSPACE}/alfresco-ssl-generator/ssl-tool/run_encryption.sh -subfoldername alfresco -servicename encryption -encstorepass mp6yc0UD9e -encmetadatapass oKIWzVdEdA -alfrescoformat $ALFRESCO_FORMAT +#T-Engine AIO +${GITHUB_WORKSPACE}/alfresco-ssl-generator/ssl-tool/run_additional.sh -servicename tengineAIO -rootcapass password -keysize 2048 -keystoretype JCEKS -keystorepass password -truststoretype JCEKS -truststorepass password -certdname "/C=GB/ST=UK/L=Maidenhead/O=Alfresco Software Ltd./OU=Unknown/CN=T-Engine AIO" -servername localhost -alfrescoformat $ALFRESCO_FORMAT \ No newline at end of file