Java tutorial
/* * 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); } }