mirror of https://github.com/apache/lucene.git
SOLR-12799: Allow Authentication Plugins to intercept internode requests on a per-request basis
Add 'forwardCredentials' parameter to BasicAuth which will then skip using PKI on sub requests
This commit is contained in:
parent
fa025e1f78
commit
81dbad54e0
|
@ -79,6 +79,10 @@ New Features
|
|||
|
||||
* SOLR-12593: The default configSet now includes an "ignored_*" dynamic field. (David Smiley)
|
||||
|
||||
* SOLR-12799: Allow Authentication Plugins to intercept internode requests on a per-request basis.
|
||||
The BasicAuth plugin now supports a new parameter 'forwardCredentials', and when set to 'true',
|
||||
user's BasicAuth credentials will be used instead of PKI for client initiated internode requests. (janhoy, noble)
|
||||
|
||||
* SOLR-12791: Add Metrics reporting for AuthenticationPlugin (janhoy)
|
||||
|
||||
Bug Fixes
|
||||
|
|
|
@ -110,6 +110,7 @@ import org.apache.solr.metrics.SolrCoreMetricManager;
|
|||
import org.apache.solr.metrics.SolrMetricManager;
|
||||
import org.apache.solr.metrics.SolrMetricProducer;
|
||||
import org.apache.solr.request.SolrRequestHandler;
|
||||
import org.apache.solr.request.SolrRequestInfo;
|
||||
import org.apache.solr.search.SolrFieldCacheBean;
|
||||
import org.apache.solr.security.AuthenticationPlugin;
|
||||
import org.apache.solr.security.AuthorizationPlugin;
|
||||
|
@ -432,15 +433,11 @@ public class CoreContainer {
|
|||
}
|
||||
|
||||
HttpClientUtil.setHttpClientRequestContextBuilder(httpClientBuilder);
|
||||
|
||||
} else {
|
||||
if (pkiAuthenticationPlugin != null) {
|
||||
//this happened due to an authc plugin reload. no need to register the pkiAuthc plugin again
|
||||
if(pkiAuthenticationPlugin.isInterceptorRegistered()) return;
|
||||
log.info("PKIAuthenticationPlugin is managing internode requests");
|
||||
setupHttpClientForAuthPlugin(pkiAuthenticationPlugin);
|
||||
pkiAuthenticationPlugin.setInterceptorRegistered();
|
||||
}
|
||||
// Always register PKI auth interceptor, which will then delegate the decision of who should secure
|
||||
// each request to the configured authentication plugin.
|
||||
if (pkiAuthenticationPlugin != null && !pkiAuthenticationPlugin.isInterceptorRegistered()) {
|
||||
pkiAuthenticationPlugin.getHttpClientBuilder(HttpClientUtil.getHttpClientBuilder());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1888,6 +1885,10 @@ public class CoreContainer {
|
|||
return tragicException != null;
|
||||
}
|
||||
|
||||
static {
|
||||
ExecutorUtil.addThreadLocalProvider(SolrRequestInfo.getInheritableThreadLocalProvider());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CloserThread extends Thread {
|
||||
|
|
|
@ -56,6 +56,7 @@ import org.apache.solr.common.util.NamedList;
|
|||
import org.apache.solr.common.util.StrUtils;
|
||||
import org.apache.solr.core.CoreDescriptor;
|
||||
import org.apache.solr.request.SolrQueryRequest;
|
||||
import org.apache.solr.request.SolrRequestInfo;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.MDC;
|
||||
|
@ -154,6 +155,8 @@ public class HttpShardHandler extends ShardHandler {
|
|||
|
||||
QueryRequest req = makeQueryRequest(sreq, params, shard);
|
||||
req.setMethod(SolrRequest.METHOD.POST);
|
||||
SolrRequestInfo requestInfo = SolrRequestInfo.getRequestInfo();
|
||||
if (requestInfo != null) req.setUserPrincipal(requestInfo.getReq().getUserPrincipal());
|
||||
|
||||
// no need to set the response parser as binary is the default
|
||||
// req.setResponseParser(new BinaryResponseParser());
|
||||
|
|
|
@ -33,6 +33,9 @@ import org.apache.solr.core.SolrInfoBean;
|
|||
import org.apache.solr.metrics.SolrMetricManager;
|
||||
import org.apache.solr.metrics.SolrMetricProducer;
|
||||
|
||||
import org.apache.http.HttpRequest;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
|
||||
/**
|
||||
*
|
||||
* @lucene.experimental
|
||||
|
@ -96,6 +99,25 @@ public abstract class AuthenticationPlugin implements Closeable, SolrInfoBean, S
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to intercept internode requests. This allows your authentication
|
||||
* plugin to decide on per-request basis whether it should handle inter-node requests or
|
||||
* delegate to {@link PKIAuthenticationPlugin}. Return true to indicate that your plugin
|
||||
* did handle the request, or false to signal that PKI plugin should handle it. This method
|
||||
* will be called by {@link PKIAuthenticationPlugin}'s interceptor.
|
||||
*
|
||||
* <p>
|
||||
* If not overridden, this method will return true for plugins implementing {@link HttpClientBuilderPlugin}.
|
||||
* This method can be overridden by subclasses e.g. to set HTTP headers, even if you don't use a clientBuilder.
|
||||
* </p>
|
||||
* @param httpRequest the httpRequest that is about to be sent to another internal Solr node
|
||||
* @param httpContext the context of that request.
|
||||
* @return true if this plugin handled authentication for the request, else false
|
||||
*/
|
||||
protected boolean interceptInternodeRequest(HttpRequest httpRequest, HttpContext httpContext) {
|
||||
return this instanceof HttpClientBuilderPlugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup any per request data
|
||||
*/
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.apache.solr.security;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
|
@ -23,21 +24,30 @@ import javax.servlet.http.HttpServletRequest;
|
|||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.auth.BasicUserPrincipal;
|
||||
import org.apache.http.HttpRequest;
|
||||
import org.apache.http.annotation.Contract;
|
||||
import org.apache.http.annotation.ThreadingBehavior;
|
||||
import org.apache.http.client.protocol.HttpClientContext;
|
||||
import org.apache.http.message.BasicHeader;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
import org.apache.solr.common.SolrException;
|
||||
import org.apache.solr.common.SolrException.ErrorCode;
|
||||
import org.apache.solr.common.SpecProvider;
|
||||
import org.apache.solr.common.util.CommandOperation;
|
||||
import org.apache.solr.common.util.ValidatingJsonMap;
|
||||
|
@ -50,6 +60,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
|
|||
private final static ThreadLocal<Header> authHeader = new ThreadLocal<>();
|
||||
private static final String X_REQUESTED_WITH_HEADER = "X-Requested-With";
|
||||
private boolean blockUnknown = false;
|
||||
private boolean forwardCredentials = false;
|
||||
|
||||
public boolean authenticate(String username, String pwd) {
|
||||
return authenticationProvider.authenticate(username, pwd);
|
||||
|
@ -62,7 +73,15 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
|
|||
try {
|
||||
blockUnknown = Boolean.parseBoolean(o.toString());
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage());
|
||||
throw new SolrException(ErrorCode.BAD_REQUEST, "Invalid value for parameter " + PROPERTY_BLOCK_UNKNOWN);
|
||||
}
|
||||
}
|
||||
o = pluginConfig.get(FORWARD_CREDENTIALS);
|
||||
if (o != null) {
|
||||
try {
|
||||
forwardCredentials = Boolean.parseBoolean(o.toString());
|
||||
} catch (Exception e) {
|
||||
throw new SolrException(ErrorCode.BAD_REQUEST, "Invalid value for parameter " + FORWARD_CREDENTIALS);
|
||||
}
|
||||
}
|
||||
authenticationProvider = getAuthenticationProvider(pluginConfig);
|
||||
|
@ -87,7 +106,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
|
|||
ConfigEditablePlugin editablePlugin = (ConfigEditablePlugin) authenticationProvider;
|
||||
return editablePlugin.edit(latestConf, commands);
|
||||
}
|
||||
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "This cannot be edited");
|
||||
throw new SolrException(ErrorCode.BAD_REQUEST, "This cannot be edited");
|
||||
}
|
||||
|
||||
protected AuthenticationProvider getAuthenticationProvider(Map<String, Object> pluginConfig) {
|
||||
|
@ -143,7 +162,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
|
|||
HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
|
||||
@Override
|
||||
public Principal getUserPrincipal() {
|
||||
return new BasicUserPrincipal(username);
|
||||
return new BasicAuthUserPrincipal(username, pwd);
|
||||
}
|
||||
};
|
||||
numAuthenticated.inc();
|
||||
|
@ -198,6 +217,22 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
|
|||
Map<String, String> getPromptHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean interceptInternodeRequest(HttpRequest httpRequest, HttpContext httpContext) {
|
||||
if (forwardCredentials) {
|
||||
if (httpContext instanceof HttpClientContext) {
|
||||
HttpClientContext httpClientContext = (HttpClientContext) httpContext;
|
||||
if (httpClientContext.getUserToken() instanceof BasicAuthUserPrincipal) {
|
||||
BasicAuthUserPrincipal principal = (BasicAuthUserPrincipal) httpClientContext.getUserToken();
|
||||
String userPassBase64 = Base64.encodeBase64String((principal.getName() + ":" + principal.getPassword()).getBytes(StandardCharsets.UTF_8));
|
||||
httpRequest.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + userPassBase64);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidatingJsonMap getSpec() {
|
||||
return authenticationProvider.getSpec();
|
||||
|
@ -208,7 +243,8 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
|
|||
|
||||
public static final String PROPERTY_BLOCK_UNKNOWN = "blockUnknown";
|
||||
public static final String PROPERTY_REALM = "realm";
|
||||
private static final Set<String> PROPS = ImmutableSet.of(PROPERTY_BLOCK_UNKNOWN, PROPERTY_REALM);
|
||||
public static final String FORWARD_CREDENTIALS = "forwardCredentials";
|
||||
private static final Set<String> PROPS = ImmutableSet.of(PROPERTY_BLOCK_UNKNOWN, PROPERTY_REALM, FORWARD_CREDENTIALS);
|
||||
|
||||
/**
|
||||
* Check if the request is an AJAX request, i.e. from the Admin UI or other SPA front
|
||||
|
@ -218,4 +254,51 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
|
|||
private boolean isAjaxRequest(HttpServletRequest request) {
|
||||
return "XMLHttpRequest".equalsIgnoreCase(request.getHeader(X_REQUESTED_WITH_HEADER));
|
||||
}
|
||||
|
||||
@Contract(threading = ThreadingBehavior.IMMUTABLE)
|
||||
private class BasicAuthUserPrincipal implements Principal, Serializable {
|
||||
private String username;
|
||||
private final String password;
|
||||
|
||||
public BasicAuthUserPrincipal(String username, String pwd) {
|
||||
this.username = username;
|
||||
this.password = pwd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean implies(Subject subject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
BasicAuthUserPrincipal that = (BasicAuthUserPrincipal) o;
|
||||
return Objects.equals(username, that.username) &&
|
||||
Objects.equals(password, that.password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(username, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
.append("username", username)
|
||||
.append("pwd", "*****")
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,10 +65,6 @@ public class PKIAuthenticationPlugin extends AuthenticationPlugin implements Htt
|
|||
private final HttpHeaderClientInterceptor interceptor = new HttpHeaderClientInterceptor();
|
||||
private boolean interceptorRegistered = false;
|
||||
|
||||
public void setInterceptorRegistered(){
|
||||
this.interceptorRegistered = true;
|
||||
}
|
||||
|
||||
public boolean isInterceptorRegistered(){
|
||||
return interceptorRegistered;
|
||||
}
|
||||
|
@ -230,6 +226,7 @@ public class PKIAuthenticationPlugin extends AuthenticationPlugin implements Htt
|
|||
@Override
|
||||
public SolrHttpClientBuilder getHttpClientBuilder(SolrHttpClientBuilder builder) {
|
||||
HttpClientUtil.addRequestInterceptor(interceptor);
|
||||
interceptorRegistered = true;
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
@ -244,8 +241,15 @@ public class PKIAuthenticationPlugin extends AuthenticationPlugin implements Htt
|
|||
|
||||
@Override
|
||||
public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
|
||||
if (disabled()) return;
|
||||
if (cores.getAuthenticationPlugin() == null) {
|
||||
return;
|
||||
}
|
||||
if (!cores.getAuthenticationPlugin().interceptInternodeRequest(httpRequest, httpContext)) {
|
||||
log.debug("{} secures this internode request", this.getClass().getSimpleName());
|
||||
setHeader(httpRequest);
|
||||
} else {
|
||||
log.debug("{} secures this internode request", cores.getAuthenticationPlugin().getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -288,14 +292,10 @@ public class PKIAuthenticationPlugin extends AuthenticationPlugin implements Htt
|
|||
return SolrRequestInfo.getRequestInfo();
|
||||
}
|
||||
|
||||
boolean disabled() {
|
||||
return cores.getAuthenticationPlugin() == null ||
|
||||
cores.getAuthenticationPlugin() instanceof HttpClientBuilderPlugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
HttpClientUtil.removeRequestInterceptor(interceptor);
|
||||
interceptorRegistered = false;
|
||||
}
|
||||
|
||||
public String getPublicKey() {
|
||||
|
|
|
@ -72,7 +72,6 @@ import org.apache.solr.metrics.AltBufferPoolMetricSet;
|
|||
import org.apache.solr.metrics.MetricsMap;
|
||||
import org.apache.solr.metrics.OperatingSystemMetricSet;
|
||||
import org.apache.solr.metrics.SolrMetricManager;
|
||||
import org.apache.solr.request.SolrRequestInfo;
|
||||
import org.apache.solr.security.AuthenticationPlugin;
|
||||
import org.apache.solr.security.PKIAuthenticationPlugin;
|
||||
import org.apache.solr.security.PublicKeyHandler;
|
||||
|
@ -168,7 +167,6 @@ public class SolrDispatchFilter extends BaseSolrFilter {
|
|||
extraProperties = new Properties();
|
||||
|
||||
String solrHome = (String) config.getServletContext().getAttribute(SOLRHOME_ATTRIBUTE);
|
||||
ExecutorUtil.addThreadLocalProvider(SolrRequestInfo.getInheritableThreadLocalProvider());
|
||||
|
||||
coresInit = createCoreContainer(solrHome == null ? SolrResourceLoader.locateSolrHome() : Paths.get(solrHome),
|
||||
extraProperties);
|
||||
|
|
|
@ -32,12 +32,13 @@ import java.util.Set;
|
|||
import java.util.concurrent.CompletionService;
|
||||
import java.util.concurrent.ExecutorCompletionService;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.NoHttpResponseException;
|
||||
import org.apache.solr.client.solrj.SolrClient;
|
||||
import org.apache.solr.client.solrj.SolrServerException;
|
||||
import org.apache.solr.client.solrj.impl.BinaryResponseParser;
|
||||
import org.apache.solr.client.solrj.impl.ConcurrentUpdateSolrClient; // jdoc
|
||||
import org.apache.solr.client.solrj.impl.ConcurrentUpdateSolrClient;
|
||||
import org.apache.solr.client.solrj.impl.HttpSolrClient;
|
||||
import org.apache.solr.client.solrj.request.AbstractUpdateRequest;
|
||||
import org.apache.solr.client.solrj.request.UpdateRequest;
|
||||
|
@ -47,6 +48,7 @@ import org.apache.solr.common.cloud.ZkStateReader;
|
|||
import org.apache.solr.common.params.ModifiableSolrParams;
|
||||
import org.apache.solr.common.util.NamedList;
|
||||
import org.apache.solr.core.Diagnostics;
|
||||
import org.apache.solr.request.SolrRequestInfo;
|
||||
import org.apache.solr.update.processor.DistributedUpdateProcessor;
|
||||
import org.apache.solr.update.processor.DistributedUpdateProcessor.LeaderRequestReplicationTracker;
|
||||
import org.apache.solr.update.processor.DistributedUpdateProcessor.RollupRequestReplicationTracker;
|
||||
|
@ -282,6 +284,11 @@ public class SolrCmdDistributor implements Closeable {
|
|||
}
|
||||
|
||||
private void submit(final Req req, boolean isCommit) {
|
||||
// Copy user principal from the original request to the new update request, for later authentication interceptor use
|
||||
if (SolrRequestInfo.getRequestInfo() != null) {
|
||||
req.uReq.setUserPrincipal(SolrRequestInfo.getRequestInfo().getReq().getUserPrincipal());
|
||||
}
|
||||
|
||||
if (req.synchronous) {
|
||||
blockAndDoRetries();
|
||||
|
||||
|
|
|
@ -18,8 +18,6 @@ package org.apache.solr.core;
|
|||
|
||||
import org.apache.solr.SolrTestCaseJ4;
|
||||
import org.apache.solr.common.params.EventParams;
|
||||
import org.apache.solr.common.util.ExecutorUtil;
|
||||
import org.apache.solr.request.SolrRequestInfo;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
|
@ -38,10 +36,6 @@ public class TestQuerySenderListener extends SolrTestCaseJ4 {
|
|||
// in the same VM
|
||||
preInitMockListenerCount = MockEventListener.getCreateCount();
|
||||
|
||||
if (usually()) {
|
||||
// This is set by the SolrDispatchFilter, used in Http calls but not Embedded
|
||||
ExecutorUtil.addThreadLocalProvider(SolrRequestInfo.getInheritableThreadLocalProvider());
|
||||
}
|
||||
initCore("solrconfig-querysender.xml","schema.xml");
|
||||
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ import java.util.Random;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.HttpClient;
|
||||
|
@ -44,14 +45,17 @@ import org.apache.http.message.AbstractHttpMessage;
|
|||
import org.apache.http.message.BasicHeader;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.apache.solr.client.solrj.SolrRequest;
|
||||
import org.apache.solr.client.solrj.SolrServerException;
|
||||
import org.apache.solr.client.solrj.embedded.JettySolrRunner;
|
||||
import org.apache.solr.client.solrj.impl.HttpClientUtil;
|
||||
import org.apache.solr.client.solrj.impl.HttpSolrClient;
|
||||
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
|
||||
import org.apache.solr.client.solrj.request.GenericSolrRequest;
|
||||
import org.apache.solr.client.solrj.request.QueryRequest;
|
||||
import org.apache.solr.client.solrj.request.RequestWriter.StringPayloadContentWriter;
|
||||
import org.apache.solr.client.solrj.request.UpdateRequest;
|
||||
import org.apache.solr.client.solrj.request.V2Request;
|
||||
import org.apache.solr.client.solrj.response.QueryResponse;
|
||||
import org.apache.solr.cloud.SolrCloudAuthTestCase;
|
||||
import org.apache.solr.common.SolrInputDocument;
|
||||
import org.apache.solr.common.cloud.DocCollection;
|
||||
|
@ -229,14 +233,7 @@ public class BasicAuthIntegrationTest extends SolrCloudAuthTestCase {
|
|||
|
||||
executeCommand(baseUrl + authzPrefix, cl,"{set-permission : { name : update , role : admin}}", "harry", "HarryIsUberCool");
|
||||
|
||||
SolrInputDocument doc = new SolrInputDocument();
|
||||
doc.setField("id","4");
|
||||
UpdateRequest update = new UpdateRequest();
|
||||
update.setBasicAuthCredentials("harry","HarryIsUberCool");
|
||||
update.add(doc);
|
||||
update.setCommitWithin(100);
|
||||
cluster.getSolrClient().request(update, COLLECTION);
|
||||
|
||||
addDocument("harry","HarryIsUberCool","id", "4");
|
||||
|
||||
executeCommand(baseUrl + authcPrefix, cl, "{set-property : { blockUnknown: true}}", "harry", "HarryIsUberCool");
|
||||
verifySecurityStatus(cl, baseUrl + authcPrefix, "authentication/blockUnknown", "true", 20, "harry", "HarryIsUberCool");
|
||||
|
@ -276,7 +273,21 @@ public class BasicAuthIntegrationTest extends SolrCloudAuthTestCase {
|
|||
cluster.getSolrClient().request(req, COLLECTION);
|
||||
|
||||
assertAuthMetricsMinimums(20, 8, 8, 1, 2, 0);
|
||||
assertPkiAuthMetricsMinimums(5, 5, 0, 0, 0, 0);
|
||||
assertPkiAuthMetricsMinimums(10, 10, 0, 0, 0, 0);
|
||||
|
||||
addDocument("harry","HarryIsUberCool","id", "5");
|
||||
assertAuthMetricsMinimums(23, 11, 9, 1, 2, 0);
|
||||
assertPkiAuthMetricsMinimums(14, 14, 0, 0, 0, 0);
|
||||
|
||||
// Validate forwardCredentials
|
||||
assertEquals(1, executeQuery(params("q", "id:5"), "harry", "HarryIsUberCool").getResults().getNumFound());
|
||||
assertAuthMetricsMinimums(24, 12, 9, 1, 2, 0);
|
||||
assertPkiAuthMetricsMinimums(18, 18, 0, 0, 0, 0);
|
||||
executeCommand(baseUrl + authcPrefix, cl, "{set-property : { forwardCredentials: true}}", "harry", "HarryIsUberCool");
|
||||
verifySecurityStatus(cl, baseUrl + authcPrefix, "authentication/forwardCredentials", "true", 20, "harry", "HarryIsUberCool");
|
||||
assertEquals(1, executeQuery(params("q", "id:5"), "harry", "HarryIsUberCool").getResults().getNumFound());
|
||||
assertAuthMetricsMinimums(31, 19, 9, 1, 2, 0);
|
||||
assertPkiAuthMetricsMinimums(18, 18, 0, 0, 0, 0);
|
||||
|
||||
executeCommand(baseUrl + authcPrefix, cl, "{set-property : { blockUnknown: false}}", "harry", "HarryIsUberCool");
|
||||
} finally {
|
||||
|
@ -293,6 +304,34 @@ public class BasicAuthIntegrationTest extends SolrCloudAuthTestCase {
|
|||
assertEquals(num, registry0.getMetrics().entrySet().stream().filter(e -> e.getKey().startsWith("SECURITY")).count());
|
||||
}
|
||||
|
||||
private QueryResponse executeQuery(ModifiableSolrParams params, String user, String pass) throws IOException, SolrServerException {
|
||||
SolrRequest req = new QueryRequest(params);
|
||||
req.setBasicAuthCredentials(user, pass);
|
||||
QueryResponse resp = (QueryResponse) req.process(cluster.getSolrClient(), COLLECTION);
|
||||
assertNull(resp.getException());
|
||||
assertEquals(0, resp.getStatus());
|
||||
return resp;
|
||||
}
|
||||
|
||||
private void addDocument(String user, String pass, String... fields) throws IOException, SolrServerException {
|
||||
SolrInputDocument doc = new SolrInputDocument();
|
||||
boolean isKey = true;
|
||||
String key = null;
|
||||
for (String field : fields) {
|
||||
if (isKey) {
|
||||
key = field;
|
||||
isKey = false;
|
||||
} else {
|
||||
doc.setField(key, field);
|
||||
}
|
||||
}
|
||||
UpdateRequest update = new UpdateRequest();
|
||||
update.setBasicAuthCredentials(user, pass);
|
||||
update.add(doc);
|
||||
cluster.getSolrClient().request(update, COLLECTION);
|
||||
update.commit(cluster.getSolrClient(), COLLECTION);
|
||||
}
|
||||
|
||||
public static void executeCommand(String url, HttpClient cl, String payload, String user, String pwd)
|
||||
throws IOException {
|
||||
HttpPost httpPost;
|
||||
|
@ -302,7 +341,9 @@ public class BasicAuthIntegrationTest extends SolrCloudAuthTestCase {
|
|||
httpPost.setEntity(new ByteArrayEntity(payload.getBytes(UTF_8)));
|
||||
httpPost.addHeader("Content-Type", "application/json; charset=UTF-8");
|
||||
r = cl.execute(httpPost);
|
||||
assertEquals(200, r.getStatusLine().getStatusCode());
|
||||
String response = IOUtils.toString(r.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
assertEquals("Non-200 response code. Response was " + response, 200, r.getStatusLine().getStatusCode());
|
||||
assertFalse("Response contained errors: " + response, response.contains("errorMessages"));
|
||||
Utils.consumeFully(r.getEntity());
|
||||
}
|
||||
|
||||
|
|
|
@ -51,11 +51,6 @@ public class TestPKIAuthenticationPlugin extends SolrTestCaseJ4 {
|
|||
super(cores, node, new PublicKeyHandler());
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean disabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
SolrRequestInfo getRequestInfo() {
|
||||
return solrRequestInfo;
|
||||
|
|
|
@ -169,12 +169,14 @@ If your plugin of choice is not supported, you will have to interact with Solr s
|
|||
|
||||
== Securing Inter-Node Requests
|
||||
|
||||
There are a lot of requests that originate from the Solr nodes itself. For example, requests from overseer to nodes, recovery threads, etc. Each Authentication plugin declares whether it is capable of securing inter-node requests or not. If not, Solr will fall back to using a special internode authentication mechanism where each Solr node is a super user and is fully trusted by other Solr nodes, described below.
|
||||
There are a lot of requests that originate from the Solr nodes itself. For example, requests from overseer to nodes, recovery threads, etc. We call these 'inter-node' request. Solr has a special built-in `PKIAuthenticationPlugin` (see below) that will always be available to secure inter-node traffic.
|
||||
|
||||
Each Authentication plugin may also decide to secure inter-node requests on its own. They may do this through the so-called `HttpClientBuilder` mechanism, or they may alternatively choose on a per-request basis whether to delegate to PKI or not by overriding a `interceptInternodeRequest()` method from the base class, where any HTTP headers can be set.
|
||||
|
||||
=== PKIAuthenticationPlugin
|
||||
|
||||
The PKIAuthenticationPlugin is used when there is any request going on between two Solr nodes, and the configured Authentication plugin does not wish to handle inter-node security.
|
||||
The `PKIAuthenticationPlugin` provides a built-in authentication mechanism where each Solr node is a super user and is fully trusted by other Solr nodes through the use of Public Key Infrastructure (PKI). Each Authentication plugn may choose to delegate all or some inter-node traffic to the PKI plugin.
|
||||
|
||||
For each outgoing request `PKIAuthenticationPlugin` adds a special header `'SolrAuth'` which carries the timestamp and principal encrypted using the private key of that node. The public key is exposed through an API so that any node can read it whenever it needs it. Any node who gets the request with that header, would get the public key from the sender and decrypt the information. If it is able to decrypt the data, the request trusted. It is invalid if the timestamp is more than 5 secs old. This assumes that the clocks of different nodes in the cluster are synchronized.
|
||||
For each outgoing request `PKIAuthenticationPlugin` adds a special header `'SolrAuth'` which carries the timestamp and principal encrypted using the private key of that node. The public key is exposed through an API so that any node can read it whenever it needs it. Any node who gets the request with that header, would get the public key from the sender and decrypt the information. If it is able to decrypt the data, the request trusted. It is invalid if the timestamp is more than 5 secs old. This assumes that the clocks of different nodes in the cluster are synchronized. Only traffic from other Solr nodes registered with Zookeeper is trusted.
|
||||
|
||||
The timeout is configurable through a system property called `pkiauth.ttl`. For example, if you wish to bump up the time-to-live to 10 seconds (10000 milliseconds), start each node with a property `'-Dpkiauth.ttl=10000'`.
|
||||
|
|
|
@ -37,13 +37,14 @@ An example `security.json` showing both sections is shown below to show how thes
|
|||
"blockUnknown": true, <2>
|
||||
"class":"solr.BasicAuthPlugin",
|
||||
"credentials":{"solr":"IV0EHq1OnNrj6gvRCwvFwTrZ1+z1oBbnQdiVC3otuq0= Ndd7LKvVBAaZIF0QAVi1ekCfAJXr1GGfLtRUXhgrF8c="}, <3>
|
||||
"realm":"My Solr users" <4>
|
||||
"realm":"My Solr users", <4>
|
||||
"forwardCredentials": false <5>
|
||||
},
|
||||
"authorization":{
|
||||
"class":"solr.RuleBasedAuthorizationPlugin",
|
||||
"permissions":[{"name":"security-edit",
|
||||
"role":"admin"}], <5>
|
||||
"user-role":{"solr":"admin"} <6>
|
||||
"role":"admin"}], <6>
|
||||
"user-role":{"solr":"admin"} <7>
|
||||
}}
|
||||
----
|
||||
|
||||
|
@ -53,8 +54,9 @@ There are several things defined in this file:
|
|||
<2> The parameter `"blockUnknown":true` means that unauthenticated requests are not allowed to pass through.
|
||||
<3> A user called 'solr', with a password `'SolrRocks'` has been defined.
|
||||
<4> We override the `realm` property to display another text on the login prompt.
|
||||
<5> The 'admin' role has been defined, and it has permission to edit security settings.
|
||||
<6> The 'solr' user has been defined to the 'admin' role.
|
||||
<5> The parameter `"forwardCredentials":false` means we let Solr's PKI authenticaion handle distributed request instead of forwarding the Basic Auth header.
|
||||
<6> The 'admin' role has been defined, and it has permission to edit security settings.
|
||||
<7> The 'solr' user has been defined to the 'admin' role.
|
||||
|
||||
Save your settings to a file called `security.json` locally. If you are using Solr in standalone mode, you should put this file in `$SOLR_HOME`.
|
||||
|
||||
|
@ -143,7 +145,7 @@ curl --user solr:SolrRocks http://localhost:8983/api/cluster/security/authentica
|
|||
|
||||
=== Set a Property
|
||||
|
||||
Set properties for the authentication plugin. The currently supported properties for the Basic Authentication plugin are `blockUnknown` and `realm`.
|
||||
Set properties for the authentication plugin. The currently supported properties for the Basic Authentication plugin are `blockUnknown`, `realm` and `forwardCredentials`.
|
||||
|
||||
[.dynamic-tabs]
|
||||
--
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.apache.solr.client.solrj;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.security.Principal;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
|
@ -36,6 +37,16 @@ import static java.util.Collections.unmodifiableSet;
|
|||
* @since solr 1.3
|
||||
*/
|
||||
public abstract class SolrRequest<T extends SolrResponse> implements Serializable {
|
||||
// This user principal is typically used by Auth plugins during distributed/sharded search
|
||||
private Principal userPrincipal;
|
||||
|
||||
public void setUserPrincipal(Principal userPrincipal) {
|
||||
this.userPrincipal = userPrincipal;
|
||||
}
|
||||
|
||||
public Principal getUserPrincipal() {
|
||||
return userPrincipal;
|
||||
}
|
||||
|
||||
public enum METHOD {
|
||||
GET,
|
||||
|
|
|
@ -24,6 +24,7 @@ import java.lang.invoke.MethodHandles;
|
|||
import java.net.ConnectException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Principal;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
@ -252,7 +253,7 @@ public class HttpSolrClient extends SolrClient {
|
|||
throws SolrServerException, IOException {
|
||||
HttpRequestBase method = createMethod(request, collection);
|
||||
setBasicAuthHeader(request, method);
|
||||
return executeMethod(method, processor, isV2ApiRequest(request));
|
||||
return executeMethod(method, request.getUserPrincipal(), processor, isV2ApiRequest(request));
|
||||
}
|
||||
|
||||
private boolean isV2ApiRequest(final SolrRequest request) {
|
||||
|
@ -296,7 +297,7 @@ public class HttpSolrClient extends SolrClient {
|
|||
ExecutorService pool = ExecutorUtil.newMDCAwareFixedThreadPool(1, new SolrjNamedThreadFactory("httpUriRequest"));
|
||||
try {
|
||||
MDC.put("HttpSolrClient.url", baseUrl);
|
||||
mrr.future = pool.submit(() -> executeMethod(method, processor, isV2ApiRequest(request)));
|
||||
mrr.future = pool.submit(() -> executeMethod(method, request.getUserPrincipal(), processor, isV2ApiRequest(request)));
|
||||
|
||||
} finally {
|
||||
pool.shutdown();
|
||||
|
@ -517,7 +518,7 @@ public class HttpSolrClient extends SolrClient {
|
|||
|
||||
private static final List<String> errPath = Arrays.asList("metadata", "error-class");//Utils.getObjectByPath(err, false,"metadata/error-class")
|
||||
|
||||
protected NamedList<Object> executeMethod(HttpRequestBase method, final ResponseParser processor, final boolean isV2Api) throws SolrServerException {
|
||||
protected NamedList<Object> executeMethod(HttpRequestBase method, Principal userPrincipal, final ResponseParser processor, final boolean isV2Api) throws SolrServerException {
|
||||
method.addHeader("User-Agent", AGENT);
|
||||
|
||||
org.apache.http.client.config.RequestConfig.Builder requestConfigBuilder = HttpClientUtil.createDefaultRequestConfigBuilder();
|
||||
|
@ -539,6 +540,12 @@ public class HttpSolrClient extends SolrClient {
|
|||
try {
|
||||
// Execute the method.
|
||||
HttpClientContext httpClientRequestContext = HttpClientUtil.createNewHttpClientRequestContext();
|
||||
if (userPrincipal != null) {
|
||||
// Normally the context contains a static userToken to enable reuse resources.
|
||||
// However, if a personal Principal object exists, we use that instead, also as a means
|
||||
// to transfer authentication information to Auth plugins that wish to intercept the request later
|
||||
httpClientRequestContext.setUserToken(userPrincipal);
|
||||
}
|
||||
final HttpResponse response = httpClient.execute(method, httpClientRequestContext);
|
||||
|
||||
int httpStatus = response.getStatusLine().getStatusCode();
|
||||
|
|
|
@ -67,7 +67,7 @@ public class SolrCloudAuthTestCase extends SolrCloudTestCase {
|
|||
* Common test method to be able to check security from any authentication plugin
|
||||
* @param prefix the metrics key prefix, currently "SECURITY./authentication." for basic auth and "SECURITY./authentication/pki." for PKI
|
||||
*/
|
||||
private void assertAuthMetricsMinimums(String prefix, int requests, int authenticated, int passThrough, int failWrongCredentials, int failMissingCredentials, int errors) {
|
||||
Map<String,Long> countAuthMetrics(String prefix) {
|
||||
List<Map<String, Metric>> metrics = new ArrayList<>();
|
||||
cluster.getJettySolrRunners().forEach(r -> {
|
||||
MetricRegistry registry = r.getCoreContainer().getMetricManager().registry("solr.node");
|
||||
|
@ -79,14 +79,33 @@ public class SolrCloudAuthTestCase extends SolrCloudTestCase {
|
|||
AUTH_METRICS_KEYS.forEach(k -> {
|
||||
counts.put(k, sumCount(prefix, k, metrics));
|
||||
});
|
||||
return counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common test method to be able to check security from any authentication plugin
|
||||
* @param prefix the metrics key prefix, currently "SECURITY./authentication." for basic auth and "SECURITY./authentication/pki." for PKI
|
||||
*/
|
||||
private void assertAuthMetricsMinimums(String prefix, int requests, int authenticated, int passThrough, int failWrongCredentials, int failMissingCredentials, int errors) {
|
||||
Map<String, Long> counts = countAuthMetrics(prefix);
|
||||
|
||||
// check each counter
|
||||
assertExpectedMetrics(requests, "requests", counts);
|
||||
assertExpectedMetrics(authenticated, "authenticated", counts);
|
||||
assertExpectedMetrics(passThrough, "passThrough", counts);
|
||||
assertExpectedMetrics(failWrongCredentials, "failWrongCredentials", counts);
|
||||
assertExpectedMetrics(failMissingCredentials, "failMissingCredentials", counts);
|
||||
assertExpectedMetrics(errors, "errors", counts);
|
||||
boolean success = isMetricEuqalOrLarger(requests, "requests", counts)
|
||||
& isMetricEuqalOrLarger(authenticated, "authenticated", counts)
|
||||
& isMetricEuqalOrLarger(passThrough, "passThrough", counts)
|
||||
& isMetricEuqalOrLarger(failWrongCredentials, "failWrongCredentials", counts)
|
||||
& isMetricEuqalOrLarger(failMissingCredentials, "failMissingCredentials", counts)
|
||||
& isMetricEuqalOrLarger(errors, "errors", counts);
|
||||
|
||||
Map<String, Long> expectedCounts = new HashMap<>();
|
||||
expectedCounts.put("requests", (long) requests);
|
||||
expectedCounts.put("authenticated", (long) authenticated);
|
||||
expectedCounts.put("passThrough", (long) passThrough);
|
||||
expectedCounts.put("failWrongCredentials", (long) failWrongCredentials);
|
||||
expectedCounts.put("failMissingCredentials", (long) failMissingCredentials);
|
||||
expectedCounts.put("errors", (long) errors);
|
||||
assertTrue("Expected metric minimums for prefix " + prefix + ": " + expectedCounts + ", but got: " + counts, success);
|
||||
|
||||
if (counts.get("requests") > 0) {
|
||||
assertTrue("requestTimes count not > 1", counts.get("requestTimes") > 1);
|
||||
assertTrue("totalTime not > 0", counts.get("totalTime") > 0);
|
||||
|
@ -94,11 +113,10 @@ public class SolrCloudAuthTestCase extends SolrCloudTestCase {
|
|||
}
|
||||
|
||||
// Check that the actual metric is equal to or greater than the expected value, never less
|
||||
private void assertExpectedMetrics(int expected, String key, Map<String, Long> counts) {
|
||||
private boolean isMetricEuqalOrLarger(int expected, String key, Map<String, Long> counts) {
|
||||
long cnt = counts.get(key);
|
||||
log.debug("Asserting that auth metrics count ({}) > expected ({})", cnt, expected);
|
||||
assertTrue("Expected " + key + " metric count to be " + expected + " or higher, but got " + cnt,
|
||||
cnt >= expected);
|
||||
return(cnt >= expected);
|
||||
}
|
||||
|
||||
// Have to sum the metrics from all three shards/nodes
|
||||
|
|
Loading…
Reference in New Issue