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