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..c67b7c5beb 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,28 +1,28 @@
-/*
- * #%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;
@@ -43,20 +43,22 @@ import org.alfresco.error.AlfrescoRuntimeException;
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 +66,10 @@ public class SOLRAuthenticationFilter implements DependencyInjectedFilter
{
return HTTPS;
}
+ else if(type.equalsIgnoreCase("secret"))
+ {
+ return SECRET;
+ }
else if(type.equalsIgnoreCase("none"))
{
return NONE;
@@ -79,7 +85,13 @@ public class SOLRAuthenticationFilter implements DependencyInjectedFilter
private static Log logger = LogFactory.getLog(SOLRAuthenticationFilter.class);
private SecureCommsType secureComms = SecureCommsType.HTTPS;
-
+
+ private String sharedSecret;
+
+ private String sharedSecretHeader = DEFAULT_SHAREDSECRET_HEADER;
+
+ private static final String DEFAULT_SHAREDSECRET_HEADER = "X-Alfresco-Search-Secret";
+
public void setSecureComms(String type)
{
try
@@ -92,6 +104,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 +138,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 +167,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/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 af8f411ce3..887b39a068 100644
--- a/repository/src/main/resources/alfresco/repository.properties
+++ b/repository/src/main/resources/alfresco/repository.properties
@@ -747,6 +747,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