mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-24 17:32:48 +00:00
ACS-1217 Additional option for SOLR to authenticate with a shared secret (#334)
Co-authored-by: Alex Mukha <alex.mukha@alfresco.com>
This commit is contained in:
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
* #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 <http://www.gnu.org/licenses/>.
|
||||
* #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<byte[], AlgorithmParameters> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -66,6 +66,8 @@
|
||||
|
||||
<bean id="SOLRAuthenticationFilter" class="org.alfresco.repo.web.scripts.solr.SOLRAuthenticationFilter">
|
||||
<property name="secureComms" value="${solr.secureComms}"/>
|
||||
<property name="sharedSecret" value="${solr.sharedSecret}"/>
|
||||
<property name="sharedSecretHeader" value="${solr.sharedSecret.header}"/>
|
||||
</bean>
|
||||
|
||||
<bean id="WebscriptAuthenticationFilter" class="org.alfresco.repo.management.subsystems.ChainingSubsystemProxyFactory">
|
||||
|
@@ -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,
|
||||
|
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
* #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());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user