From 4112979808a39f4dd7190edcf58194337625d68d Mon Sep 17 00:00:00 2001 From: Noble Paul Date: Wed, 5 Aug 2015 13:59:14 +0000 Subject: [PATCH] SOLR-7849: Solr-managed inter-node authentication when authentication enabled git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1694217 13f79535-47bb-0310-9956-ffa450edef68 --- solr/CHANGES.txt | 2 + .../org/apache/solr/core/CoreContainer.java | 30 +- .../apache/solr/request/SolrQueryRequest.java | 3 + .../solr/request/SolrQueryRequestBase.java | 5 + .../solr/security/AuthenticationPlugin.java | 32 +- .../security/HttpClientInterceptorPlugin.java | 32 ++ .../apache/solr/security/KerberosPlugin.java | 4 +- .../security/PKIAuthenticationPlugin.java | 299 ++++++++++++++++++ .../org/apache/solr/servlet/HttpSolrCall.java | 12 +- .../solr/servlet/SolrDispatchFilter.java | 36 +-- .../solr/servlet/SolrRequestParsers.java | 23 +- .../java/org/apache/solr/util/CryptoKeys.java | 91 ++++++ .../cloud/TestAuthenticationFramework.java | 5 +- .../security/MockAuthenticationPlugin.java | 54 ++++ .../security/MockAuthorizationPlugin.java | 26 +- .../PKIAuthenticationIntegrationTest.java | 115 +++++++ .../security/TestAuthorizationFramework.java | 9 + .../security/TestPKIAuthenticationPlugin.java | 173 ++++++++++ .../org/apache/solr/common/util/Base64.java | 4 + .../apache/solr/common/util/ExecutorUtil.java | 9 + 20 files changed, 906 insertions(+), 58 deletions(-) create mode 100644 solr/core/src/java/org/apache/solr/security/HttpClientInterceptorPlugin.java create mode 100644 solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java create mode 100644 solr/core/src/test/org/apache/solr/security/MockAuthenticationPlugin.java create mode 100644 solr/core/src/test/org/apache/solr/security/PKIAuthenticationIntegrationTest.java create mode 100644 solr/core/src/test/org/apache/solr/security/TestPKIAuthenticationPlugin.java diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index 5359a111f91..2857d2a2948 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -183,6 +183,8 @@ New Features * SOLR-7766: support creation of a coreless collection via createNodeSet=EMPTY (Christine Poerschke) +* SOLR-7849: Solr-managed inter-node authentication when authentication enabled (Noble Paul) + Bug Fixes ---------------------- diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java index 883c73ce849..9712c0d0816 100644 --- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java +++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java @@ -30,7 +30,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; @@ -53,6 +52,8 @@ import org.apache.solr.logging.MDCLoggingContext; import org.apache.solr.request.SolrRequestHandler; import org.apache.solr.security.AuthorizationPlugin; import org.apache.solr.security.AuthenticationPlugin; +import org.apache.solr.security.HttpClientInterceptorPlugin; +import org.apache.solr.security.PKIAuthenticationPlugin; import org.apache.solr.update.UpdateShardHandler; import org.apache.solr.util.DefaultSolrThreadFactory; import org.apache.solr.util.FileUtils; @@ -61,6 +62,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.solr.security.AuthenticationPlugin.AUTHENTICATION_PLUGIN_PROP; /** @@ -94,6 +96,8 @@ public class CoreContainer { protected CollectionsHandler collectionsHandler = null; private InfoHandler infoHandler; + private PKIAuthenticationPlugin pkiAuthenticationPlugin; + protected Properties containerProperties; private ConfigSetService coreConfigService; @@ -124,8 +128,6 @@ public class CoreContainer { public static final String COLLECTIONS_HANDLER_PATH = "/admin/collections"; public static final String INFO_HANDLER_PATH = "/admin/info"; - final public static String AUTHENTICATION_PLUGIN_PROP = "authenticationPlugin"; - private PluginBag containerHandlers = new PluginBag<>(SolrRequestHandler.class, null); private boolean asyncSolrCoreLoad; @@ -264,9 +266,15 @@ public class CoreContainer { } if (authenticationPlugin != null) { authenticationPlugin.init(authenticationConfig); + addHttpConfigurer(authenticationPlugin); + } + } + private void addHttpConfigurer(Object authcPlugin) { + log.info("addHttpConfigurer()");//TODO no commit + if (authcPlugin instanceof HttpClientInterceptorPlugin) { // Setup HttpClient to use the plugin's configurer for internode communication - HttpClientConfigurer configurer = authenticationPlugin.getDefaultConfigurer(); + HttpClientConfigurer configurer = ((HttpClientInterceptorPlugin) authcPlugin).getClientConfigurer(); HttpClientUtil.setConfigurer(configurer); // The default http client of the core container's shardHandlerFactory has already been created and @@ -274,9 +282,14 @@ public class CoreContainer { // http client configurer to set it up for internode communication. log.info("Reconfiguring the shard handler factory and update shard handler."); if (getShardHandlerFactory() instanceof HttpShardHandlerFactory) { - ((HttpShardHandlerFactory)getShardHandlerFactory()).reconfigureHttpClient(configurer); + ((HttpShardHandlerFactory) getShardHandlerFactory()).reconfigureHttpClient(configurer); } getUpdateShardHandler().reconfigureHttpClient(configurer); + } else { + if (pkiAuthenticationPlugin != null) { + log.info("PKIAuthenticationPlugin is managing internode requests"); + addHttpConfigurer(pkiAuthenticationPlugin); + } } } @@ -317,6 +330,10 @@ public class CoreContainer { return containerProperties; } + public PKIAuthenticationPlugin getPkiAuthenticationPlugin() { + return pkiAuthenticationPlugin; + } + //------------------------------------------------------------------- // Initialization / Cleanup //------------------------------------------------------------------- @@ -348,6 +365,7 @@ public class CoreContainer { hostName = cfg.getNodeName(); zkSys.initZooKeeper(this, solrHome, cfg.getCloudConfig()); + if(isZooKeeperAware()) pkiAuthenticationPlugin = new PKIAuthenticationPlugin(this, zkSys.getZkController().getNodeName()); initializeAuthenticationPlugin(); @@ -361,6 +379,8 @@ public class CoreContainer { containerHandlers.put(INFO_HANDLER_PATH, infoHandler); coreAdminHandler = createHandler(cfg.getCoreAdminHandlerClass(), CoreAdminHandler.class); containerHandlers.put(CORES_HANDLER_PATH, coreAdminHandler); + if(pkiAuthenticationPlugin != null) + containerHandlers.put(PKIAuthenticationPlugin.PATH, pkiAuthenticationPlugin.getRequestHandler()); coreConfigService = ConfigSetService.createConfigSetService(cfg, loader, zkSys.zkController); diff --git a/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java b/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java index 8385c9e6de1..973f40f2c56 100644 --- a/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java +++ b/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java @@ -25,6 +25,7 @@ import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.ContentStream; import org.apache.solr.core.SolrCore; +import java.security.Principal; import java.util.Map; /** @@ -97,6 +98,8 @@ public interface SolrQueryRequest { public Map getJSON(); public void setJSON(Map json); + + public Principal getUserPrincipal(); } diff --git a/solr/core/src/java/org/apache/solr/request/SolrQueryRequestBase.java b/solr/core/src/java/org/apache/solr/request/SolrQueryRequestBase.java index f8d2fd14589..b6798700749 100644 --- a/solr/core/src/java/org/apache/solr/request/SolrQueryRequestBase.java +++ b/solr/core/src/java/org/apache/solr/request/SolrQueryRequestBase.java @@ -27,6 +27,7 @@ import org.apache.solr.common.util.ContentStream; import org.apache.solr.core.SolrCore; import java.io.Closeable; +import java.security.Principal; import java.util.Map; import java.util.HashMap; @@ -175,4 +176,8 @@ public abstract class SolrQueryRequestBase implements SolrQueryRequest, Closeabl this.json = json; } + @Override + public Principal getUserPrincipal() { + return null; + } } diff --git a/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java b/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java index 5366065acd1..9c63deaf381 100644 --- a/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java @@ -1,11 +1,19 @@ package org.apache.solr.security; import javax.servlet.FilterChain; +import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; import java.io.Closeable; +import java.io.IOException; +import java.nio.file.attribute.UserPrincipal; +import java.security.Principal; import java.util.Map; +import org.apache.http.auth.BasicUserPrincipal; import org.apache.solr.client.solrj.impl.HttpClientConfigurer; /* @@ -31,11 +39,27 @@ import org.apache.solr.client.solrj.impl.HttpClientConfigurer; */ public abstract class AuthenticationPlugin implements Closeable { + final public static String AUTHENTICATION_PLUGIN_PROP = "authenticationPlugin"; + /** * This is called upon loading up of a plugin, used for setting it up. * @param pluginConfig Config parameters, possibly from a ZK source */ public abstract void init(Map pluginConfig); + + protected void forward(String user, ServletRequest req, ServletResponse rsp, + FilterChain chain) throws IOException, ServletException { + if(user != null) { + final Principal p = new BasicUserPrincipal(user); + req = new HttpServletRequestWrapper((HttpServletRequest) req) { + @Override + public Principal getUserPrincipal() { + return p; + } + }; + } + chain.doFilter(req,rsp); + } /** * This method must authenticate the request. Upon a successful authentication, this @@ -51,13 +75,5 @@ public abstract class AuthenticationPlugin implements Closeable { public abstract void doAuthenticate(ServletRequest request, ServletResponse response, FilterChain filterChain) throws Exception; - /** - * - * @return Returns an instance of a HttpClientConfigurer to be used for configuring the - * httpclients for use with SolrJ clients. - * - * @lucene.experimental - */ - public abstract HttpClientConfigurer getDefaultConfigurer(); } diff --git a/solr/core/src/java/org/apache/solr/security/HttpClientInterceptorPlugin.java b/solr/core/src/java/org/apache/solr/security/HttpClientInterceptorPlugin.java new file mode 100644 index 00000000000..ea208215dcc --- /dev/null +++ b/solr/core/src/java/org/apache/solr/security/HttpClientInterceptorPlugin.java @@ -0,0 +1,32 @@ +package org.apache.solr.security; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import org.apache.solr.client.solrj.impl.HttpClientConfigurer; + +public interface HttpClientInterceptorPlugin { + /** + * + * @return Returns an instance of a HttpClientConfigurer to be used for configuring the + * httpclients for use with SolrJ clients. + * + * @lucene.experimental + */ + public HttpClientConfigurer getClientConfigurer(); +} diff --git a/solr/core/src/java/org/apache/solr/security/KerberosPlugin.java b/solr/core/src/java/org/apache/solr/security/KerberosPlugin.java index 7321f617f63..28b94bad94f 100644 --- a/solr/core/src/java/org/apache/solr/security/KerberosPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/KerberosPlugin.java @@ -50,7 +50,7 @@ import org.slf4j.LoggerFactory; * limitations under the License. */ -public class KerberosPlugin extends AuthenticationPlugin { +public class KerberosPlugin extends AuthenticationPlugin implements HttpClientInterceptorPlugin { static final Logger log = LoggerFactory.getLogger(KerberosPlugin.class); HttpClientConfigurer kerberosConfigurer = new Krb5HttpClientConfigurer(); @@ -121,7 +121,7 @@ public class KerberosPlugin extends AuthenticationPlugin { } @Override - public HttpClientConfigurer getDefaultConfigurer() { + public HttpClientConfigurer getClientConfigurer() { return kerberosConfigurer; } diff --git a/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java b/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java new file mode 100644 index 00000000000..54abbfc42cc --- /dev/null +++ b/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java @@ -0,0 +1,299 @@ +package org.apache.solr.security; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import javax.servlet.FilterChain; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.Principal; +import java.security.PublicKey; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.http.HttpException; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.HttpResponse; +import org.apache.http.auth.BasicUserPrincipal; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; +import org.apache.solr.client.solrj.impl.HttpClientConfigurer; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.util.Base64; +import org.apache.solr.common.util.ExecutorUtil; +import org.apache.solr.common.util.StrUtils; +import org.apache.solr.common.util.Utils; +import org.apache.solr.core.CoreContainer; +import org.apache.solr.handler.RequestHandlerBase; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.request.SolrRequestHandler; +import org.apache.solr.request.SolrRequestInfo; +import org.apache.solr.response.SolrQueryResponse; +import org.apache.solr.util.CryptoKeys; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class PKIAuthenticationPlugin extends AuthenticationPlugin implements HttpClientInterceptorPlugin { + static final Logger log = LoggerFactory.getLogger(PKIAuthenticationPlugin.class); + private final Map keyCache = new ConcurrentHashMap<>(); + private CryptoKeys.RSAKeyPair keyPair = new CryptoKeys.RSAKeyPair(); + private final CoreContainer cores; + private int maxValidity = 5000; + private final String myNodeName; + + public PKIAuthenticationPlugin(CoreContainer cores, String nodeName) { + this.cores = cores; + myNodeName = nodeName; + } + + @Override + public void init(Map pluginConfig) { + } + + + @Override + public void doAuthenticate(ServletRequest request, ServletResponse response, FilterChain filterChain) throws Exception { + + String requestURI = ((HttpServletRequest) request).getRequestURI(); + if (requestURI.endsWith(PATH)) { + filterChain.doFilter(request, response); + return; + } + long receivedTime = System.currentTimeMillis(); + String header = ((HttpServletRequest) request).getHeader(HEADER); + if (header == null) { + log.error("No SolrAuth header present"); + filterChain.doFilter(request, response); + return; + } + + List authInfo = StrUtils.splitWS(header, false); + if (authInfo.size() < 2) { + log.error("Invalid SolrAuth Header"); + return; + } + + String nodeName = authInfo.get(0); + String cipher = authInfo.get(1); + + byte[] decipher = decipherData(nodeName, cipher); + if (decipher == null) { + return; + } + + String s = new String(decipher, StandardCharsets.UTF_8).trim(); + List pcs = StrUtils.splitWS(s, false); + if (pcs.size() < 2) { + return; + } + + final String userName = pcs.get(0); + String timeStr = pcs.get(1); + try { + long timeMillis = Long.parseLong(timeStr); + if ((receivedTime - timeMillis) > maxValidity) { + log.error("Invalid key "); + filterChain.doFilter(request, response); + return; + } + } catch (NumberFormatException e) { + log.error("Invalid time " + timeStr, e); + return; + } + final Principal principal = "$".equals(userName) ? + SU : + new BasicUserPrincipal(userName); + + filterChain.doFilter(getWrapper((HttpServletRequest) request, principal), response); + } + + private static HttpServletRequestWrapper getWrapper(final HttpServletRequest request, final Principal principal) { + return new HttpServletRequestWrapper(request) { + @Override + public Principal getUserPrincipal() { + return principal; + } + }; + } + + private byte[] decipherData(String nodeName, String cipherBase64) { + boolean freshKey = false; + PublicKey key = keyCache.get(nodeName); + if (key == null) { + key = getRemotePublicKey(nodeName); + freshKey = true; + } + + try { + return CryptoKeys.decryptRSA(Base64.base64ToByteArray(cipherBase64), key); + } catch (InvalidKeyException e) { + if (!freshKey) { + key = getRemotePublicKey(nodeName); + if (key == null) { + return null; + } + try { + return CryptoKeys.decryptRSA(Base64.base64ToByteArray(cipherBase64), key); + } catch (Exception e1) { + log.error("Error decrypting"); + return null; + } + } + + } catch (Exception e) { + log.error("Error decrypting"); + return null; + } + + return null; + } + + PublicKey getRemotePublicKey(String nodename) { + String url = cores.getZkController().getZkStateReader().getBaseUrlForNodeName(nodename); + try { + HttpResponse rsp = cores.getUpdateShardHandler().getHttpClient().execute(new HttpGet(url + PATH + "?wt=json&omitHeader=true")); + byte[] bytes = EntityUtils.toByteArray(rsp.getEntity()); + Map m = (Map) Utils.fromJSON(bytes); + String key = (String) m.get("key"); + if (key == null) { + log.error("No key available from " + url + PATH); + return null; + } + PublicKey pubKey = CryptoKeys.deserializeX509PublicKey(key); + keyCache.put(nodename, pubKey); + return pubKey; + } catch (Exception e) { + log.error("Exception trying to get public key from : " + url, e); + return null; + } + + } + + private HttpHeaderClientConfigurer clientConfigurer = new HttpHeaderClientConfigurer(); + + @Override + public HttpClientConfigurer getClientConfigurer() { + return clientConfigurer; + } + + public SolrRequestHandler getRequestHandler() { + return new RequestHandlerBase() { + @Override + public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception { + rsp.add("key", keyPair.getPublicKeyStr()); + } + + @Override + public String getDescription() { + return "Return the public key of this server"; + } + }; + } + + public boolean needsAuthorization(HttpServletRequest req) { + if (req.getUserPrincipal() == SU) return false; + + return true; + } + + private class HttpHeaderClientConfigurer extends HttpClientConfigurer implements + HttpRequestInterceptor { + + @Override + public void configure(DefaultHttpClient httpClient, SolrParams config) { + super.configure(httpClient, config); + httpClient.addRequestInterceptor(this); + } + + @Override + public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException { + if (disabled()) return; + setHeader(httpRequest); + } + + } + + + void setHeader(HttpRequest httpRequest) { + SolrRequestInfo reqInfo = getRequestInfo(); + String usr = null; + if (reqInfo != null) { + Principal principal = reqInfo.getReq().getUserPrincipal(); + if (principal == null) { + //this had a request but not authenticated + //so we don't not need to set a principal + return; + } else { + usr = principal.getName(); + } + } else { + if (!isSolrThread()) { + //if this is not running inside a Solr threadpool (as in testcases) + // then no need to add any header + return; + } + //this request seems to be originated from Solr itself + usr = "$"; //special name to denote the user is the node itself + } + + String s = usr + " " + System.currentTimeMillis(); + + byte[] payload = s.getBytes(StandardCharsets.UTF_8); + byte[] payloadCipher = keyPair.encrypt(ByteBuffer.wrap(payload)); + String base64Cipher = Base64.byteArrayToBase64(payloadCipher); + httpRequest.setHeader(HEADER, myNodeName + " " + base64Cipher); + } + + boolean isSolrThread() { + return ExecutorUtil.isSolrServerThread(); + } + + SolrRequestInfo getRequestInfo() { + return SolrRequestInfo.getRequestInfo(); + } + + boolean disabled() { + return cores.getAuthenticationPlugin() == null || + cores.getAuthenticationPlugin() instanceof HttpClientInterceptorPlugin; + } + + @Override + public void close() throws IOException { + + } + + public String getPublicKey() { + return keyPair.getPublicKeyStr(); + } + + public static final String HEADER = "SolrAuth"; + public static final String PATH = "/admin/info/key"; + public static final String NODE_IS_USER = "$"; + // special principal to denote the cluster member + private static final Principal SU = new BasicUserPrincipal("$"); +} diff --git a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java index 8f2b015e29c..e86072a6bcf 100644 --- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java +++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java @@ -104,6 +104,7 @@ import org.apache.solr.security.AuthorizationContext; import org.apache.solr.security.AuthorizationContext.CollectionRequest; import org.apache.solr.security.AuthorizationContext.RequestType; import org.apache.solr.security.AuthorizationResponse; +import org.apache.solr.security.PKIAuthenticationPlugin; import org.apache.solr.servlet.SolrDispatchFilter.Action; import org.apache.solr.servlet.cache.HttpCacheHeaderUtil; import org.apache.solr.servlet.cache.Method; @@ -417,7 +418,7 @@ public class HttpSolrCall { 1. Authorization is enabled, and 2. The requested resource is not a known static file */ - if (cores.getAuthorizationPlugin() != null) { + if (cores.getAuthorizationPlugin() != null && shouldAuthorize()) { AuthorizationContext context = getAuthCtx(); log.info(context.toString()); AuthorizationResponse authResponse = cores.getAuthorizationPlugin().authorize(context); @@ -484,6 +485,15 @@ public class HttpSolrCall { } + private boolean shouldAuthorize() { + if(PKIAuthenticationPlugin.PATH.equals(path)) return false; + //admin/info/key is the path where public key is exposed . it is always unsecured + if( cores.getPkiAuthenticationPlugin() != null && req.getUserPrincipal() != null){ + return cores.getPkiAuthenticationPlugin().needsAuthorization(req); + } + return true; + } + void destroy() { try { if (solrReq != null) { diff --git a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java index 208755db333..28107056d1b 100644 --- a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java +++ b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java @@ -24,7 +24,6 @@ import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.ArrayList; @@ -34,16 +33,11 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.solr.client.solrj.impl.HttpClientConfigurer; -import org.apache.solr.client.solrj.impl.HttpClientUtil; +import org.apache.http.client.HttpClient; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrException.ErrorCode; import org.apache.solr.common.cloud.SolrZkClient; -import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.util.ExecutorUtil; import org.apache.solr.core.CoreContainer; import org.apache.solr.core.NodeConfig; @@ -52,6 +46,7 @@ import org.apache.solr.core.SolrResourceLoader; import org.apache.solr.core.SolrXmlConfig; import org.apache.solr.request.SolrRequestInfo; import org.apache.solr.security.AuthenticationPlugin; +import org.apache.solr.security.PKIAuthenticationPlugin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,7 +61,7 @@ public class SolrDispatchFilter extends BaseSolrFilter { protected volatile CoreContainer cores; protected String abortErrorMessage = null; - protected final CloseableHttpClient httpClient = HttpClientUtil.createClient(new ModifiableSolrParams()); + protected HttpClient httpClient; private ArrayList excludePatterns; /** @@ -112,14 +107,7 @@ public class SolrDispatchFilter extends BaseSolrFilter { ExecutorUtil.addThreadLocalProvider(SolrRequestInfo.getInheritableThreadLocalProvider()); this.cores = createCoreContainer(solrHome, extraProperties); - - if (this.cores.getAuthenticationPlugin() != null) { - HttpClientConfigurer configurer = this.cores.getAuthenticationPlugin().getDefaultConfigurer(); - if (configurer != null) { - configurer.configure((DefaultHttpClient) httpClient, new ModifiableSolrParams()); - } - } - + this.httpClient = cores.getUpdateShardHandler().getHttpClient(); log.info("user.dir=" + System.getProperty("user.dir")); } catch( Throwable t ) { @@ -180,13 +168,9 @@ public class SolrDispatchFilter extends BaseSolrFilter { @Override public void destroy() { - try { - if (cores != null) { - cores.shutdown(); - cores = null; - } - } finally { - IOUtils.closeQuietly(httpClient); + if (cores != null) { + cores.shutdown(); + cores = null; } } @@ -198,7 +182,7 @@ public class SolrDispatchFilter extends BaseSolrFilter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain, boolean retry) throws IOException, ServletException { if (!(request instanceof HttpServletRequest)) return; - AtomicReference wrappedRequest = new AtomicReference(); + AtomicReference wrappedRequest = new AtomicReference<>(); if (!authenticateRequest(request, response, wrappedRequest)) { // the response and status code have already been sent return; } @@ -254,6 +238,10 @@ public class SolrDispatchFilter extends BaseSolrFilter { if (authenticationPlugin == null) { return true; } else { + //special case when solr is securing inter-node requests + String header = ((HttpServletRequest) request).getHeader(PKIAuthenticationPlugin.HEADER); + if (header != null && cores.getPkiAuthenticationPlugin() != null) + authenticationPlugin = cores.getPkiAuthenticationPlugin(); try { log.debug("Request to authenticate: {}, domain: {}, port: {}", request, request.getLocalName(), request.getLocalPort()); // upon successful authentication, this should call the chain's next filter. diff --git a/solr/core/src/java/org/apache/solr/servlet/SolrRequestParsers.java b/solr/core/src/java/org/apache/solr/servlet/SolrRequestParsers.java index 98589067494..b074634f681 100644 --- a/solr/core/src/java/org/apache/solr/servlet/SolrRequestParsers.java +++ b/solr/core/src/java/org/apache/solr/servlet/SolrRequestParsers.java @@ -29,6 +29,7 @@ import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; +import java.security.Principal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -158,7 +159,7 @@ public class SolrRequestParsers // Pick the parser from the request... ArrayList streams = new ArrayList<>(1); SolrParams params = parser.parseParamsAndFillStreams( req, streams ); - SolrQueryRequest sreq = buildRequestFrom( core, params, streams, getRequestTimer(req) ); + SolrQueryRequest sreq = buildRequestFrom(core, params, streams, getRequestTimer(req), req); // Handlers and login will want to know the path. If it contains a ':' // the handler could use it for RESTful URLs @@ -170,14 +171,13 @@ public class SolrRequestParsers } return sreq; } - - public SolrQueryRequest buildRequestFrom( SolrCore core, SolrParams params, Collection streams ) throws Exception - { - return buildRequestFrom( core, params, streams, new RTimer() ); + + public SolrQueryRequest buildRequestFrom(SolrCore core, SolrParams params, Collection streams) throws Exception { + return buildRequestFrom(core, params, streams, new RTimer(), null); } - private SolrQueryRequest buildRequestFrom( SolrCore core, SolrParams params, Collection streams, RTimer requestTimer ) throws Exception - { + private SolrQueryRequest buildRequestFrom(SolrCore core, SolrParams params, Collection streams, + RTimer requestTimer, final HttpServletRequest req) throws Exception { // The content type will be applied to all streaming content String contentType = params.get( CommonParams.STREAM_CONTENTTYPE ); @@ -222,8 +222,13 @@ public class SolrRequestParsers streams.add( stream ); } } - - SolrQueryRequestBase q = new SolrQueryRequestBase( core, params, requestTimer ) { }; + + SolrQueryRequestBase q = new SolrQueryRequestBase(core, params, requestTimer) { + @Override + public Principal getUserPrincipal() { + return req == null ? null : req.getUserPrincipal(); + } + }; if( streams != null && streams.size() > 0 ) { q.setContentStreams( streams ); } diff --git a/solr/core/src/java/org/apache/solr/util/CryptoKeys.java b/solr/core/src/java/org/apache/solr/util/CryptoKeys.java index 847729340bb..6cdb5bebd48 100644 --- a/solr/core/src/java/org/apache/solr/util/CryptoKeys.java +++ b/solr/core/src/java/org/apache/solr/util/CryptoKeys.java @@ -24,12 +24,16 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.KeyFactory; +import java.security.KeyPairGenerator; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; import java.security.PublicKey; +import java.security.SecureRandom; import java.security.Signature; import java.security.SignatureException; import java.security.spec.X509EncodedKeySpec; @@ -252,4 +256,91 @@ public final class CryptoKeys { } } + public static PublicKey deserializeX509PublicKey(String pubKey) { + try { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.base64ToByteArray(pubKey)); + return keyFactory.generatePublic(publicKeySpec); + } catch (Exception e) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,e); + } + } + + public static byte[] decryptRSA(byte[] buffer, PublicKey pubKey) throws InvalidKeyException, BadPaddingException, IllegalBlockSizeException { + Cipher rsaCipher = null; + try { + rsaCipher = Cipher.getInstance("RSA/ECB/nopadding"); + } catch (Exception e) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,e); + } + rsaCipher.init(Cipher.DECRYPT_MODE, pubKey); + return rsaCipher.doFinal(buffer, 0, buffer.length); + + } + + public static class RSAKeyPair { + private final String pubKeyStr; + private final PublicKey publicKey; + private final PrivateKey privateKey; + private final SecureRandom random = new SecureRandom(); + + + public RSAKeyPair() { + KeyPairGenerator keyGen = null; + try { + keyGen = KeyPairGenerator.getInstance("RSA"); + } catch (NoSuchAlgorithmException e) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e); + } + keyGen.initialize(512); + java.security.KeyPair keyPair = keyGen.genKeyPair(); + privateKey = keyPair.getPrivate(); + publicKey = keyPair.getPublic(); + + X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec( + publicKey.getEncoded()); + + pubKeyStr = Base64.byteArrayToBase64(x509EncodedKeySpec.getEncoded()); + } + + public String getPublicKeyStr() { + return pubKeyStr; + } + + public byte[] encrypt(ByteBuffer buffer) { + try { + Cipher rsaCipher = Cipher.getInstance("RSA/ECB/nopadding"); + rsaCipher.init(Cipher.ENCRYPT_MODE, privateKey); + return rsaCipher.doFinal(buffer.array(),buffer.position(), buffer.limit()); + } catch (Exception e) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,e); + } + } + public byte[] signSha256(byte[] bytes) throws InvalidKeyException, SignatureException { + Signature dsa = null; + try { + dsa = Signature.getInstance("SHA256withRSA"); + } catch (NoSuchAlgorithmException e) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e); + } + dsa.initSign(privateKey); + dsa.update(bytes,0,bytes.length); + return dsa.sign(); + + } + + } + + public static void main(String[] args) throws Exception { + RSAKeyPair keyPair = new RSAKeyPair(); + System.out.println(keyPair.getPublicKeyStr()); + PublicKey pk = deserializeX509PublicKey(keyPair.getPublicKeyStr()); + byte[] payload = "Hello World!".getBytes(StandardCharsets.UTF_8); + byte[] encrypted = keyPair.encrypt(ByteBuffer.wrap(payload)); + String cipherBase64 = Base64.byteArrayToBase64(encrypted); + System.out.println("encrypted: "+ cipherBase64); + System.out.println("signed: "+ Base64.byteArrayToBase64(keyPair.signSha256(payload))); + System.out.println("decrypted "+ new String(decryptRSA(encrypted , pk), StandardCharsets.UTF_8)); + } + } diff --git a/solr/core/src/test/org/apache/solr/cloud/TestAuthenticationFramework.java b/solr/core/src/test/org/apache/solr/cloud/TestAuthenticationFramework.java index 2a76a4e0116..173192bf1c2 100644 --- a/solr/core/src/test/org/apache/solr/cloud/TestAuthenticationFramework.java +++ b/solr/core/src/test/org/apache/solr/cloud/TestAuthenticationFramework.java @@ -35,6 +35,7 @@ import org.apache.lucene.util.LuceneTestCase.SuppressSysoutChecks; import org.apache.solr.client.solrj.impl.HttpClientConfigurer; import org.apache.solr.common.params.SolrParams; import org.apache.solr.security.AuthenticationPlugin; +import org.apache.solr.security.HttpClientInterceptorPlugin; import org.apache.solr.util.RevertDefaultThreadHandlerRule; import org.junit.ClassRule; import org.junit.Rule; @@ -110,7 +111,7 @@ public class TestAuthenticationFramework extends TestMiniSolrCloudCluster { super.tearDown(); } - public static class MockAuthenticationPlugin extends AuthenticationPlugin { + public static class MockAuthenticationPlugin extends AuthenticationPlugin implements HttpClientInterceptorPlugin { private static Logger log = LoggerFactory.getLogger(MockAuthenticationPlugin.class); public static String expectedUsername = "solr"; @@ -135,7 +136,7 @@ public class TestAuthenticationFramework extends TestMiniSolrCloudCluster { } @Override - public HttpClientConfigurer getDefaultConfigurer() { + public HttpClientConfigurer getClientConfigurer() { return new MockClientConfigurer(); } diff --git a/solr/core/src/test/org/apache/solr/security/MockAuthenticationPlugin.java b/solr/core/src/test/org/apache/solr/security/MockAuthenticationPlugin.java new file mode 100644 index 00000000000..87767a15fbe --- /dev/null +++ b/solr/core/src/test/org/apache/solr/security/MockAuthenticationPlugin.java @@ -0,0 +1,54 @@ +package org.apache.solr.security; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.IOException; +import java.security.Principal; +import java.util.Map; +import java.util.function.Predicate; + +public class MockAuthenticationPlugin extends AuthenticationPlugin { + static Predicate predicate; + + @Override + public void init(Map pluginConfig) { + } + + @Override + public void doAuthenticate(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { + String user = null; + if (predicate != null) { + if (predicate.test(request)) { + user = (String) request.getAttribute(Principal.class.getName()); + request.removeAttribute(Principal.class.getName()); + } + } + forward(user, request, response, filterChain); + } + + + @Override + public void close() throws IOException { + + } +} diff --git a/solr/core/src/test/org/apache/solr/security/MockAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/MockAuthorizationPlugin.java index d6836579202..f21b726b041 100644 --- a/solr/core/src/test/org/apache/solr/security/MockAuthorizationPlugin.java +++ b/solr/core/src/test/org/apache/solr/security/MockAuthorizationPlugin.java @@ -20,19 +20,34 @@ package org.apache.solr.security; import java.io.IOException; import java.util.HashSet; import java.util.Map; +import java.util.function.Predicate; +import org.apache.solr.common.SolrException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MockAuthorizationPlugin implements AuthorizationPlugin{ private Logger log = LoggerFactory.getLogger(MockAuthorizationPlugin.class); - HashSet denyUsers; - + static final HashSet denyUsers = new HashSet<>(); + static Predicate predicate; + @Override public AuthorizationResponse authorize(AuthorizationContext context) { - log.info("User request: " + context.getParams().get("uname")); - if(denyUsers.contains(context.getParams().get("uname"))) + String uname = context.getUserPrincipal()== null? null : context.getUserPrincipal().getName(); + if(predicate != null){ + try { + predicate.test(context); + return new AuthorizationResponse(200); + } catch (SolrException e) { + return new AuthorizationResponse(e.code()); + } + } + + + if(uname == null) uname = context.getParams().get("uname"); + log.info("User request: " + uname); + if(denyUsers.contains(uname)) return new AuthorizationResponse(403); else return new AuthorizationResponse(200); @@ -40,9 +55,6 @@ public class MockAuthorizationPlugin implements AuthorizationPlugin{ @Override public void init(Map initInfo) { - denyUsers = new HashSet(); - denyUsers.add("user1"); - denyUsers.add("user2"); } @Override diff --git a/solr/core/src/test/org/apache/solr/security/PKIAuthenticationIntegrationTest.java b/solr/core/src/test/org/apache/solr/security/PKIAuthenticationIntegrationTest.java new file mode 100644 index 00000000000..25c14a3cfe4 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/security/PKIAuthenticationIntegrationTest.java @@ -0,0 +1,115 @@ +package org.apache.solr.security; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import java.security.Principal; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.client.solrj.request.QueryRequest; +import org.apache.solr.cloud.AbstractFullDistribZkTestBase; +import org.apache.solr.common.cloud.ZkStateReader; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.util.Utils; +import org.apache.solr.request.LocalSolrQueryRequest; +import org.apache.zookeeper.CreateMode; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.util.Collections.singletonMap; +import static org.apache.solr.common.util.Utils.makeMap; + +@SolrTestCaseJ4.SuppressSSL +public class PKIAuthenticationIntegrationTest extends AbstractFullDistribZkTestBase { + final private Logger log = LoggerFactory.getLogger(PKIAuthenticationIntegrationTest.class); + + static final int TIMEOUT = 10000; + + public void distribSetUp() throws Exception { + super.distribSetUp(); + + byte[] bytes = Utils.toJSON(makeMap("authorization", singletonMap("class", MockAuthorizationPlugin.class.getName()), + "authentication", singletonMap("class", MockAuthenticationPlugin.class.getName()))); + + try (ZkStateReader zkStateReader = new ZkStateReader(zkServer.getZkAddress(), + TIMEOUT, TIMEOUT)) { + zkStateReader.getZkClient().create(ZkStateReader.SOLR_SECURITY_CONF_PATH, bytes, CreateMode.PERSISTENT, true); + } + } + + @Test + public void testPkiAuth() throws Exception { + waitForThingsToLevelOut(10); + log.info("Starting test"); + ModifiableSolrParams params = new ModifiableSolrParams(); + params.add("q", "*:*"); + params.add("__user", "solr"); + params.add("__pwd", "SolrRocks"); + // This should work fine. + final AtomicInteger count = new AtomicInteger(); + + + MockAuthorizationPlugin.predicate = new Predicate() { + @Override + public boolean test(AuthorizationContext context) { + if ("/select".equals(context.getResource())) { + Principal principal = context.getUserPrincipal(); + log.info("principalIs : {}", principal); + if (principal != null && principal.getName().equals("solr")) { + count.incrementAndGet(); + } + } + return true; + } + }; + + MockAuthenticationPlugin.predicate = new Predicate() { + @Override + public boolean test(ServletRequest servletRequest) { + String s = ((HttpServletRequest) servletRequest).getQueryString(); + if (s != null && s.contains("__user=solr") && s.contains("__pwd=SolrRocks")) { + servletRequest.setAttribute(Principal.class.getName(), "solr"); + } + return true; + } + }; + QueryRequest query = new QueryRequest(params); + LocalSolrQueryRequest lsqr = new LocalSolrQueryRequest(null, new ModifiableSolrParams()) { + @Override + public Principal getUserPrincipal() { + return null; + } + }; + query.process(cloudClient); + log.info("count :{}", count); + assertTrue(count.get() > 2); + } + + @Override + public void distribTearDown() throws Exception { + super.distribTearDown(); + MockAuthenticationPlugin.predicate = null; + MockAuthorizationPlugin.predicate = null; + } + +} diff --git a/solr/core/src/test/org/apache/solr/security/TestAuthorizationFramework.java b/solr/core/src/test/org/apache/solr/security/TestAuthorizationFramework.java index b3db2d81215..7f7878806bd 100644 --- a/solr/core/src/test/org/apache/solr/security/TestAuthorizationFramework.java +++ b/solr/core/src/test/org/apache/solr/security/TestAuthorizationFramework.java @@ -45,6 +45,8 @@ public class TestAuthorizationFramework extends AbstractFullDistribZkTestBase { @Test public void authorizationFrameworkTest() throws Exception { + MockAuthorizationPlugin.denyUsers.add("user1"); + MockAuthorizationPlugin.denyUsers.add("user1"); waitForThingsToLevelOut(10); log.info("Starting test"); ModifiableSolrParams params = new ModifiableSolrParams(); @@ -60,4 +62,11 @@ public class TestAuthorizationFramework extends AbstractFullDistribZkTestBase { } catch (Exception e) {} log.info("Ending test"); } + + @Override + public void distribTearDown() throws Exception { + super.distribTearDown(); + MockAuthorizationPlugin.denyUsers.clear(); + + } } diff --git a/solr/core/src/test/org/apache/solr/security/TestPKIAuthenticationPlugin.java b/solr/core/src/test/org/apache/solr/security/TestPKIAuthenticationPlugin.java new file mode 100644 index 00000000000..05fff9641e7 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/security/TestPKIAuthenticationPlugin.java @@ -0,0 +1,173 @@ +package org.apache.solr.security; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.IOException; +import java.security.Principal; +import java.security.PublicKey; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.http.Header; +import org.apache.http.auth.BasicUserPrincipal; +import org.apache.http.message.BasicHttpRequest; +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.core.CoreContainer; +import org.apache.solr.request.LocalSolrQueryRequest; +import org.apache.solr.request.SolrRequestInfo; +import org.apache.solr.response.SolrQueryResponse; +import org.apache.solr.util.CryptoKeys; +import org.easymock.EasyMock; +import org.easymock.IAnswer; + +import static org.easymock.EasyMock.getCurrentArguments; + +public class TestPKIAuthenticationPlugin extends SolrTestCaseJ4 { + + static class MockPKIAuthenticationPlugin extends PKIAuthenticationPlugin { + SolrRequestInfo solrRequestInfo; + + Map remoteKeys = new HashMap<>(); + + public MockPKIAuthenticationPlugin(CoreContainer cores, String node) { + super(cores, node); + } + + @Override + boolean disabled() { + return false; + } + + @Override + SolrRequestInfo getRequestInfo() { + return solrRequestInfo; + } + + @Override + PublicKey getRemotePublicKey(String nodename) { + return remoteKeys.get(nodename); + } + + @Override + boolean isSolrThread() { + return true; + } + } + + public void test() throws Exception { + AtomicReference principal = new AtomicReference<>(); + String nodeName = "node_x_233"; + + final MockPKIAuthenticationPlugin mock = new MockPKIAuthenticationPlugin(null, nodeName); + LocalSolrQueryRequest localSolrQueryRequest = new LocalSolrQueryRequest(null, new ModifiableSolrParams()) { + @Override + public Principal getUserPrincipal() { + return principal.get(); + } + }; + mock.remoteKeys.put(nodeName, CryptoKeys.deserializeX509PublicKey(mock.getPublicKey())); + principal.set(new BasicUserPrincipal("solr")); + mock.solrRequestInfo = new SolrRequestInfo(localSolrQueryRequest, new SolrQueryResponse()); + BasicHttpRequest request = new BasicHttpRequest("GET", "http://localhost:56565"); + mock.setHeader(request); + final AtomicReference
header = new AtomicReference<>(); + header.set(request.getFirstHeader(PKIAuthenticationPlugin.HEADER)); + assertNotNull(header.get()); + assertTrue(header.get().getValue().startsWith(nodeName)); + final AtomicReference wrappedRequestByFilter = new AtomicReference<>(); + HttpServletRequest mockReq = createMockRequest(header); + FilterChain filterChain = new FilterChain() { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) + throws IOException, ServletException { + wrappedRequestByFilter.set(servletRequest); + } + }; + mock.doAuthenticate(mockReq, null, filterChain); + + assertNotNull(wrappedRequestByFilter.get()); + assertEquals("solr", ((HttpServletRequest) wrappedRequestByFilter.get()).getUserPrincipal().getName()); + + //test 2 + principal.set(null); // no user + header.set(null); + wrappedRequestByFilter.set(null);// + request = new BasicHttpRequest("GET", "http://localhost:56565"); + mock.setHeader(request); + assertNull(request.getFirstHeader(PKIAuthenticationPlugin.HEADER)); + mock.doAuthenticate(mockReq, null, filterChain); + assertNotNull(wrappedRequestByFilter.get()); + assertNull(((HttpServletRequest) wrappedRequestByFilter.get()).getUserPrincipal()); + + //test 3 . No user request . Request originated from Solr + mock.solrRequestInfo = null; + header.set(null); + wrappedRequestByFilter.set(null); + request = new BasicHttpRequest("GET", "http://localhost:56565"); + mock.setHeader(request); + header.set(request.getFirstHeader(PKIAuthenticationPlugin.HEADER)); + assertNotNull(header.get()); + assertTrue(header.get().getValue().startsWith(nodeName)); + + mock.doAuthenticate(mockReq, null, filterChain); + assertNotNull(wrappedRequestByFilter.get()); + assertEquals("$", ((HttpServletRequest) wrappedRequestByFilter.get()).getUserPrincipal().getName()); + + } + + private HttpServletRequest createMockRequest(final AtomicReference
header) { + HttpServletRequest mockReq = EasyMock.createMock(HttpServletRequest.class); + EasyMock.reset(mockReq); + mockReq.getHeader(EasyMock.anyObject(String.class)); + EasyMock.expectLastCall().andAnswer(new IAnswer() { + @Override + public String answer() throws Throwable { + if (PKIAuthenticationPlugin.HEADER.equals(getCurrentArguments()[0])) { + if (header.get() == null) return null; + return header.get().getValue(); + } else return null; + } + }).anyTimes(); + mockReq.getUserPrincipal(); + EasyMock.expectLastCall().andAnswer(new IAnswer() { + @Override + public Principal answer() throws Throwable { + return null; + } + }).anyTimes(); + + mockReq.getRequestURI(); + EasyMock.expectLastCall().andAnswer(new IAnswer() { + @Override + public String answer() throws Throwable { + return "/collection1/select"; + } + }).anyTimes(); + + EasyMock.replay(mockReq); + return mockReq; + } +} diff --git a/solr/solrj/src/java/org/apache/solr/common/util/Base64.java b/solr/solrj/src/java/org/apache/solr/common/util/Base64.java index 8cc3381f693..e470ed94495 100644 --- a/solr/solrj/src/java/org/apache/solr/common/util/Base64.java +++ b/solr/solrj/src/java/org/apache/solr/common/util/Base64.java @@ -52,6 +52,10 @@ public class Base64 { 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 }; + public static String byteArrayToBase64(byte[] a) { + return byteArrayToBase64(a, 0, a.length); + } + public static String byteArrayToBase64(byte[] a, int offset, int len) { int aLen = len; int numFullGroups = aLen / 3; diff --git a/solr/solrj/src/java/org/apache/solr/common/util/ExecutorUtil.java b/solr/solrj/src/java/org/apache/solr/common/util/ExecutorUtil.java index 25913390ef2..f0c6e05f0a0 100644 --- a/solr/solrj/src/java/org/apache/solr/common/util/ExecutorUtil.java +++ b/solr/solrj/src/java/org/apache/solr/common/util/ExecutorUtil.java @@ -193,6 +193,7 @@ public class ExecutorUtil { super.execute(new Runnable() { @Override public void run() { + isServerPool.set(Boolean.TRUE); if (ctx != null) { for (int i = 0; i < providersCopy.size(); i++) providersCopy.get(i).set(ctx.get(i)); } @@ -214,6 +215,7 @@ public class ExecutorUtil { log.error("Uncaught exception {} thrown by thread: {}", t, currentThread.getName(), submitterStackTrace); throw t; } finally { + isServerPool.remove(); if (threadContext != null && !threadContext.isEmpty()) { MDC.setContextMap(threadContext); } else { @@ -228,4 +230,11 @@ public class ExecutorUtil { }); } } + + private static final ThreadLocal isServerPool = new ThreadLocal<>(); + + public static boolean isSolrServerThread() { + return Boolean.TRUE.equals(isServerPool.get()); + } + }