diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/AlfrescoCoreAdminHandler.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/AlfrescoCoreAdminHandler.java index 83eeeeeaa..cdd8211ba 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/AlfrescoCoreAdminHandler.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/AlfrescoCoreAdminHandler.java @@ -32,6 +32,8 @@ import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.solr.adapters.IOpenBitSet; import org.alfresco.solr.client.SOLRAPIClientFactory; import org.alfresco.solr.config.ConfigUtil; +import org.alfresco.solr.io.interceptor.SharedSecretRequestInterceptor; +import org.alfresco.solr.security.SecretSharedPropertyCollector; import org.alfresco.solr.tracker.AclTracker; import org.alfresco.solr.tracker.ActivatableTracker; import org.alfresco.solr.tracker.ShardStatePublisher; @@ -46,6 +48,7 @@ import org.alfresco.solr.utils.Utils; import org.alfresco.util.Pair; import org.alfresco.util.shard.ExplicitShardingPolicy; import org.apache.commons.io.FileUtils; +import org.apache.http.HttpRequestInterceptor; import org.apache.solr.common.SolrException; import org.apache.solr.common.params.CoreAdminParams; import org.apache.solr.common.params.SolrParams; @@ -103,6 +106,7 @@ import static org.alfresco.solr.HandlerReportHelper.buildAclTxReport; import static org.alfresco.solr.HandlerReportHelper.buildNodeReport; import static org.alfresco.solr.HandlerReportHelper.buildTrackerReport; import static org.alfresco.solr.HandlerReportHelper.buildTxReport; +import static org.alfresco.solr.InterceptorRegistry.registerSolrClientInterceptors; import static org.alfresco.solr.utils.Utils.isNotNullAndNotEmpty; import static org.alfresco.solr.utils.Utils.isNullOrEmpty; import static org.alfresco.solr.utils.Utils.notNullOrEmpty; @@ -219,6 +223,9 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler String createDefaultCores = ConfigUtil.locateProperty(ALFRESCO_DEFAULTS, ""); int numShards = Integer.parseInt(ConfigUtil.locateProperty(NUM_SHARDS, "1")); String shardIds = ConfigUtil.locateProperty(SHARD_IDS, null); + registerSolrClientInterceptors(); + + if (createDefaultCores != null && !createDefaultCores.isEmpty()) { Thread thread = new Thread(() -> @@ -230,6 +237,7 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler } } + /** * Creates new default cores based on the "createDefaultCores" String passed in. * @@ -2230,4 +2238,4 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler return response; } -} \ No newline at end of file +} diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/InterceptorRegistry.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/InterceptorRegistry.java new file mode 100644 index 000000000..7b95fe535 --- /dev/null +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/InterceptorRegistry.java @@ -0,0 +1,57 @@ +/* + * #%L + * Alfresco Search Services + * %% + * Copyright (C) 2005 - 2022 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.solr; + +import org.alfresco.solr.io.interceptor.SharedSecretRequestInterceptor; +import org.alfresco.solr.security.SecretSharedPropertyCollector; +import org.apache.http.HttpRequestInterceptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class InterceptorRegistry +{ + protected static final Logger LOGGER = LoggerFactory.getLogger(InterceptorRegistry.class); + /** + * Register the required {@link HttpRequestInterceptor}s + */ + public static void registerSolrClientInterceptors() + { + try + { + if (SecretSharedPropertyCollector.isCommsSecretShared()) + { + SharedSecretRequestInterceptor.register(); + } + } + catch (Throwable t) + { + LOGGER.warn("It was not possible to add the Shared Secret Authentication interceptor. " + + "Please make sure to pass the required -Dalfresco.secureComms=secret and " + + "-Dalfresco.secureComms.secret=my-secret-value JVM args if trying to use Secret Authentication with Solr."); + } + } +} diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/io/interceptor/SharedSecretRequestInterceptor.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/io/interceptor/SharedSecretRequestInterceptor.java new file mode 100644 index 000000000..08efab006 --- /dev/null +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/io/interceptor/SharedSecretRequestInterceptor.java @@ -0,0 +1,98 @@ +/* + * #%L + * Alfresco Search Services + * %% + * Copyright (C) 2005 - 2022 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.solr.io.interceptor; + +import java.io.IOException; + +import org.alfresco.solr.security.SecretSharedPropertyCollector; +import org.apache.http.HttpException; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.message.BasicHeader; +import org.apache.http.protocol.HttpContext; +import org.apache.solr.client.solrj.impl.HttpClientUtil; + +/** + * This HttpRequestInterceptor adds the header that is required for Shared Secret Authentication with Solr + * + * @author Domenico Sibilio + */ +public class SharedSecretRequestInterceptor implements HttpRequestInterceptor +{ + + private static volatile SharedSecretRequestInterceptor INSTANCE; + + private SharedSecretRequestInterceptor() + { + } + + /** + * A typical thread-safe singleton implementation + * @return The unique instance of this class + */ + public static SharedSecretRequestInterceptor getInstance() + { + if (INSTANCE == null) + { + synchronized (SharedSecretRequestInterceptor.class) + { + if (INSTANCE == null) + { + INSTANCE = new SharedSecretRequestInterceptor(); + } + } + } + + return INSTANCE; + } + + /** + * Decorates the enclosing request with the Shared Secret Authentication header + * @param httpRequest + * @param httpContext + * @throws HttpException + * @throws IOException + */ + @Override + public void process(HttpRequest httpRequest, HttpContext httpContext) + throws HttpException, IOException + { + String secretName = SecretSharedPropertyCollector.getSecretHeader(); + String secretValue = SecretSharedPropertyCollector.getSecret(); + httpRequest.addHeader(new BasicHeader(secretName, secretValue)); + } + + /** + * Utility method to register the unique instance of this {@link HttpRequestInterceptor} + */ + public static void register() + { + HttpClientUtil.removeRequestInterceptor(getInstance()); + HttpClientUtil.addRequestInterceptor(getInstance()); + } + +} \ No newline at end of file diff --git a/search-services/alfresco-search/src/test/java/org/alfresco/solr/io/interceptor/SharedSecretRequestInterceptorTest.java b/search-services/alfresco-search/src/test/java/org/alfresco/solr/io/interceptor/SharedSecretRequestInterceptorTest.java new file mode 100644 index 000000000..56bbf7c8b --- /dev/null +++ b/search-services/alfresco-search/src/test/java/org/alfresco/solr/io/interceptor/SharedSecretRequestInterceptorTest.java @@ -0,0 +1,136 @@ +/* + * #%L + * Alfresco Search Services + * %% + * Copyright (C) 2005 - 2022 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.solr.io.interceptor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +import java.util.stream.IntStream; + +import org.alfresco.httpclient.HttpClientFactory; +import org.apache.http.Header; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.impl.client.SystemDefaultHttpClient; +import org.apache.http.message.BasicHttpRequest; +import org.apache.solr.client.solrj.impl.HttpClientUtil; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit tests for {@link SharedSecretRequestInterceptor}. + */ +public class SharedSecretRequestInterceptorTest +{ + + private static final String SECRET_HEADER_PROPERTY = "alfresco.secureComms.secret.header"; + private static final String SECRET_HEADER_VALUE = "X-My-Secret-Header"; + private static final String SECRET_PROPERTY = "alfresco.secureComms.secret"; + private static final String SECRET_VALUE = "my-secret"; + + @Before + public void setUp() + { + System.clearProperty(SECRET_HEADER_PROPERTY); + System.clearProperty(SECRET_PROPERTY); + } + + @Test + public void theInterceptor_shouldBeSingleton() + { + SharedSecretRequestInterceptor interceptor1 = SharedSecretRequestInterceptor.getInstance(); + SharedSecretRequestInterceptor interceptor2 = SharedSecretRequestInterceptor.getInstance(); + + assertSame("There should only be one instance of the interceptor.", interceptor1, interceptor2); + } + + @Test + public void registeringTheInterceptor_shouldAddOneInterceptor() + { + SharedSecretRequestInterceptor.register(); + + SystemDefaultHttpClient client = (SystemDefaultHttpClient) HttpClientUtil.createClient(null); + long sharedSecretInterceptorsCount = getSharedSecretInterceptorsCount(client); + + assertEquals("There should be one Shared Secret request interceptor.", 1, sharedSecretInterceptorsCount); + } + + @Test + public void registeringTheInterceptorMultipleTimes_shouldAddOnlyOneInterceptor() + { + IntStream.range(0, 5).forEach(i -> SharedSecretRequestInterceptor.register()); + + SystemDefaultHttpClient client = (SystemDefaultHttpClient) HttpClientUtil.createClient(null); + long sharedSecretInterceptorsCount = getSharedSecretInterceptorsCount(client); + + assertEquals("There should be only one Shared Secret request interceptor.", 1, sharedSecretInterceptorsCount); + } + + @Test + public void requestProcessing_shouldAddDefaultSecretHeaderToOutgoingRequests() throws Exception + { + System.setProperty(SECRET_PROPERTY, SECRET_VALUE); + BasicHttpRequest httpRequest = new BasicHttpRequest("", ""); + + SharedSecretRequestInterceptor.getInstance().process(httpRequest, null); + Header[] headers = httpRequest.getHeaders(HttpClientFactory.DEFAULT_SHAREDSECRET_HEADER); + + assertEquals("There should be only one secret header.", 1, headers.length); + assertEquals("The secret header should have the expected value.", SECRET_VALUE, headers[0].getValue()); + } + + @Test + public void requestProcessing_shouldAddCustomSecretHeaderToOutgoingRequests() throws Exception + { + System.setProperty(SECRET_HEADER_PROPERTY, SECRET_HEADER_VALUE); + System.setProperty(SECRET_PROPERTY, SECRET_VALUE); + BasicHttpRequest httpRequest = new BasicHttpRequest("", ""); + + SharedSecretRequestInterceptor.getInstance().process(httpRequest, null); + Header[] headers = httpRequest.getHeaders(SECRET_HEADER_VALUE); + + assertEquals("There should be only one secret header.", 1, headers.length); + assertEquals("The secret header should have the expected value.", SECRET_VALUE, headers[0].getValue()); + } + + @Test(expected = RuntimeException.class) + public void requestProcessing_shouldFailWhenMissingSecretValue() throws Exception + { + BasicHttpRequest httpRequest = new BasicHttpRequest("", ""); + + SharedSecretRequestInterceptor.getInstance().process(httpRequest, null); + } + + private static long getSharedSecretInterceptorsCount(SystemDefaultHttpClient client) + { + return IntStream.range(0, client.getRequestInterceptorCount()) + .mapToObj(client::getRequestInterceptor) + .map(HttpRequestInterceptor::getClass) + .filter(clazz -> clazz == SharedSecretRequestInterceptor.class) + .count(); + } + +} \ No newline at end of file