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;