From b86c922c84172bb50cd941dadbaaa10111b0a3d1 Mon Sep 17 00:00:00 2001 From: Angel Borroy Date: Mon, 26 Apr 2021 13:44:27 +0200 Subject: [PATCH] SEARCH-2802: HttpClientFactory (for Repository and Search Services clients) support for Shared Secret communication. Cherry picked from fef8cc9 and 20dd0ef --- .../httpclient/HttpClientFactory.java | 216 ++++++++------ .../httpclient/RequestHeadersHttpClient.java | 87 ++++++ .../web-client-application-context.xml | 2 + .../solr/SOLRAuthenticationFilter.java | 272 +++++------------- .../servlet/AlfrescoX509ServletFilter.java | 5 +- .../web-client-application-context.xml | 2 + .../alfresco/AppContextExtraTestSuite.java | 1 + .../solr/SOLRAuthenticationFilterTest.java | 176 ++++++++++++ .../resources/alfresco/repository.properties | 2 + .../Search/solr6/solr-search-context.xml | 2 + .../solr/SolrStoreMappingWrapperTest.java | 26 +- 11 files changed, 499 insertions(+), 292 deletions(-) create mode 100644 core/src/main/java/org/alfresco/httpclient/RequestHeadersHttpClient.java create mode 100644 remote-api/src/test/java/org/alfresco/repo/web/scripts/solr/SOLRAuthenticationFilterTest.java diff --git a/core/src/main/java/org/alfresco/httpclient/HttpClientFactory.java b/core/src/main/java/org/alfresco/httpclient/HttpClientFactory.java index 45a70358e4..5ad7649446 100644 --- a/core/src/main/java/org/alfresco/httpclient/HttpClientFactory.java +++ b/core/src/main/java/org/alfresco/httpclient/HttpClientFactory.java @@ -21,7 +21,6 @@ package org.alfresco.httpclient; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.security.AlgorithmParameters; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -32,14 +31,11 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import org.alfresco.encryption.AlfrescoKeyStore; import org.alfresco.encryption.AlfrescoKeyStoreImpl; import org.alfresco.encryption.EncryptionUtils; -import org.alfresco.encryption.Encryptor; -import org.alfresco.encryption.KeyProvider; import org.alfresco.encryption.KeyResourceLoader; import org.alfresco.encryption.KeyStoreParameters; import org.alfresco.encryption.ssl.AuthSSLProtocolSocketFactory; import org.alfresco.encryption.ssl.SSLEncryptionParameters; import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.util.Pair; import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler; import org.apache.commons.httpclient.HostConfiguration; import org.apache.commons.httpclient.HttpClient; @@ -53,8 +49,6 @@ import org.apache.commons.httpclient.SimpleHttpConnectionManager; import org.apache.commons.httpclient.URI; import org.apache.commons.httpclient.URIException; import org.apache.commons.httpclient.cookie.CookiePolicy; -import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; -import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.params.DefaultHttpParams; import org.apache.commons.httpclient.params.DefaultHttpParamsFactory; import org.apache.commons.httpclient.params.HttpClientParams; @@ -75,23 +69,25 @@ import org.apache.commons.logging.LogFactory; */ public class HttpClientFactory { + /** + * Communication type for HttpClient: + * - NONE is plain http + * - SECRET is plain http with a shared secret via request header + * - HTTPS is mTLS with client authentication (certificates are required) + */ public static enum SecureCommsType { - HTTPS, NONE; + HTTPS, NONE, SECRET; public static SecureCommsType getType(String type) { - if(type.equalsIgnoreCase("https")) + switch (type.toLowerCase()) { - return HTTPS; - } - else if(type.equalsIgnoreCase("none")) - { - return NONE; - } - else - { - throw new IllegalArgumentException("Invalid communications type"); + case "https": return HTTPS; + case "none": return NONE; + case "secret": return SECRET; + default: throw new IllegalArgumentException("Invalid communications type"); + } } }; @@ -122,14 +118,24 @@ public class HttpClientFactory private int connectionTimeout = 0; + // Shared secret parameters + private String sharedSecret; + private String sharedSecretHeader = DEFAULT_SHAREDSECRET_HEADER; + + // Default name for HTTP Request Header when using shared secret communication + public static final String DEFAULT_SHAREDSECRET_HEADER = "X-Alfresco-Search-Secret"; + public HttpClientFactory() { } - + + /** + * Default constructor for legacy subsystems. + */ public HttpClientFactory(SecureCommsType secureCommsType, SSLEncryptionParameters sslEncryptionParameters, - KeyResourceLoader keyResourceLoader, KeyStoreParameters keyStoreParameters, - MD5EncryptionParameters encryptionParameters, String host, int port, int sslPort, int maxTotalConnections, - int maxHostConnections, int socketTimeout) + KeyResourceLoader keyResourceLoader, KeyStoreParameters keyStoreParameters, + MD5EncryptionParameters encryptionParameters, String host, int port, int sslPort, + int maxTotalConnections, int maxHostConnections, int socketTimeout) { this.secureCommsType = secureCommsType; this.sslEncryptionParameters = sslEncryptionParameters; @@ -145,6 +151,21 @@ public class HttpClientFactory init(); } + /** + * Recommended constructor for subsystems supporting Shared Secret communication. + * This constructor supports Shared Secret ("secret") communication method additionally to the legacy ones: "none" and "https". + */ + public HttpClientFactory(SecureCommsType secureCommsType, SSLEncryptionParameters sslEncryptionParameters, + KeyResourceLoader keyResourceLoader, KeyStoreParameters keyStoreParameters, + MD5EncryptionParameters encryptionParameters, String sharedSecret, String sharedSecretHeader, + String host, int port, int sslPort, int maxTotalConnections, int maxHostConnections, int socketTimeout) + { + this(secureCommsType, sslEncryptionParameters, keyResourceLoader, keyStoreParameters, encryptionParameters, + host, port, sslPort, maxTotalConnections, maxHostConnections, socketTimeout); + this.sharedSecret = sharedSecret; + this.sharedSecretHeader = sharedSecretHeader; + } + public void init() { this.sslKeyStore = new AlfrescoKeyStoreImpl(sslEncryptionParameters.getKeyStoreParameters(), keyResourceLoader); @@ -272,10 +293,44 @@ public class HttpClientFactory this.connectionTimeout = connectionTimeout; } - protected HttpClient constructHttpClient() + /** + * Shared secret used for SECRET communication + * @param secret shared secret word + */ + public void setSharedSecret(String sharedSecret) + { + this.sharedSecret = sharedSecret; + } + + /** + * @return Shared secret used for SECRET communication + */ + public String getSharedSecret() + { + return sharedSecret; + } + + /** + * HTTP Request header used for SECRET communication + * @param sharedSecretHeader HTTP Request header + */ + public void setSharedSecretHeader(String sharedSecretHeader) + { + this.sharedSecretHeader = sharedSecretHeader; + } + + /** + * @return HTTP Request header used for SECRET communication + */ + public String getSharedSecretHeader() + { + return sharedSecretHeader; + } + + protected RequestHeadersHttpClient constructHttpClient() { MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager(); - HttpClient httpClient = new HttpClient(connectionManager); + RequestHeadersHttpClient httpClient = new RequestHeadersHttpClient(connectionManager); HttpClientParams params = httpClient.getParams(); params.setBooleanParameter(HttpConnectionParams.TCP_NODELAY, true); params.setBooleanParameter(HttpConnectionParams.STALE_CONNECTION_CHECK, true); @@ -291,15 +346,15 @@ public class HttpClientFactory return httpClient; } - protected HttpClient getHttpsClient() + protected RequestHeadersHttpClient getHttpsClient() { return getHttpsClient(host, sslPort); } - protected HttpClient getHttpsClient(String httpsHost, int httpsPort) + protected RequestHeadersHttpClient getHttpsClient(String httpsHost, int httpsPort) { // Configure a custom SSL socket factory that will enforce mutual authentication - HttpClient httpClient = constructHttpClient(); + RequestHeadersHttpClient httpClient = constructHttpClient(); // Default port is 443 for the HostFactory, when including customised port (like 8983) the port name is skipped from "getHostURL" string HttpHostFactory hostFactory = new HttpHostFactory(new Protocol("https", sslSocketFactory, HttpsURL.DEFAULT_PORT)); httpClient.setHostConfiguration(new HostConfigurationWithHostFactory(hostFactory)); @@ -307,28 +362,54 @@ public class HttpClientFactory return httpClient; } - protected HttpClient getDefaultHttpClient() + protected RequestHeadersHttpClient getDefaultHttpClient() { return getDefaultHttpClient(host, port); } - protected HttpClient getDefaultHttpClient(String httpHost, int httpPort) + protected RequestHeadersHttpClient getDefaultHttpClient(String httpHost, int httpPort) { - HttpClient httpClient = constructHttpClient(); + RequestHeadersHttpClient httpClient = constructHttpClient(); httpClient.getHostConfiguration().setHost(httpHost, httpPort); return httpClient; } + + /** + * Build HTTP Client using default headers + * @return RequestHeadersHttpClient including default header for shared secret method + */ + protected RequestHeadersHttpClient constructSharedSecretHttpClient() + { + RequestHeadersHttpClient client = constructHttpClient(); + client.setDefaultHeaders(Map.of(sharedSecretHeader, sharedSecret)); + return client; + } + + protected RequestHeadersHttpClient getSharedSecretHttpClient() + { + return getSharedSecretHttpClient(host, port); + } + + protected RequestHeadersHttpClient getSharedSecretHttpClient(String httpHost, int httpPort) + { + RequestHeadersHttpClient httpClient = constructSharedSecretHttpClient(); + httpClient.getHostConfiguration().setHost(httpHost, httpPort); + return httpClient; + } protected AlfrescoHttpClient getAlfrescoHttpsClient() { - AlfrescoHttpClient repoClient = new HttpsClient(getHttpsClient()); - return repoClient; + return new HttpsClient(getHttpsClient()); } protected AlfrescoHttpClient getAlfrescoHttpClient() { - AlfrescoHttpClient repoClient = new DefaultHttpClient(getDefaultHttpClient()); - return repoClient; + return new DefaultHttpClient(getDefaultHttpClient()); + } + + protected AlfrescoHttpClient getAlfrescoSharedSecretClient() + { + return new DefaultHttpClient(getSharedSecretHttpClient()); } protected HttpClient getMD5HttpClient(String host, int port) @@ -341,66 +422,37 @@ public class HttpClientFactory public AlfrescoHttpClient getRepoClient(String host, int port) { - AlfrescoHttpClient repoClient = null; - - if(secureCommsType == SecureCommsType.HTTPS) + switch (secureCommsType) { - repoClient = getAlfrescoHttpsClient(); + case HTTPS: return getAlfrescoHttpsClient(); + case NONE: return getAlfrescoHttpClient(); + case SECRET: return getAlfrescoSharedSecretClient(); + default: throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in [solr|alfresco].secureComms, should be 'ssl', 'none' or 'secret'"); } - else if(secureCommsType == SecureCommsType.NONE) + } + + public RequestHeadersHttpClient getHttpClient() + { + switch (secureCommsType) { - repoClient = getAlfrescoHttpClient(); + case HTTPS: return getHttpsClient(); + case NONE: return getDefaultHttpClient(); + case SECRET: return getSharedSecretHttpClient(); + default: throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in [solr|alfresco].secureComms, should be 'ssl', 'none' or 'secret'"); } - else - { - throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in alfresco.secureComms, should be 'ssl'or 'none'"); - } - - return repoClient; } - public HttpClient getHttpClient() + public RequestHeadersHttpClient getHttpClient(String host, int port) { - HttpClient httpClient = null; - - if(secureCommsType == SecureCommsType.HTTPS) + switch (secureCommsType) { - httpClient = getHttpsClient(); + case HTTPS: return getHttpsClient(host, port); + case NONE: return getDefaultHttpClient(host, port); + case SECRET: return getSharedSecretHttpClient(host, port); + default: throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in [solr|alfresco].secureComms, should be 'ssl', 'none' or 'secret'"); } - else if(secureCommsType == SecureCommsType.NONE) - { - httpClient = getDefaultHttpClient(); - } - else - { - throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in alfresco.secureComms, should be 'ssl'or 'none'"); - } - - return httpClient; } - public HttpClient getHttpClient(String host, int port) - { - HttpClient httpClient = null; - - if(secureCommsType == SecureCommsType.HTTPS) - { - httpClient = getHttpsClient(host, port); - } - else if(secureCommsType == SecureCommsType.NONE) - { - httpClient = getDefaultHttpClient(host, port); - } - else - { - throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in alfresco.secureComms, should be 'ssl'or 'none'"); - } - - return httpClient; - } - - - /** * A secure client connection to the repository. * diff --git a/core/src/main/java/org/alfresco/httpclient/RequestHeadersHttpClient.java b/core/src/main/java/org/alfresco/httpclient/RequestHeadersHttpClient.java new file mode 100644 index 0000000000..78e5d36727 --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/RequestHeadersHttpClient.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2005-2021 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.io.IOException; +import java.util.Map; + +import org.apache.commons.httpclient.HostConfiguration; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.HttpState; +import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; + +/** + * Since Apache HttpClient 3.1 doesn't support including custom headers by default, + * this class is adding that custom headers every time a method is invoked. + */ +public class RequestHeadersHttpClient extends HttpClient +{ + + private Map defaultHeaders; + + public RequestHeadersHttpClient(MultiThreadedHttpConnectionManager connectionManager) + { + super(connectionManager); + } + + public Map getDefaultHeaders() + { + return defaultHeaders; + } + + public void setDefaultHeaders(Map defaultHeaders) + { + this.defaultHeaders = defaultHeaders; + } + + private void addDefaultHeaders(HttpMethod method) + { + if (defaultHeaders != null) + { + defaultHeaders.forEach((k,v) -> { + method.addRequestHeader(k, v); + }); + } + } + + @Override + public int executeMethod(HttpMethod method) throws IOException, HttpException + { + addDefaultHeaders(method); + return super.executeMethod(method); + } + + @Override + public int executeMethod(HostConfiguration hostConfiguration, HttpMethod method) throws IOException, HttpException + { + addDefaultHeaders(method); + return super.executeMethod(hostConfiguration, method); + } + + @Override + public int executeMethod(HostConfiguration hostconfig, HttpMethod method, HttpState state) + throws IOException, HttpException + { + addDefaultHeaders(method); + return super.executeMethod(hostconfig, method, state); + } + +} diff --git a/packaging/war/src/main/resources/alfresco/web-client-application-context.xml b/packaging/war/src/main/resources/alfresco/web-client-application-context.xml index 15ac443933..5303596137 100644 --- a/packaging/war/src/main/resources/alfresco/web-client-application-context.xml +++ b/packaging/war/src/main/resources/alfresco/web-client-application-context.xml @@ -66,6 +66,8 @@ + + diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/SOLRAuthenticationFilter.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/SOLRAuthenticationFilter.java index 990e7054a2..bbce3d1b86 100644 --- a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/SOLRAuthenticationFilter.java +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/SOLRAuthenticationFilter.java @@ -1,62 +1,61 @@ -/* - * #%L - * Alfresco Remote API - * %% - * Copyright (C) 2005 - 2016 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% - */ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 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.web.scripts.solr; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.PrintWriter; import javax.servlet.FilterChain; import javax.servlet.ServletContext; import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.httpclient.HttpClientFactory; import org.alfresco.repo.web.filter.beans.DependencyInjectedFilter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; /** - * This filter protects the solr callback urls by verifying MACs on requests and encrypting responses - * and generating MACs on responses, if the secureComms property is set to "md5". If it is set to "https" - * or "none", the filter does nothing to the request and response. - * + * This filter protects the solr callback urls by verifying a shared secret on the request header if + * the secureComms property is set to "secret". If it is set to "https", this will will just verify + * that the request came in through a "secure" tomcat connector. (but it will not validate the certificate + * on the request; this done in a different filter). + * * @since 4.0 * */ -public class SOLRAuthenticationFilter implements DependencyInjectedFilter +public class SOLRAuthenticationFilter implements DependencyInjectedFilter, InitializingBean { public static enum SecureCommsType { - HTTPS, NONE; + HTTPS, SECRET, NONE; public static SecureCommsType getType(String type) { @@ -64,6 +63,10 @@ public class SOLRAuthenticationFilter implements DependencyInjectedFilter { return HTTPS; } + else if(type.equalsIgnoreCase("secret")) + { + return SECRET; + } else if(type.equalsIgnoreCase("none")) { return NONE; @@ -79,7 +82,11 @@ public class SOLRAuthenticationFilter implements DependencyInjectedFilter private static Log logger = LogFactory.getLog(SOLRAuthenticationFilter.class); private SecureCommsType secureComms = SecureCommsType.HTTPS; - + + private String sharedSecret; + + private String sharedSecretHeader = HttpClientFactory.DEFAULT_SHAREDSECRET_HEADER; + public void setSecureComms(String type) { try @@ -92,6 +99,33 @@ public class SOLRAuthenticationFilter implements DependencyInjectedFilter } } + public void setSharedSecret(String sharedSecret) + { + this.sharedSecret = sharedSecret; + } + + public void setSharedSecretHeader(String sharedSecretHeader) + { + this.sharedSecretHeader = sharedSecretHeader; + } + + @Override + public void afterPropertiesSet() throws Exception + { + if(secureComms == SecureCommsType.SECRET) + { + if(sharedSecret == null || sharedSecret.length()==0) + { + logger.fatal("Missing value for solr.sharedSecret configuration property. If solr.secureComms is set to \"secret\", a value for solr.sharedSecret is required. See https://docs.alfresco.com/search-services/latest/install/options/"); + throw new AlfrescoRuntimeException("Missing value for solr.sharedSecret configuration property"); + } + if(sharedSecretHeader == null || sharedSecretHeader.length()==0) + { + throw new AlfrescoRuntimeException("Missing value for sharedSecretHeader"); + } + } + } + public void doFilter(ServletContext context, ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException @@ -99,52 +133,22 @@ public class SOLRAuthenticationFilter implements DependencyInjectedFilter HttpServletRequest httpRequest = (HttpServletRequest)request; HttpServletResponse httpResponse = (HttpServletResponse)response; -/* if(secureComms == SecureCommsType.ALFRESCO) + if(secureComms == SecureCommsType.SECRET) { - // Need to get as a byte array because we need to read the request twice, once for authentication - // and again by the web service. - SOLRHttpServletRequestWrapper requestWrapper = new SOLRHttpServletRequestWrapper(httpRequest, encryptionUtils); - - if(logger.isDebugEnabled()) + if(sharedSecret.equals(httpRequest.getHeader(sharedSecretHeader))) { - logger.debug("Authenticating " + httpRequest.getRequestURI()); - } - - if(encryptionUtils.authenticate(httpRequest, requestWrapper.getDecryptedBody())) - { - try - { - OutputStream out = response.getOutputStream(); - - GenericResponseWrapper responseWrapper = new GenericResponseWrapper(httpResponse); - - // TODO - do I need to chain to other authenticating filters - probably not? - // Could also remove sending of credentials with http request - chain.doFilter(requestWrapper, responseWrapper); - - Pair pair = encryptor.encrypt(KeyProvider.ALIAS_SOLR, null, responseWrapper.getData()); - - encryptionUtils.setResponseAuthentication(httpRequest, httpResponse, responseWrapper.getData(), pair.getSecond()); - - httpResponse.setHeader("Content-Length", Long.toString(pair.getFirst().length)); - out.write(pair.getFirst()); - out.close(); - } - catch(Exception e) - { - throw new AlfrescoRuntimeException("", e); - } + chain.doFilter(request, response); } else { - httpResponse.setStatus(401); + httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Authentication failure"); } } - else */if(secureComms == SecureCommsType.HTTPS) + else if(secureComms == SecureCommsType.HTTPS) { if(httpRequest.isSecure()) { - // https authentication + // https authentication; cert got verified in X509 filter chain.doFilter(request, response); } else @@ -158,128 +162,4 @@ public class SOLRAuthenticationFilter implements DependencyInjectedFilter } } - protected boolean validateTimestamp(String timestampStr) - { - if(timestampStr == null || timestampStr.equals("")) - { - throw new AlfrescoRuntimeException("Missing timestamp on request"); - } - long timestamp = -1; - try - { - timestamp = Long.valueOf(timestampStr); - } - catch(NumberFormatException e) - { - throw new AlfrescoRuntimeException("Invalid timestamp on request"); - } - if(timestamp == -1) - { - throw new AlfrescoRuntimeException("Invalid timestamp on request"); - } - long currentTime = System.currentTimeMillis(); - return((currentTime - timestamp) < 30 * 1000); // 5s - } - -/* private static class SOLRHttpServletRequestWrapper extends HttpServletRequestWrapper - { - private byte[] body; - - SOLRHttpServletRequestWrapper(HttpServletRequest req, EncryptionUtils encryptionUtils) throws IOException - { - super(req); - this.body = encryptionUtils.decryptBody(req); - } - - byte[] getDecryptedBody() - { - return body; - } - - public ServletInputStream getInputStream() - { - final InputStream in = (body != null ? new ByteArrayInputStream(body) : null); - return new ServletInputStream() - { - public int read() throws IOException - { - if(in == null) - { - return -1; - } - else - { - int i = in.read(); - if(i == -1) - { - in.close(); - } - return i; - } - } - }; - } - }*/ - - private static class ByteArrayServletOutputStream extends ServletOutputStream - { - private ByteArrayOutputStream out = new ByteArrayOutputStream(); - - ByteArrayServletOutputStream() - { - } - - public byte[] getData() - { - return out.toByteArray(); - } - - @Override - public void write(int b) throws IOException - { - out.write(b); - } - } - - public static class GenericResponseWrapper extends HttpServletResponseWrapper { - private ByteArrayServletOutputStream output; - private int contentLength; - private String contentType; - - public GenericResponseWrapper(HttpServletResponse response) { - super(response); - output = new ByteArrayServletOutputStream(); - } - - public byte[] getData() { - return output.getData(); - } - - public ServletOutputStream getOutputStream() { - return output; - } - - public PrintWriter getWriter() { - return new PrintWriter(getOutputStream(),true); - } - - public void setContentLength(int length) { - this.contentLength = length; - super.setContentLength(length); - } - - public int getContentLength() { - return contentLength; - } - - public void setContentType(String type) { - this.contentType = type; - super.setContentType(type); - } - - - public String getContentType() { - return contentType; - } - } } diff --git a/remote-api/src/main/java/org/alfresco/web/app/servlet/AlfrescoX509ServletFilter.java b/remote-api/src/main/java/org/alfresco/web/app/servlet/AlfrescoX509ServletFilter.java index f433c0dbf2..77399944b6 100644 --- a/remote-api/src/main/java/org/alfresco/web/app/servlet/AlfrescoX509ServletFilter.java +++ b/remote-api/src/main/java/org/alfresco/web/app/servlet/AlfrescoX509ServletFilter.java @@ -31,6 +31,7 @@ import java.util.Properties; import javax.servlet.ServletContext; +import org.alfresco.httpclient.HttpClientFactory.SecureCommsType; import org.alfresco.web.scripts.servlet.X509ServletFilterBase; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -70,7 +71,9 @@ public class AlfrescoX509ServletFilter extends X509ServletFilterBase * Return true or false based on the property. This will switch on/off X509 enforcement in the X509ServletFilterBase. */ - if (prop == null || "none".equals(prop)) + if (prop == null || + SecureCommsType.getType(prop) == SecureCommsType.NONE || + SecureCommsType.getType(prop) == SecureCommsType.SECRET) { return false; } diff --git a/remote-api/src/main/resources/alfresco/web-client-application-context.xml b/remote-api/src/main/resources/alfresco/web-client-application-context.xml index 557e7a69db..ad7cb9dbf1 100644 --- a/remote-api/src/main/resources/alfresco/web-client-application-context.xml +++ b/remote-api/src/main/resources/alfresco/web-client-application-context.xml @@ -66,6 +66,8 @@ + + diff --git a/remote-api/src/test/java/org/alfresco/AppContextExtraTestSuite.java b/remote-api/src/test/java/org/alfresco/AppContextExtraTestSuite.java index b541224cca..2452955e0e 100644 --- a/remote-api/src/test/java/org/alfresco/AppContextExtraTestSuite.java +++ b/remote-api/src/test/java/org/alfresco/AppContextExtraTestSuite.java @@ -39,6 +39,7 @@ import org.junit.runners.Suite; org.alfresco.repo.web.scripts.workflow.WorkflowModelBuilderTest.class, org.alfresco.repo.web.scripts.solr.StatsGetTest.class, org.alfresco.repo.web.scripts.solr.SOLRSerializerTest.class, + org.alfresco.repo.web.scripts.solr.SOLRAuthenticationFilterTest.class, org.alfresco.repo.web.util.PagingCursorTest.class, org.alfresco.repo.web.util.paging.PagingTest.class, org.alfresco.repo.webdav.GetMethodTest.class, diff --git a/remote-api/src/test/java/org/alfresco/repo/web/scripts/solr/SOLRAuthenticationFilterTest.java b/remote-api/src/test/java/org/alfresco/repo/web/scripts/solr/SOLRAuthenticationFilterTest.java new file mode 100644 index 0000000000..073ec05c1e --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/repo/web/scripts/solr/SOLRAuthenticationFilterTest.java @@ -0,0 +1,176 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2021 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.web.scripts.solr; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import javax.servlet.FilterChain; +import javax.servlet.ServletContext; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static org.junit.Assert.assertEquals; + +public class SOLRAuthenticationFilterTest +{ + @Test(expected = AlfrescoRuntimeException.class) + public void testSharedSecretNotConfigured() throws Exception + { + SOLRAuthenticationFilter filter = new SOLRAuthenticationFilter(); + filter.setSecureComms(SOLRAuthenticationFilter.SecureCommsType.SECRET.name()); + filter.afterPropertiesSet(); + } + + @Test(expected = AlfrescoRuntimeException.class) + public void testSharedHeaderNotConfigured() throws Exception + { + SOLRAuthenticationFilter filter = new SOLRAuthenticationFilter(); + filter.setSecureComms(SOLRAuthenticationFilter.SecureCommsType.SECRET.name()); + filter.setSharedSecret("shared-secret"); + filter.setSharedSecretHeader(""); + filter.afterPropertiesSet(); + } + + @Test + public void testHTTPSFilterAndSharedSecretSet() throws Exception + { + String headerKey = "test-header"; + String sharedSecret = "shared-secret"; + SOLRAuthenticationFilter filter = new SOLRAuthenticationFilter(); + filter.setSecureComms(SOLRAuthenticationFilter.SecureCommsType.HTTPS.name()); + filter.setSharedSecret(sharedSecret); + filter.setSharedSecretHeader(headerKey); + filter.afterPropertiesSet(); + + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + Mockito.when(request.getHeader(headerKey)).thenReturn(sharedSecret); + Mockito.when(request.isSecure()).thenReturn(true); + + FilterChain chain = Mockito.mock(FilterChain.class); + + filter.doFilter(Mockito.mock(ServletContext.class), request, response, chain); + Mockito.verify(chain, Mockito.times(1)).doFilter(request, response); + } + + @Test(expected = AlfrescoRuntimeException.class) + public void testHTTPSFilterAndInsecureRequest() throws Exception + { + SOLRAuthenticationFilter filter = new SOLRAuthenticationFilter(); + filter.setSecureComms(SOLRAuthenticationFilter.SecureCommsType.HTTPS.name()); + filter.afterPropertiesSet(); + + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + Mockito.when(request.isSecure()).thenReturn(false); + + FilterChain chain = Mockito.mock(FilterChain.class); + + filter.doFilter(Mockito.mock(ServletContext.class), request, response, chain); + } + + @Test + public void testNoAuthentication() throws Exception + { + SOLRAuthenticationFilter filter = new SOLRAuthenticationFilter(); + filter.setSecureComms(SOLRAuthenticationFilter.SecureCommsType.NONE.name()); + filter.afterPropertiesSet(); + + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + + FilterChain chain = Mockito.mock(FilterChain.class); + + filter.doFilter(Mockito.mock(ServletContext.class), request, response, chain); + Mockito.verify(chain, Mockito.times(1)).doFilter(request, response); + } + + @Test + public void testSharedSecretFilter() throws Exception + { + String headerKey = "test-header"; + String sharedSecret = "shared-secret"; + SOLRAuthenticationFilter filter = new SOLRAuthenticationFilter(); + filter.setSecureComms(SOLRAuthenticationFilter.SecureCommsType.SECRET.name()); + filter.setSharedSecret(sharedSecret); + filter.setSharedSecretHeader(headerKey); + filter.afterPropertiesSet(); + + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + Mockito.when(request.getHeader(headerKey)).thenReturn(sharedSecret); + + FilterChain chain = Mockito.mock(FilterChain.class); + + filter.doFilter(Mockito.mock(ServletContext.class), request, response, chain); + Mockito.verify(chain, Mockito.times(1)).doFilter(request, response); + } + + @Test + public void testSharedSecretDontMatch() throws Exception + { + String headerKey = "test-header"; + String sharedSecret = "shared-secret"; + SOLRAuthenticationFilter filter = new SOLRAuthenticationFilter(); + filter.setSecureComms(SOLRAuthenticationFilter.SecureCommsType.SECRET.name()); + filter.setSharedSecret(sharedSecret); + filter.setSharedSecretHeader(headerKey); + filter.afterPropertiesSet(); + + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + Mockito.when(request.getHeader(headerKey)).thenReturn("wrong-secret"); + + FilterChain chain = Mockito.mock(FilterChain.class); + + filter.doFilter(Mockito.mock(ServletContext.class), request, response, chain); + Mockito.verify(chain, Mockito.times(0)).doFilter(request, response); + Mockito.verify(response).sendError(Mockito.eq(HttpServletResponse.SC_FORBIDDEN), Mockito.anyString()); + } + + @Test + public void testSharedHeaderNotPresent() throws Exception + { + String headerKey = "test-header"; + String sharedSecret = "shared-secret"; + SOLRAuthenticationFilter filter = new SOLRAuthenticationFilter(); + filter.setSecureComms(SOLRAuthenticationFilter.SecureCommsType.SECRET.name()); + filter.setSharedSecret(sharedSecret); + filter.setSharedSecretHeader(headerKey); + filter.afterPropertiesSet(); + + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + + FilterChain chain = Mockito.mock(FilterChain.class); + + filter.doFilter(Mockito.mock(ServletContext.class), request, response, chain); + Mockito.verify(chain, Mockito.times(0)).doFilter(request, response); + Mockito.verify(response).sendError(Mockito.eq(HttpServletResponse.SC_FORBIDDEN), Mockito.anyString()); + } +} diff --git a/repository/src/main/resources/alfresco/repository.properties b/repository/src/main/resources/alfresco/repository.properties index 1b5318d10b..ede0b70d43 100644 --- a/repository/src/main/resources/alfresco/repository.properties +++ b/repository/src/main/resources/alfresco/repository.properties @@ -864,6 +864,8 @@ solr.solrUser=solr solr.solrPassword=solr # none, https solr.secureComms=https +solr.sharedSecret= +solr.sharedSecret.header=X-Alfresco-Search-Secret solr.cmis.alternativeDictionary=DEFAULT_DICTIONARY solr.max.total.connections=40 diff --git a/repository/src/main/resources/alfresco/subsystems/Search/solr6/solr-search-context.xml b/repository/src/main/resources/alfresco/subsystems/Search/solr6/solr-search-context.xml index acc0e055a1..579394fd6e 100644 --- a/repository/src/main/resources/alfresco/subsystems/Search/solr6/solr-search-context.xml +++ b/repository/src/main/resources/alfresco/subsystems/Search/solr6/solr-search-context.xml @@ -161,6 +161,8 @@ + + diff --git a/repository/src/test/java/org/alfresco/repo/search/impl/solr/SolrStoreMappingWrapperTest.java b/repository/src/test/java/org/alfresco/repo/search/impl/solr/SolrStoreMappingWrapperTest.java index db668a7951..e80af814e0 100644 --- a/repository/src/test/java/org/alfresco/repo/search/impl/solr/SolrStoreMappingWrapperTest.java +++ b/repository/src/test/java/org/alfresco/repo/search/impl/solr/SolrStoreMappingWrapperTest.java @@ -31,14 +31,16 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import java.io.UnsupportedEncodingException; + import org.alfresco.httpclient.HttpClientFactory; +import org.alfresco.httpclient.RequestHeadersHttpClient; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.util.Pair; import org.apache.commons.codec.net.URLCodec; import org.apache.commons.httpclient.HostConfiguration; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.protocol.Protocol; -import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -46,8 +48,6 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.beans.factory.BeanFactory; -import java.io.UnsupportedEncodingException; - /** * @author Andy * @@ -64,34 +64,34 @@ public class SolrStoreMappingWrapperTest HttpClientFactory httpClientFactory; @Mock - HttpClient httpClientCommon; + RequestHeadersHttpClient httpClientCommon; @Mock - HttpClient httpClient1; + RequestHeadersHttpClient httpClient1; @Mock - HttpClient httpClient2; + RequestHeadersHttpClient httpClient2; @Mock - HttpClient httpClient3; + RequestHeadersHttpClient httpClient3; @Mock - HttpClient httpClient4; + RequestHeadersHttpClient httpClient4; @Mock - HttpClient httpClient5; + RequestHeadersHttpClient httpClient5; @Mock - HttpClient httpClient6; + RequestHeadersHttpClient httpClient6; @Mock - HttpClient httpClient7; + RequestHeadersHttpClient httpClient7; @Mock - HttpClient httpClient8; + RequestHeadersHttpClient httpClient8; @Mock - HttpClient httpClient9; + RequestHeadersHttpClient httpClient9; @Mock HostConfiguration hostConfigurationCommon;