blob: a0652205005afd8200889e01375bb41255563f9b [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.sentry.binding.solr.authz;
import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION;
import static org.apache.sentry.binding.solr.authz.SolrAuthzBinding.QUERY;
import static org.apache.sentry.binding.solr.authz.SolrAuthzBinding.UPDATE;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.authentication.util.KerberosName;
import org.apache.http.auth.BasicUserPrincipal;
import org.apache.sentry.binding.solr.conf.SolrAuthzConf;
import org.apache.sentry.core.common.Subject;
import org.apache.sentry.core.common.exception.SentryUserException;
import org.apache.sentry.core.model.solr.AdminOperation;
import org.apache.sentry.core.model.solr.Collection;
import org.apache.sentry.core.model.solr.SolrConstants;
import org.apache.sentry.core.model.solr.SolrModelAction;
import org.apache.sentry.core.model.solr.SolrModelAuthorizable;
import org.apache.sentry.provider.file.SimpleFileProviderBackend;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.params.CoreAdminParams;
import org.apache.solr.security.AuthorizationContext;
import org.apache.solr.security.AuthorizationContext.CollectionRequest;
import org.apache.solr.security.AuthorizationPlugin;
import org.apache.solr.security.AuthorizationResponse;
import org.apache.solr.security.PermissionNameProvider;
import org.apache.solr.security.PermissionNameProvider.Name;
import org.apache.solr.sentry.AuditLogger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Preconditions;
/**
* A concrete implementation of Solr {@linkplain AuthorizationPlugin} backed by Sentry.
*
*/
public class SentrySolrPluginImpl implements AuthorizationPlugin {
private static final Logger LOG = LoggerFactory.getLogger(SentrySolrPluginImpl.class);
/**
* A property specifies the value of the prefix to be used to define Java system property
* for configuring the authentication mechanism. The name of the Java system property is
* defined by appending the configuration parmeter namne to this prefix value e.g. if prefix
* is 'solr' then the Java system property 'solr.kerberos.principal' defines the value of
* configuration parameter 'kerberos.principal'.
*/
private static final String SYSPROP_PREFIX_PROPERTY = "sysPropPrefix";
/**
* A property specifying the configuration parameters required by the Sentry authorization
* plugin.
*/
private static final String AUTH_CONFIG_NAMES_PROPERTY = "authConfigs";
/**
* A property specifying the default values for the configuration parameters specified by the
* {@linkplain #AUTH_CONFIG_NAMES_PROPERTY} property. The default values are specified as a
* collection of key-value pairs (i.e. property-name : default_value).
*/
private static final String DEFAULT_AUTH_CONFIGS_PROPERTY = "defaultConfigs";
/**
* A configuration property specifying location of sentry-site.xml
*/
public static final String SNTRY_SITE_LOCATION_PROPERTY = "authorization.sentry.site";
/**
* A configuration property specifying the Solr super-user name. The Sentry permissions
* check will be skipped if the request is authenticated with this user name.
*/
public static final String SENTRY_SOLR_AUTH_SUPERUSER = "authorization.superuser";
/**
* A configuration property to enable audit log for the Solr operations. Please note that
* audit log is available only for operations handled by the Solr authorization framework.
*/
public static final String SENTRY_ENABLE_SOLR_AUDITLOG = "authorization.enable.auditlog";
/**
* A configuration property to specify the location of Hadoop configuration files (specifically
* core-site.xml) required to properly setup Hadoop {@linkplain UserGroupInformation} context.
*/
public static final String SENTRY_HADOOP_CONF_DIR_PROPERTY = "authorization.sentry.hadoop.conf";
/**
* A configuration property to specify the kerberos principal to be used for communicating with
* HDFS. This is required only in case of {@linkplain SimpleFileProviderBackend} when the policy
* file is stored on HDFS.
*/
public static final String SENTRY_HDFS_KERBEROS_PRINCIPAL = "authorization.hdfs.kerberos.principal";
/**
* A configuration property to specify the kerberos keytab file to be used for communicating with
* HDFS. This is required only in case of {@linkplain SimpleFileProviderBackend} when the policy
* file is stored on HDFS.
*/
public static final String SENTRY_HDFS_KERBEROS_KEYTAB = "authorization.hdfs.kerberos.keytabfile";
private String solrSuperUser;
private SolrAuthzBinding binding;
private Optional<AuditLogger> auditLog = Optional.empty();
@SuppressWarnings("unchecked")
@Override
public void init(Map<String, Object> pluginConfig) {
Map<String, String> params = new HashMap<>();
String sysPropPrefix = (String) pluginConfig.getOrDefault(SYSPROP_PREFIX_PROPERTY, "solr.");
java.util.Collection<String> authConfigNames = (java.util.Collection<String>) pluginConfig.
getOrDefault(AUTH_CONFIG_NAMES_PROPERTY, Collections.emptyList());
Map<String,String> authConfigDefaults = (Map<String,String>) pluginConfig
.getOrDefault(DEFAULT_AUTH_CONFIGS_PROPERTY, Collections.emptyMap());
for ( String configName : authConfigNames) {
String systemProperty = sysPropPrefix + configName;
String defaultConfigVal = authConfigDefaults.get(configName);
String configVal = System.getProperty(systemProperty, defaultConfigVal);
if (configVal != null) {
params.put(configName, configVal);
}
}
initializeSentry(params);
}
@Override
public void close() throws IOException {
if (this.binding != null) {
this.binding.close();
}
}
@Override
public AuthorizationResponse authorize(AuthorizationContext authCtx) {
if (authCtx.getUserPrincipal() == null) { // Request not authenticated.
return AuthorizationResponse.PROMPT;
}
if (LOG.isDebugEnabled()) {
LOG.debug("Authorizing a request with authorization context {} ", SolrAuthzUtil.toString(authCtx));
}
String userNameStr = getShortUserName(authCtx.getUserPrincipal());
if (this.solrSuperUser.equals(userNameStr)) {
return AuthorizationResponse.OK;
}
if (authCtx.getHandler() instanceof PermissionNameProvider) {
Subject userName = new Subject(userNameStr);
Name perm = ((PermissionNameProvider) authCtx.getHandler()).getPermissionName(authCtx);
switch (perm) {
case READ_PERM:
case UPDATE_PERM: {
AuthorizationResponse resp = AuthorizationResponse.FORBIDDEN;
Set<SolrModelAction> actions = (perm == Name.READ_PERM) ? QUERY : UPDATE;
for (CollectionRequest req : authCtx.getCollectionRequests()) {
resp = binding.authorizeCollection(userName,
new Collection(req.collectionName), actions);
if (!AuthorizationResponse.OK.equals(resp)) {
break;
}
}
audit (perm, authCtx, resp);
return resp;
}
case SECURITY_EDIT_PERM: {
return binding.authorize(userName, Collections.singleton(AdminOperation.SECURITY), UPDATE);
}
case SECURITY_READ_PERM: {
return binding.authorize(userName, Collections.singleton(AdminOperation.SECURITY), QUERY);
}
case CORE_READ_PERM:
case CORE_EDIT_PERM:
case COLL_READ_PERM:
case COLL_EDIT_PERM: {
AuthorizationResponse resp = AuthorizationResponse.FORBIDDEN;
SolrModelAuthorizable auth = (perm == Name.COLL_READ_PERM || perm == Name.COLL_EDIT_PERM)
? AdminOperation.COLLECTIONS : AdminOperation.CORES;
Set<SolrModelAction> actions = (perm == Name.COLL_READ_PERM || perm == Name.CORE_READ_PERM)
? QUERY : UPDATE;
resp = binding.authorize(userName, Collections.singleton(auth), actions);
audit (perm, authCtx, resp);
if (AuthorizationResponse.OK.equals(resp)) {
// Apply collection/core-level permissions check as well.
for (Map.Entry<String, SolrModelAction> entry :
SolrAuthzUtil.getCollectionsForAdminOp(authCtx).entrySet()) {
resp = binding.authorizeCollection(userName,
new Collection(entry.getKey()), Collections.singleton(entry.getValue()));
Name p = entry.getValue().equals(SolrModelAction.UPDATE) ? Name.UPDATE_PERM : Name.READ_PERM;
audit(p, authCtx, resp);
if (!AuthorizationResponse.OK.equals(resp)) {
break;
}
}
}
return resp;
}
case CONFIG_EDIT_PERM: {
return binding.authorize(userName, SolrAuthzUtil.getConfigAuthorizables(authCtx), UPDATE);
}
case CONFIG_READ_PERM: {
return binding.authorize(userName, SolrAuthzUtil.getConfigAuthorizables(authCtx), QUERY);
}
case SCHEMA_EDIT_PERM: {
return binding.authorize(userName, SolrAuthzUtil.getSchemaAuthorizables(authCtx), UPDATE);
}
case SCHEMA_READ_PERM: {
return binding.authorize(userName, SolrAuthzUtil.getSchemaAuthorizables(authCtx), QUERY);
}
case METRICS_HISTORY_READ_PERM:
case METRICS_READ_PERM: {
return binding.authorize(userName, Collections.singleton(AdminOperation.METRICS), QUERY);
}
case AUTOSCALING_READ_PERM:
case AUTOSCALING_HISTORY_READ_PERM: {
return binding.authorize(userName, Collections.singleton(AdminOperation.AUTOSCALING), QUERY);
}
case AUTOSCALING_WRITE_PERM: {
return binding.authorize(userName, Collections.singleton(AdminOperation.AUTOSCALING), UPDATE);
}
case ALL: {
return AuthorizationResponse.OK;
}
}
}
/*
* The switch-case statement above handles all possible permission types. Some of the request handlers
* in SOLR do not implement PermissionNameProvider interface and hence are incapable to providing the
* type of permission to be enforced for this request. This is a design limitation (or a bug) on the SOLR
* side. Until that issue is resolved, Solr/Sentry plugin needs to return OK for such requests.
* Ref: SOLR-11623
*/
return AuthorizationResponse.OK;
}
/**
* This method returns the roles associated with the specified user name.
*/
public Set<String> getRoles (String userName) throws SentryUserException {
return binding.getRoles(userName);
}
private void initializeSentry(Map<String, String> config) {
String sentrySiteLoc =
Preconditions.checkNotNull(config.get(SNTRY_SITE_LOCATION_PROPERTY),
"The authorization plugin configuration is missing " + SNTRY_SITE_LOCATION_PROPERTY
+ " property");
String sentryHadoopConfLoc = (String)config.get(SENTRY_HADOOP_CONF_DIR_PROPERTY);
try {
List<URL> configFiles = getHadoopConfigFiles(sentryHadoopConfLoc);
configFiles.add((new File(sentrySiteLoc)).toURI().toURL());
SolrAuthzConf conf = new SolrAuthzConf(configFiles);
if (shouldInitializeKereberos(conf)) {
String princ = Preconditions.checkNotNull(config.get(SENTRY_HDFS_KERBEROS_PRINCIPAL),
"The authorization plugin is missing the " + SENTRY_HDFS_KERBEROS_PRINCIPAL + " property.");
String keytab = Preconditions.checkNotNull(config.get(SENTRY_HDFS_KERBEROS_KEYTAB),
"The authorization plugin is missing the " + SENTRY_HDFS_KERBEROS_KEYTAB + " property.");
initKerberos(conf, keytab, princ);
}
binding = new SolrAuthzBinding(conf);
LOG.info("SolrAuthzBinding created successfully");
} catch (Exception e) {
throw new SolrException(ErrorCode.SERVER_ERROR, "Unable to create SolrAuthzBinding", e);
}
this.solrSuperUser = Preconditions.checkNotNull(config.get(SENTRY_SOLR_AUTH_SUPERUSER));
boolean enableAuditLog = Boolean.parseBoolean(
Preconditions.checkNotNull(config.get(SENTRY_ENABLE_SOLR_AUDITLOG)));
if (enableAuditLog) {
this.auditLog = Optional.of(new AuditLogger());
}
}
private void audit (Name perm, AuthorizationContext ctx, AuthorizationResponse resp) {
if (!auditLog.isPresent() || !auditLog.get().isLogEnabled()) {
return;
}
String userName = getShortUserName(ctx.getUserPrincipal());
String ipAddress = ctx.getRemoteAddr();
long eventTime = System.currentTimeMillis();
int allowed = (resp.statusCode == AuthorizationResponse.OK.statusCode)
? AuditLogger.ALLOWED : AuditLogger.UNAUTHORIZED;
String operationParams = ctx.getParams().toString();
switch (perm) {
case COLL_EDIT_PERM:
case COLL_READ_PERM: {
String collectionName = "admin";
String actionName = ctx.getParams().get(CoreAdminParams.ACTION);
String operationName = (actionName != null) ?
"CollectionAction." + ctx.getParams().get(CoreAdminParams.ACTION)
: ctx.getHandler().getClass().getName();
auditLog.get().log (userName, null, ipAddress,
operationName, operationParams, eventTime, allowed, collectionName);
break;
}
case CORE_EDIT_PERM:
case CORE_READ_PERM: {
String collectionName = "admin";
String operationName = "CoreAdminAction.STATUS";
if (ctx.getParams().get(CoreAdminParams.ACTION) != null) {
operationName = "CoreAdminAction." + ctx.getParams().get(CoreAdminParams.ACTION);
}
auditLog.get().log (userName, null, ipAddress,
operationName, operationParams, eventTime, allowed, collectionName);
break;
}
case READ_PERM:
case UPDATE_PERM: {
List<String> names = new ArrayList<>();
for (CollectionRequest r : ctx.getCollectionRequests()) {
names.add(r.collectionName);
}
String collectionName = String.join(",", names);
String operationName = (perm == Name.READ_PERM) ? SolrConstants.QUERY : SolrConstants.UPDATE;
auditLog.get().log (userName, null, ipAddress,
operationName, operationParams, eventTime, allowed, collectionName);
break;
}
default: {
// Do nothing.
break;
}
}
}
/**
* Workaround until SOLR-10814 is fixed. This method allows extracting short user-name from
* Solr provided {@linkplain Principal} instance.
*
* @param ctx The Solr provided authorization context
* @return The short name of the authenticated user for this request
*/
public static String getShortUserName (Principal princ) {
if (princ instanceof BasicUserPrincipal) {
return princ.getName();
}
KerberosName name = new KerberosName(princ.getName());
try {
return name.getShortName();
} catch (IOException e) {
LOG.error("Error converting kerberos name. principal = {}, KerberosName.rules = {}",
princ, KerberosName.getRules());
throw new SolrException(ErrorCode.SERVER_ERROR, "Unexpected error converting a kerberos name", e);
}
}
/**
* This method provides the path(s) of various Hadoop configuration files required
* by the Sentry/Solr plugin.
* @param confDir Location of a folder (on local file-system) storing Sentry Hadoop
* configuration files
* @return A list of URLs containing the Sentry Hadoop
* configuration files
*/
private List<URL> getHadoopConfigFiles(String confDir) {
List<URL> result = new ArrayList<>();
if (confDir != null && !confDir.isEmpty()) {
File confDirFile = new File(confDir);
if (!confDirFile.exists()) {
throw new SolrException(ErrorCode.SERVER_ERROR,
"Specified Sentry hadoop config directory does not exist: "
+ confDirFile.getAbsolutePath());
}
if (!confDirFile.isDirectory()) {
throw new SolrException(ErrorCode.SERVER_ERROR,
"Specified Sentry hadoop config directory path is not a directory: "
+ confDirFile.getAbsolutePath());
}
if (!confDirFile.canRead()) {
throw new SolrException(ErrorCode.SERVER_ERROR,
"Specified Sentry hadoop config directory must be readable by the Solr process: "
+ confDirFile.getAbsolutePath());
}
for (String file : Arrays.asList("core-site.xml",
"hdfs-site.xml", "ssl-client.xml")) {
File f = new File(confDirFile, file);
if (f.exists()) {
try {
result.add(f.toURI().toURL());
} catch (MalformedURLException e) {
throw new SolrException(ErrorCode.SERVER_ERROR, e.getMessage(), e);
}
}
}
}
return result;
}
/**
* Initialize kerberos via UserGroupInformation. Will only attempt to login
* during the first request, subsequent calls will have no effect.
*/
private void initKerberos(SolrAuthzConf authzConf, String keytabFile, String principal) {
synchronized (SentrySolrPluginImpl.class) {
UserGroupInformation.setConfiguration(authzConf);
LOG.info(
"Attempting to acquire kerberos ticket with keytab: {}, principal: {} ",
keytabFile, principal);
try {
UserGroupInformation.loginUserFromKeytab(principal, keytabFile);
} catch (IOException ioe) {
throw new SolrException(ErrorCode.SERVER_ERROR, ioe);
}
LOG.info("Got Kerberos ticket");
}
}
private boolean shouldInitializeKereberos(SolrAuthzConf conf) {
String providerBackend = conf.get(SolrAuthzConf.AuthzConfVars.AUTHZ_PROVIDER_BACKEND.getVar());
String authVal = conf.get(HADOOP_SECURITY_AUTHENTICATION);
return SimpleFileProviderBackend.class.getName().equals(providerBackend)
&& "kerberos".equalsIgnoreCase(authVal);
}
}