SOLR-13619: Kerberos plugin to forward original user principal

This commit is contained in:
Ishan Chattopadhyaya 2019-07-15 15:10:07 +05:30
parent 7e0af71c1e
commit 26ede632e6
6 changed files with 127 additions and 5 deletions

View File

@ -223,6 +223,9 @@ Bug Fixes
* SOLR-13472: Forwarded requests should skip authorization on receiving nodes (adfel, Ishan Chattopadhyaya)
* SOLR-13619: Kerberos plugin to pass original user principal to avoid 403 on nodes not hosting a collection
(adfel, Ishan Chattopadhyaya, noble)
Other Changes
----------------------

View File

@ -31,6 +31,7 @@ import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.util.ExecutorUtil;
import org.apache.solr.handler.component.ResponseBuilder;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.servlet.SolrDispatchFilter;
import org.apache.solr.util.TimeZoneUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -46,6 +47,7 @@ public class SolrRequestInfo {
protected TimeZone tz;
protected ResponseBuilder rb;
protected List<Closeable> closeHooks;
protected SolrDispatchFilter.Action action;
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@ -86,11 +88,21 @@ public class SolrRequestInfo {
this.req = req;
this.rsp = rsp;
}
public SolrRequestInfo(HttpServletRequest httpReq, SolrQueryResponse rsp) {
public SolrRequestInfo(SolrQueryRequest req, SolrQueryResponse rsp, SolrDispatchFilter.Action action) {
this(req, rsp);
this.setAction(action);
}
public SolrRequestInfo(HttpServletRequest httpReq, SolrQueryResponse rsp) {
this.httpRequest = httpReq;
this.rsp = rsp;
}
public SolrRequestInfo(HttpServletRequest httpReq, SolrQueryResponse rsp, SolrDispatchFilter.Action action) {
this(httpReq, rsp);
this.action = action;
}
public Principal getUserPrincipal() {
if (req != null) return req.getUserPrincipal();
if (httpRequest != null) return httpRequest.getUserPrincipal();
@ -149,6 +161,14 @@ public class SolrRequestInfo {
}
}
public SolrDispatchFilter.Action getAction() {
return action;
}
public void setAction(SolrDispatchFilter.Action action) {
this.action = action;
}
public static ExecutorUtil.InheritableThreadLocalProvider getInheritableThreadLocalProvider() {
return new ExecutorUtil.InheritableThreadLocalProvider() {
@Override

View File

@ -17,6 +17,8 @@
package org.apache.solr.security;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.security.Principal;
import java.util.Locale;
import javax.servlet.FilterChain;
@ -25,13 +27,24 @@ 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 org.apache.hadoop.security.authentication.server.AuthenticationFilter;
import org.apache.hadoop.security.authentication.server.AuthenticationHandler;
import org.apache.solr.core.CoreContainer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class KerberosFilter extends AuthenticationFilter {
private final Locale defaultLocale = Locale.getDefault();
private final CoreContainer coreContainer;
public KerberosFilter(CoreContainer coreContainer) {
this.coreContainer = coreContainer;
}
@Override
public void init(FilterConfig conf) throws ServletException {
@ -51,13 +64,56 @@ public class KerberosFilter extends AuthenticationFilter {
newAuthHandler.setAuthHandler(authHandler);
}
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@Override
protected void doFilter(FilterChain filterChain, HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException {
Locale.setDefault(defaultLocale);
request = substituteOriginalUserRequest(request);
super.doFilter(filterChain, request, response);
}
/**
* If principal is an admin user, i.e. has ALL permissions (e.g. request coming from Solr
* node), and "originalUserPrincipal" is specified, then set originalUserPrincipal
* as the principal. This is the case in forwarded/remote requests
* through KerberosPlugin. This is needed because the original node that received
* this request did not perform any authorization, and hence we are the first ones
* to authorize the request (and we need the original user principal to do so).
* @return Substituted request, if applicable, or the original request
*/
private HttpServletRequest substituteOriginalUserRequest(HttpServletRequest request) {
final HttpServletRequest originalRequest = request;
AuthorizationPlugin authzPlugin = coreContainer.getAuthorizationPlugin();
if (authzPlugin instanceof RuleBasedAuthorizationPlugin) {
RuleBasedAuthorizationPlugin ruleBased = (RuleBasedAuthorizationPlugin) authzPlugin;
if (request.getHeader(KerberosPlugin.ORIGINAL_USER_PRINCIPAL_HEADER) != null &&
ruleBased.doesUserHavePermission(request.getUserPrincipal().getName(), PermissionNameProvider.Name.ALL)) {
request = new HttpServletRequestWrapper(request) {
@Override
public Principal getUserPrincipal() {
String originalUserPrincipal = originalRequest.getHeader(KerberosPlugin.ORIGINAL_USER_PRINCIPAL_HEADER);
log.info("Substituting user principal from {} to {}.", originalRequest.getUserPrincipal(), originalUserPrincipal);
return new Principal() {
@Override
public String getName() {
return originalUserPrincipal;
}
@Override
public String toString() {
return originalUserPrincipal;
}
};
}
};
}
}
return request;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain filterChain) throws IOException, ServletException {

View File

@ -33,6 +33,8 @@ import com.fasterxml.jackson.core.JsonGenerator;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.collections.iterators.IteratorEnumeration;
import org.apache.hadoop.security.token.delegation.web.DelegationTokenAuthenticationHandler;
import org.apache.http.HttpRequest;
import org.apache.http.protocol.HttpContext;
import org.apache.solr.client.solrj.impl.Http2SolrClient;
import org.apache.solr.client.solrj.impl.Krb5HttpClientBuilder;
import org.apache.solr.client.solrj.impl.SolrHttpClientBuilder;
@ -41,6 +43,8 @@ import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.cloud.SecurityAwareZkACLProvider;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.request.SolrRequestInfo;
import org.apache.solr.servlet.SolrDispatchFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -71,6 +75,8 @@ public class KerberosPlugin extends AuthenticationPlugin implements HttpClientBu
public static final String IMPERSONATOR_DO_AS_HTTP_PARAM = "doAs";
public static final String IMPERSONATOR_USER_NAME = "solr.impersonator.user.name";
public static final String ORIGINAL_USER_PRINCIPAL_HEADER = "originalUserPrincipal";
static final String DELEGATION_TOKEN_ZK_CLIENT =
"solr.kerberos.delegation.token.zk.client";
@ -177,7 +183,7 @@ public class KerberosPlugin extends AuthenticationPlugin implements HttpClientBu
// pass an attribute-enabled context in order to pass the zkClient
// and because the filter may pass a curator instance.
} else {
kerberosFilter = new KerberosFilter();
kerberosFilter = new KerberosFilter(coreContainer);
}
log.info("Params: "+params);
@ -226,6 +232,7 @@ public class KerberosPlugin extends AuthenticationPlugin implements HttpClientBu
FilterChain chain) throws Exception {
log.debug("Request to authenticate using kerberos: "+req);
kerberosFilter.doFilter(req, rsp, chain);
String requestContinuesAttr = (String)req.getAttribute(RequestContinuesRecorderAuthenticationHandler.REQUEST_CONTINUES_ATTR);
if (requestContinuesAttr == null) {
log.warn("Could not find " + RequestContinuesRecorderAuthenticationHandler.REQUEST_CONTINUES_ATTR);
@ -235,6 +242,20 @@ public class KerberosPlugin extends AuthenticationPlugin implements HttpClientBu
}
}
@Override
protected boolean interceptInternodeRequest(HttpRequest httpRequest, HttpContext httpContext) {
SolrRequestInfo info = SolrRequestInfo.getRequestInfo();
if (info != null && (info.getAction() == SolrDispatchFilter.Action.FORWARD ||
info.getAction() == SolrDispatchFilter.Action.REMOTEQUERY)) {
if (info.getUserPrincipal() != null) {
log.info("Setting original user principal: {}", info.getUserPrincipal().getName());
httpRequest.setHeader(ORIGINAL_USER_PRINCIPAL_HEADER, info.getUserPrincipal().getName());
return true;
}
}
return false;
}
@Override
public SolrHttpClientBuilder getHttpClientBuilder(SolrHttpClientBuilder builder) {
return kerberosBuilder.getBuilder(builder);

View File

@ -189,6 +189,24 @@ public class RuleBasedAuthorizationPlugin implements AuthorizationPlugin, Config
return MatchStatus.FORBIDDEN;
}
public boolean doesUserHavePermission(String user, PermissionNameProvider.Name permission) {
Set<String> roles = usersVsRoles.get(user);
if (roles != null) {
for (String role: roles) {
if (mapping.get(null) == null) continue;
List<Permission> permissions = mapping.get(null).get(null);
if (permissions != null) {
for (Permission p: permissions) {
if (permission.equals(p.wellknownName) && p.role.contains(role)) {
return true;
}
}
}
}
}
return false;
}
@Override
public void init(Map<String, Object> initInfo) {
mapping.put(null, new WildCardSupportMap());

View File

@ -137,6 +137,8 @@ import static org.apache.solr.servlet.SolrDispatchFilter.Action.RETURN;
public class HttpSolrCall {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static final String ORIGINAL_USER_PRINCIPAL_HEADER = "originalUserPrincipal";
public static final Random random;
static {
// We try to make things reproducible in the context of our tests by initializing the random instance
@ -414,10 +416,12 @@ public class HttpSolrCall {
if (path.equals(req.getServletPath())) {
// avoid endless loop - pass through to Restlet via webapp
action = PASSTHROUGH;
SolrRequestInfo.getRequestInfo().setAction(action);
return;
} else {
// forward rewritten URI (without path prefix and core/collection name) to Restlet
action = FORWARD;
SolrRequestInfo.getRequestInfo().setAction(action);
return;
}
}
@ -542,7 +546,7 @@ public class HttpSolrCall {
handleAdminRequest();
return RETURN;
case REMOTEQUERY:
SolrRequestInfo.setRequestInfo(new SolrRequestInfo(req, new SolrQueryResponse()));
SolrRequestInfo.setRequestInfo(new SolrRequestInfo(req, new SolrQueryResponse(), action));
remoteQuery(coreUrl + path, resp);
return RETURN;
case PROCESS:
@ -558,7 +562,7 @@ public class HttpSolrCall {
* QueryResponseWriter is selected and we get the correct
* Content-Type)
*/
SolrRequestInfo.setRequestInfo(new SolrRequestInfo(solrReq, solrRsp));
SolrRequestInfo.setRequestInfo(new SolrRequestInfo(solrReq, solrRsp, action));
execute(solrRsp);
if (shouldAudit()) {
EventType eventType = solrRsp.getException() == null ? EventType.COMPLETED : EventType.ERROR;