blob: 9c419dffdf667376be93cac8b62b28e66d611c65 [file] [log] [blame]
/*
* 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.
*/
package org.apache.ranger.authorization.solr.authorizer;
import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Joiner;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ranger.audit.provider.MiscUtil;
import org.apache.ranger.authorization.hadoop.config.RangerConfiguration;
import org.apache.ranger.plugin.policyengine.RangerAccessRequestImpl;
import org.apache.ranger.plugin.policyengine.RangerAccessResourceImpl;
import org.apache.ranger.plugin.policyengine.RangerAccessResult;
import org.apache.ranger.plugin.service.RangerBasePlugin;
import org.apache.ranger.plugin.util.RangerPerfTracer;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.SolrCore;
import org.apache.solr.handler.component.ResponseBuilder;
import org.apache.solr.handler.component.SearchComponent;
import org.apache.solr.request.LocalSolrQueryRequest;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.security.AuthorizationContext.RequestType;
import org.apache.solr.security.AuthorizationPlugin;
import org.apache.solr.security.AuthorizationResponse;
import org.apache.solr.security.AuthorizationContext;
import org.apache.solr.security.AuthorizationContext.CollectionRequest;
import javax.servlet.http.HttpServletRequest;
public class RangerSolrAuthorizer extends SearchComponent implements AuthorizationPlugin {
private static final Log logger = LogFactory
.getLog(RangerSolrAuthorizer.class);
private static final Log PERF_SOLRAUTH_REQUEST_LOG = RangerPerfTracer.getPerfLogger("solrauth.request");
public static final String SUPERUSER = System.getProperty("solr.authorization.superuser", "solr");
public static final String AUTH_FIELD_PROP = "rangerAuthField";
public static final String DEFAULT_AUTH_FIELD = "ranger_auth";
public static final String ALL_ROLES_TOKEN_PROP = "allRolesToken";
public static final String ENABLED_PROP = "enabled";
public static final String MODE_PROP = "matchMode";
public static final String DEFAULT_MODE_PROP = MatchType.DISJUNCTIVE.toString();
public static final String ALLOW_MISSING_VAL_PROP = "allow_missing_val";
public static final String TOKEN_COUNT_PROP = "tokenCountField";
public static final String DEFAULT_TOKEN_COUNT_FIELD_PROP = "ranger_auth_count";
public static final String QPARSER_PROP = "qParser";
public static final String PROP_USE_PROXY_IP = "xasecure.solr.use_proxy_ip";
public static final String PROP_PROXY_IP_HEADER = "xasecure.solr.proxy_ip_header";
public static final String PROP_SOLR_APP_NAME = "xasecure.solr.app.name";
public static final String KEY_COLLECTION = "collection";
public static final String ACCESS_TYPE_CREATE = "create";
public static final String ACCESS_TYPE_UPDATE = "update";
public static final String ACCESS_TYPE_QUERY = "query";
public static final String ACCESS_TYPE_OTHERS = "others";
public static final String ACCESS_TYPE_ADMIN = "solr_admin";
private static volatile RangerBasePlugin solrPlugin = null;
private RangerSolrAuditHandler auditHandler = null;
boolean useProxyIP = false;
String proxyIPHeader = "HTTP_X_FORWARDED_FOR";
String solrAppName = "Client";
private String authField;
private String allRolesToken;
private boolean enabled;
private MatchType matchMode;
private String tokenCountField;
private boolean allowMissingValue;
private String qParserName;
private enum MatchType {
DISJUNCTIVE,
CONJUNCTIVE
}
public RangerSolrAuthorizer() {
logger.info("RangerSolrAuthorizer()");
}
@Override
public void init(NamedList args) {
SolrParams params = args.toSolrParams();
this.authField = params.get(AUTH_FIELD_PROP, DEFAULT_AUTH_FIELD);
this.allRolesToken = params.get(ALL_ROLES_TOKEN_PROP, "");
this.enabled = params.getBool(ENABLED_PROP, false);
this.matchMode = MatchType.valueOf(params.get(MODE_PROP, DEFAULT_MODE_PROP).toUpperCase());
if (this.matchMode == MatchType.CONJUNCTIVE) {
this.qParserName = params.get(QPARSER_PROP, "subset").trim();
this.allowMissingValue = params.getBool(ALLOW_MISSING_VAL_PROP, false);
this.tokenCountField = params.get(TOKEN_COUNT_PROP, DEFAULT_TOKEN_COUNT_FIELD_PROP);
}
logger.info("RangerSolrAuthorizer.init(): authField={" + authField + "}, allRolesToken={" + allRolesToken +
"}, enabled={" + enabled + "}, matchType={" + matchMode + "}, qParserName={" + qParserName +
"}, allowMissingValue={" + allowMissingValue + "}, tokenCountField={" + tokenCountField + "}");
}
/*
* (non-Javadoc)
*
* @see org.apache.solr.security.SolrAuthorizationPlugin#init(java.util.Map)
*/
@Override
public void init(Map<String, Object> initInfo) {
logger.info("init()");
try {
RangerBasePlugin me = solrPlugin;
if (me == null) {
synchronized(RangerSolrAuthorizer.class) {
me = solrPlugin;
logger.info("RangerSolrAuthorizer(): init called");
if (me == null) {
me = solrPlugin = new RangerBasePlugin("solr", "solr");
}
}
}
solrPlugin.init();
auditHandler = new RangerSolrAuditHandler(solrPlugin.getConfig());
solrPlugin.setResultProcessor(auditHandler);
} catch (Throwable t) {
logger.fatal("Error creating and initializing RangerBasePlugin()");
}
try {
useProxyIP = solrPlugin.getConfig().getBoolean(
PROP_USE_PROXY_IP, useProxyIP);
proxyIPHeader = solrPlugin.getConfig().get(
PROP_PROXY_IP_HEADER, proxyIPHeader);
// First get from the -D property
solrAppName = System.getProperty("solr.kerberos.jaas.appname",
solrAppName);
// Override if required from Ranger properties
solrAppName = solrPlugin.getConfig().get(
PROP_SOLR_APP_NAME, solrAppName);
logger.info("init(): useProxyIP=" + useProxyIP);
logger.info("init(): proxyIPHeader=" + proxyIPHeader);
logger.info("init(): solrAppName=" + solrAppName);
logger.info("init(): KerberosName.rules="
+ MiscUtil.getKerberosNamesRules());
authToJAASFile();
} catch (Throwable t) {
logger.fatal("Error init", t);
}
}
/*
* (non-Javadoc)
*
* @see java.io.Closeable#close()
*/
@Override
public void close() throws IOException {
logger.info("close() called");
try {
solrPlugin.cleanup();
/* Solr shutdown is not graceful so that JVM shutdown hooks
* are not always invoked and the audit store are not flushed. So
* we are forcing a cleanup here.
*/
if (solrPlugin.getAuditProviderFactory() != null) {
solrPlugin.getAuditProviderFactory().shutdown();
}
} catch (Throwable t) {
logger.error("Error cleaning up Ranger plugin. Ignoring error", t);
}
}
/*
* (non-Javadoc)
*
* @see
* org.apache.solr.security.SolrAuthorizationPlugin#authorize(org.apache
* .solr.security.SolrRequestContext)
*/
@Override
public AuthorizationResponse authorize(AuthorizationContext context) {
boolean isDenied = false;
try {
if (logger.isDebugEnabled()) {
logger.debug("==> RangerSolrAuthorizer.authorize()");
logAuthorizationConext(context);
}
RangerPerfTracer perf = null;
if(RangerPerfTracer.isPerfTraceEnabled(PERF_SOLRAUTH_REQUEST_LOG)) {
perf = RangerPerfTracer.getPerfTracer(PERF_SOLRAUTH_REQUEST_LOG, "RangerSolrAuthorizer.authorize()");
}
String userName = getUserName(context);
Set<String> userGroups = getGroupsForUser(userName);
String ip = null;
Date eventTime = new Date();
// // Set the IP
if (useProxyIP) {
ip = context.getHttpHeader(proxyIPHeader);
}
if (ip == null) {
ip = context.getHttpHeader("REMOTE_ADDR");
}
if (ip == null) {
ip = context.getRemoteAddr();
}
// Create the list of requests for access check. Each field is
// broken
// into a request
List<RangerAccessRequestImpl> rangerRequests = new ArrayList<RangerAccessRequestImpl>();
List<CollectionRequest> collectionRequests = context.getCollectionRequests();
if (CollectionUtils.isEmpty(collectionRequests)) {
// if Collection is empty we set the collection to *. This happens when LIST is done.
RangerAccessRequestImpl requestForCollection = createRequest(
userName, userGroups, ip, eventTime, context,
null);
if (requestForCollection != null) {
rangerRequests.add(requestForCollection);
}
} else {
// Create the list of requests for access check. Each field is
// broken
// into a request
for (CollectionRequest collectionRequest : context
.getCollectionRequests()) {
RangerAccessRequestImpl requestForCollection = createRequest(
userName, userGroups, ip, eventTime, context,
collectionRequest);
if (requestForCollection != null) {
rangerRequests.add(requestForCollection);
}
}
}
if (logger.isDebugEnabled()) {
logger.debug("rangerRequests.size()=" + rangerRequests.size());
}
try {
// Let's check the access for each request/resource
for (RangerAccessRequestImpl rangerRequest : rangerRequests) {
RangerAccessResult result = solrPlugin.isAccessAllowed(
rangerRequest, auditHandler);
if (logger.isDebugEnabled()) {
logger.debug("rangerRequest=" + result);
}
if (result == null || !result.getIsAllowed()) {
isDenied = true;
// rejecting on first failure
break;
}
}
} finally {
auditHandler.flushAudit();
RangerPerfTracer.log(perf);
}
} catch (Throwable t) {
isDenied = true;
MiscUtil.logErrorMessageByInterval(logger, t.getMessage(), t);
}
AuthorizationResponse response = null;
if (isDenied) {
response = new AuthorizationResponse(403);
} else {
response = new AuthorizationResponse(200);
}
if (logger.isDebugEnabled()) {
logger.debug( "<== RangerSolrAuthorizer.authorize() result: " + isDenied + "Response : " + response.getMessage());
}
return response;
}
@Override
public void prepare(ResponseBuilder rb) throws IOException {
if (!enabled) {
return;
}
String userName = getUserName(rb.req);
if (SUPERUSER.equals(userName)) {
return;
}
Set<String> roles = getRolesForUser(userName);
if (roles != null && !roles.isEmpty()) {
String filterQuery;
if (matchMode == MatchType.DISJUNCTIVE) {
filterQuery = getDisjunctiveFilterQueryStr(roles);
} else {
filterQuery = getConjunctiveFilterQueryStr(roles);
}
ModifiableSolrParams newParams = new ModifiableSolrParams(rb.req.getParams());
newParams.add("fq", filterQuery);
rb.req.setParams(newParams);
if (logger.isDebugEnabled()) {
logger.debug("Adding filter query {" + filterQuery + "} for user {" + userName + "} with roles {" + roles + "}");
}
} else {
throw new SolrException(SolrException.ErrorCode.UNAUTHORIZED,
"Request from user: " + userName + " rejected because user is not associated with any roles");
}
}
@Override
public void process(ResponseBuilder rb) throws IOException {
}
@Override
public String getDescription() {
return "Handle Query Document Authorization";
}
private void authToJAASFile() {
try {
MiscUtil.setUGIFromJAASConfig(solrAppName);
logger.info("LoginUser=" + MiscUtil.getUGILoginUser());
} catch (Throwable t) {
logger.error("Error authenticating for appName=" + solrAppName, t);
}
}
/**
* @param context
*/
private void logAuthorizationConext(AuthorizationContext context) {
try {
// Note: This method should be called with isDebugEnabled()
String collections = "";
int i = -1;
for (CollectionRequest collectionRequest : context
.getCollectionRequests()) {
i++;
if (i > 0) {
collections += ",";
}
collections += collectionRequest.collectionName;
}
String headers = "";
i = -1;
@SuppressWarnings("unchecked")
Enumeration<String> eList = context.getHeaderNames();
while (eList.hasMoreElements()) {
i++;
if (i > 0) {
headers += ",";
}
String header = eList.nextElement();
String value = context.getHttpHeader(header);
headers += header + "=" + value;
}
String ipAddress = context.getHttpHeader("HTTP_X_FORWARDED_FOR");
if (ipAddress == null) {
ipAddress = context.getHttpHeader("REMOTE_HOST");
}
if (ipAddress == null) {
ipAddress = context.getHttpHeader("REMOTE_ADDR");
}
if (ipAddress == null) {
ipAddress = context.getRemoteAddr();
}
String userName = getUserName(context);
Set<String> groups = getGroupsForUser(userName);
String resource = context.getResource();
String solrParams = "";
try {
solrParams = context.getParams().toQueryString();
} catch (Throwable t) {
//Exception ignored
}
RequestType requestType = context.getRequestType();
String accessType = mapToRangerAccessType(context);
Principal principal = context.getUserPrincipal();
String contextString = new String("AuthorizationContext: ");
contextString = contextString + "context.getResource()= " + ((resource != null ) ? resource : "");
contextString = contextString + ", solarParams= " + (( solrParams != null ) ? solrParams : "");
contextString = contextString + ", requestType= " + (( requestType != null ) ? requestType : "");
contextString = contextString + ", ranger.requestType= " + ((accessType != null ) ? accessType : "");
contextString = contextString + ", userPrincipal= " + ((principal != null ) ? principal : "");
contextString = contextString + ", userName= " + userName;
contextString = contextString + ", groups= " + groups;
contextString = contextString + ", ipAddress= " + ipAddress;
contextString = contextString + ", collections= " + collections;
contextString = contextString + ", headers= " + headers;
logger.debug(contextString);
} catch (Throwable t) {
logger.error("Error getting request context!!!", t);
}
}
/**
* @param userName
* @param userGroups
* @param ip
* @param eventTime
* @param context
* @param collectionRequest
* @return
*/
private RangerAccessRequestImpl createRequest(String userName,
Set<String> userGroups, String ip, Date eventTime,
AuthorizationContext context, CollectionRequest collectionRequest) {
String accessType = mapToRangerAccessType(context);
String action = accessType;
RangerAccessRequestImpl rangerRequest = createBaseRequest(userName,
userGroups, ip, eventTime);
RangerAccessResourceImpl rangerResource = new RangerAccessResourceImpl();
if (collectionRequest == null) {
rangerResource.setValue(KEY_COLLECTION, "*");
} else {
rangerResource.setValue(KEY_COLLECTION, collectionRequest.collectionName);
}
rangerRequest.setResource(rangerResource);
rangerRequest.setAccessType(accessType);
rangerRequest.setAction(action);
return rangerRequest;
}
private RangerAccessRequestImpl createBaseRequest(String userName,
Set<String> userGroups, String ip, Date eventTime) {
RangerAccessRequestImpl rangerRequest = new RangerAccessRequestImpl();
if (userName != null && !userName.isEmpty()) {
rangerRequest.setUser(userName);
}
if (userGroups != null && userGroups.size() > 0) {
rangerRequest.setUserGroups(userGroups);
}
if (ip != null && !ip.isEmpty()) {
rangerRequest.setClientIPAddress(ip);
}
rangerRequest.setAccessTime(eventTime);
return rangerRequest;
}
private String getUserName(AuthorizationContext context) {
Principal principal = context.getUserPrincipal();
if (principal != null) {
return MiscUtil.getShortNameFromPrincipalName(principal.getName());
}
return null;
}
/**
* @param name
* @return
*/
private Set<String> getGroupsForUser(String name) {
return MiscUtil.getGroupsForRequestUser(name);
}
String mapToRangerAccessType(AuthorizationContext context) {
String accessType = ACCESS_TYPE_OTHERS;
RequestType requestType = context.getRequestType();
if (RequestType.ADMIN.equals(requestType)) {
accessType = ACCESS_TYPE_ADMIN;
} else if (RequestType.READ.equals(requestType)) {
accessType = ACCESS_TYPE_QUERY;
} else if (RequestType.WRITE.equals(requestType)) {
accessType = ACCESS_TYPE_UPDATE;
} else if (RequestType.UNKNOWN.equals(requestType)) {
logger.info("UNKNOWN request type. Mapping it to " + accessType
+ ". Resource=" + context.getResource());
accessType = ACCESS_TYPE_OTHERS;
} else {
logger.info("Request type is not supported. requestType="
+ requestType + ". Mapping it to " + accessType
+ ". Resource=" + context.getResource());
}
return accessType;
}
private void addDisjunctiveRawClause(StringBuilder builder, String value) {
// requires a space before the first term, so the
// default lucene query parser will be used
builder.append(" {!raw f=").append(authField).append(" v=")
.append(value).append("}");
}
private String getDisjunctiveFilterQueryStr(Set<String> roles) {
if (roles != null && !roles.isEmpty()) {
StringBuilder builder = new StringBuilder();
for (String role : roles) {
addDisjunctiveRawClause(builder, role);
}
if (allRolesToken != null && !allRolesToken.isEmpty()) {
addDisjunctiveRawClause(builder, allRolesToken);
}
return builder.toString();
}
return null;
}
private String getConjunctiveFilterQueryStr(Set<String> roles) {
StringBuilder filterQuery = new StringBuilder();
filterQuery
.append(" {!").append(qParserName)
.append(" set_field=\"").append(authField).append("\"")
.append(" set_value=\"").append(Joiner.on(',').join(roles.iterator())).append("\"")
.append(" count_field=\"").append(tokenCountField).append("\"");
if (allRolesToken != null && !allRolesToken.equals("")) {
filterQuery.append(" wildcard_token=\"").append(allRolesToken).append("\"");
}
filterQuery.append(" allow_missing_val=").append(allowMissingValue).append(" }");
return filterQuery.toString();
}
private Set<String> getRolesForUser(String name) {
if (solrPlugin.getCurrentRangerAuthContext() != null) {
return solrPlugin.getCurrentRangerAuthContext().getRolesFromUserAndGroups(name, getGroupsForUser(name));
}
else {
logger.info("Current Ranger Auth Context is null!!");
return null;
}
}
/**
* This method return the user name from the provided {@linkplain SolrQueryRequest}
*/
private final String getUserName(SolrQueryRequest req) {
// If a local request, treat it like a super user request; i.e. it is equivalent to an
// http request from the same process.
if (req instanceof LocalSolrQueryRequest) {
return SUPERUSER;
}
SolrCore solrCore = req.getCore();
HttpServletRequest httpServletRequest = (HttpServletRequest) req.getContext().get("httpRequest");
if (httpServletRequest == null) {
StringBuilder builder = new StringBuilder("Unable to locate HttpServletRequest");
if (solrCore != null && !solrCore.getSolrConfig().getBool("requestDispatcher/requestParsers/@addHttpRequestToContext", true)) {
builder.append(", ensure requestDispatcher/requestParsers/@addHttpRequestToContext is set to true in solrconfig.xml");
}
throw new SolrException(SolrException.ErrorCode.UNAUTHORIZED, builder.toString());
}
String userName = httpServletRequest.getRemoteUser();
if (userName == null) {
userName = MiscUtil.getShortNameFromPrincipalName(httpServletRequest.getUserPrincipal().getName());
}
if (userName == null) {
throw new SolrException(SolrException.ErrorCode.UNAUTHORIZED, "This request is not authenticated.");
}
return userName;
}
}