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

View File

@ -17,6 +17,8 @@
package org.apache.solr.security; package org.apache.solr.security;
import java.io.IOException; import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.security.Principal;
import java.util.Locale; import java.util.Locale;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
@ -25,14 +27,25 @@ import javax.servlet.ServletException;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.hadoop.security.authentication.server.AuthenticationFilter; import org.apache.hadoop.security.authentication.server.AuthenticationFilter;
import org.apache.hadoop.security.authentication.server.AuthenticationHandler; 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 { public class KerberosFilter extends AuthenticationFilter {
private final Locale defaultLocale = Locale.getDefault(); private final Locale defaultLocale = Locale.getDefault();
private final CoreContainer coreContainer;
public KerberosFilter(CoreContainer coreContainer) {
this.coreContainer = coreContainer;
}
@Override @Override
public void init(FilterConfig conf) throws ServletException { public void init(FilterConfig conf) throws ServletException {
super.init(conf); super.init(conf);
@ -51,13 +64,56 @@ public class KerberosFilter extends AuthenticationFilter {
newAuthHandler.setAuthHandler(authHandler); newAuthHandler.setAuthHandler(authHandler);
} }
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@Override @Override
protected void doFilter(FilterChain filterChain, HttpServletRequest request, protected void doFilter(FilterChain filterChain, HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException { HttpServletResponse response) throws IOException, ServletException {
Locale.setDefault(defaultLocale); Locale.setDefault(defaultLocale);
request = substituteOriginalUserRequest(request);
super.doFilter(filterChain, request, response); 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 @Override
public void doFilter(ServletRequest request, ServletResponse response, public void doFilter(ServletRequest request, ServletResponse response,
FilterChain filterChain) throws IOException, ServletException { FilterChain filterChain) throws IOException, ServletException {

View File

@ -33,6 +33,8 @@ import com.fasterxml.jackson.core.JsonGenerator;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.collections.iterators.IteratorEnumeration; import org.apache.commons.collections.iterators.IteratorEnumeration;
import org.apache.hadoop.security.token.delegation.web.DelegationTokenAuthenticationHandler; 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.Http2SolrClient;
import org.apache.solr.client.solrj.impl.Krb5HttpClientBuilder; import org.apache.solr.client.solrj.impl.Krb5HttpClientBuilder;
import org.apache.solr.client.solrj.impl.SolrHttpClientBuilder; 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.SolrException.ErrorCode;
import org.apache.solr.common.cloud.SecurityAwareZkACLProvider; import org.apache.solr.common.cloud.SecurityAwareZkACLProvider;
import org.apache.solr.core.CoreContainer; 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.Logger;
import org.slf4j.LoggerFactory; 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_DO_AS_HTTP_PARAM = "doAs";
public static final String IMPERSONATOR_USER_NAME = "solr.impersonator.user.name"; 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 = static final String DELEGATION_TOKEN_ZK_CLIENT =
"solr.kerberos.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 // pass an attribute-enabled context in order to pass the zkClient
// and because the filter may pass a curator instance. // and because the filter may pass a curator instance.
} else { } else {
kerberosFilter = new KerberosFilter(); kerberosFilter = new KerberosFilter(coreContainer);
} }
log.info("Params: "+params); log.info("Params: "+params);
@ -226,6 +232,7 @@ public class KerberosPlugin extends AuthenticationPlugin implements HttpClientBu
FilterChain chain) throws Exception { FilterChain chain) throws Exception {
log.debug("Request to authenticate using kerberos: "+req); log.debug("Request to authenticate using kerberos: "+req);
kerberosFilter.doFilter(req, rsp, chain); kerberosFilter.doFilter(req, rsp, chain);
String requestContinuesAttr = (String)req.getAttribute(RequestContinuesRecorderAuthenticationHandler.REQUEST_CONTINUES_ATTR); String requestContinuesAttr = (String)req.getAttribute(RequestContinuesRecorderAuthenticationHandler.REQUEST_CONTINUES_ATTR);
if (requestContinuesAttr == null) { if (requestContinuesAttr == null) {
log.warn("Could not find " + RequestContinuesRecorderAuthenticationHandler.REQUEST_CONTINUES_ATTR); 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 @Override
public SolrHttpClientBuilder getHttpClientBuilder(SolrHttpClientBuilder builder) { public SolrHttpClientBuilder getHttpClientBuilder(SolrHttpClientBuilder builder) {
return kerberosBuilder.getBuilder(builder); return kerberosBuilder.getBuilder(builder);

View File

@ -189,6 +189,24 @@ public class RuleBasedAuthorizationPlugin implements AuthorizationPlugin, Config
return MatchStatus.FORBIDDEN; 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 @Override
public void init(Map<String, Object> initInfo) { public void init(Map<String, Object> initInfo) {
mapping.put(null, new WildCardSupportMap()); mapping.put(null, new WildCardSupportMap());

View File

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