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
This commit is contained in:
Noble Paul 2015-08-05 13:59:14 +00:00
parent eb24e46781
commit 4112979808
20 changed files with 906 additions and 58 deletions

View File

@ -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
----------------------

View File

@ -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<SolrRequestHandler> 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);

View File

@ -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<String,Object> getJSON();
public void setJSON(Map<String,Object> json);
public Principal getUserPrincipal();
}

View File

@ -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;
}
}

View File

@ -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<String, Object> 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();
}

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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<String, PublicKey> 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<String, Object> 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<String> 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<String> 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("$");
}

View File

@ -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) {

View File

@ -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<Pattern> 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<ServletRequest> wrappedRequest = new AtomicReference();
AtomicReference<ServletRequest> 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.

View File

@ -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<ContentStream> 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<ContentStream> streams ) throws Exception
{
return buildRequestFrom( core, params, streams, new RTimer() );
public SolrQueryRequest buildRequestFrom(SolrCore core, SolrParams params, Collection<ContentStream> streams) throws Exception {
return buildRequestFrom(core, params, streams, new RTimer(), null);
}
private SolrQueryRequest buildRequestFrom( SolrCore core, SolrParams params, Collection<ContentStream> streams, RTimer requestTimer ) throws Exception
{
private SolrQueryRequest buildRequestFrom(SolrCore core, SolrParams params, Collection<ContentStream> 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 );
}

View File

@ -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));
}
}

View File

@ -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();
}

View File

@ -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<ServletRequest> predicate;
@Override
public void init(Map<String, Object> 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 {
}
}

View File

@ -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<String> denyUsers;
static final HashSet<String> denyUsers = new HashSet<>();
static Predicate<AuthorizationContext> 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<String, Object> initInfo) {
denyUsers = new HashSet();
denyUsers.add("user1");
denyUsers.add("user2");
}
@Override

View File

@ -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<AuthorizationContext>() {
@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<ServletRequest>() {
@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;
}
}

View File

@ -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();
}
}

View File

@ -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<String, PublicKey> 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> 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> header = new AtomicReference<>();
header.set(request.getFirstHeader(PKIAuthenticationPlugin.HEADER));
assertNotNull(header.get());
assertTrue(header.get().getValue().startsWith(nodeName));
final AtomicReference<ServletRequest> 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> header) {
HttpServletRequest mockReq = EasyMock.createMock(HttpServletRequest.class);
EasyMock.reset(mockReq);
mockReq.getHeader(EasyMock.anyObject(String.class));
EasyMock.expectLastCall().andAnswer(new IAnswer<String>() {
@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<Principal>() {
@Override
public Principal answer() throws Throwable {
return null;
}
}).anyTimes();
mockReq.getRequestURI();
EasyMock.expectLastCall().andAnswer(new IAnswer<String>() {
@Override
public String answer() throws Throwable {
return "/collection1/select";
}
}).anyTimes();
EasyMock.replay(mockReq);
return mockReq;
}
}

View File

@ -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;

View File

@ -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<Boolean> isServerPool = new ThreadLocal<>();
public static boolean isSolrServerThread() {
return Boolean.TRUE.equals(isServerPool.get());
}
}