/** * 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.falcon.security; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang.Validate; import org.apache.falcon.FalconException; import org.apache.falcon.entity.EntityNotRegisteredException; import org.apache.falcon.entity.EntityUtil; import org.apache.falcon.entity.v0.AccessControlList; import org.apache.falcon.entity.v0.Entity; import org.apache.falcon.entity.v0.EntityType; import org.apache.falcon.util.StartupProperties; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.authorize.AuthorizationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; /** * Default implementation of AuthorizationProvider in Falcon. * * The authorization is enforced in the following way: * * if admin resource, * if authenticated user name matches the admin users configuration * Else if groups of the authenticated user matches the admin groups configuration * Else if entities or instance resource * if the authenticated user matches the owner in ACL for the entity * Else if the groups of the authenticated user matches the group in ACL for the entity * Else if lineage resource * All have read-only permissions * Else bad resource */ public class DefaultAuthorizationProvider implements AuthorizationProvider { private static final Logger LOG = LoggerFactory.getLogger(DefaultAuthorizationProvider.class); private static final Set<String> RESOURCES = new HashSet<String>( Arrays.asList(new String[]{"admin", "entities", "instance", "metadata", "extension", })); private static final String LIST_OPERATION = "list"; /** * Constant for the configuration property that indicates the prefix. */ protected static final String FALCON_PREFIX = "falcon.security.authorization."; /** * Constant for the configuration property that indicates the blacklisted super users for falcon. */ private static final String ADMIN_USERS_KEY = FALCON_PREFIX + "admin.users"; private static final String ADMIN_GROUPS_KEY = FALCON_PREFIX + "admin.groups"; /** * The super-user is the user with the same identity as falcon process itself. * Loosely, if you started falcon, then you are the super-user. */ protected static final String SUPER_USER = System.getProperty("user.name"); /** * Constant for the configuration property that indicates the super user group. */ private static final String SUPER_USER_GROUP_KEY = FALCON_PREFIX + "superusergroup"; /** * Super user group. */ private final String superUserGroup; private final Set<String> adminUsers; private final Set<String> adminGroups; public DefaultAuthorizationProvider() { superUserGroup = StartupProperties.get().getProperty(SUPER_USER_GROUP_KEY); adminUsers = getAdminNamesFromConfig(ADMIN_USERS_KEY); adminGroups = getAdminNamesFromConfig(ADMIN_GROUPS_KEY); } private Set<String> getAdminNamesFromConfig(String key) { Set<String> adminNames = new HashSet<String>(); String adminNamesConfig = StartupProperties.get().getProperty(key); if (!StringUtils.isEmpty(adminNamesConfig)) { adminNames.addAll(Arrays.asList(adminNamesConfig.split(","))); } return Collections.unmodifiableSet(adminNames); } /** * Determines if the authenticated user is the user who started this process * or belongs to the super user group. * * @param authenticatedUGI UGI * @return true if super user else false. */ public boolean isSuperUser(UserGroupInformation authenticatedUGI) { return SUPER_USER.equals(authenticatedUGI.getShortUserName()) || (!StringUtils.isEmpty(superUserGroup) && isUserInGroup(superUserGroup, authenticatedUGI)); } /** * Checks if authenticated user should proxy the entity acl owner. * * @param authenticatedUGI proxy ugi for the authenticated user. * @param aclOwner entity ACL Owner. * @param aclGroup entity ACL group. * @throws IOException */ @Override public boolean shouldProxy(UserGroupInformation authenticatedUGI, final String aclOwner, final String aclGroup) throws IOException { Validate.notNull(authenticatedUGI, "User cannot be empty or null"); Validate.notEmpty(aclOwner, "User cannot be empty or null"); Validate.notEmpty(aclGroup, "Group cannot be empty or null"); return isSuperUser(authenticatedUGI) || (!isUserACLOwner(authenticatedUGI.getShortUserName(), aclOwner) && isUserInGroup(aclGroup, authenticatedUGI)); } /** * Determines if the authenticated user is authorized to execute the action on the resource. * Throws an exception if not authorized. * * @param resource api resource, admin, entities or instance * @param action action being authorized on resource and entity if applicable * @param entityType entity type in question, not for admin resource * @param entityName entity name in question, not for admin resource * @param authenticatedUGI proxy ugi for the authenticated user * @throws org.apache.hadoop.security.authorize.AuthorizationException */ @Override public void authorizeResource(String resource, String action, String entityType, String entityName, UserGroupInformation authenticatedUGI) throws AuthorizationException, EntityNotRegisteredException { Validate.notEmpty(resource, "Resource cannot be empty or null"); Validate.isTrue(RESOURCES.contains(resource), "Illegal resource: " + resource); Validate.notEmpty(action, "Action cannot be empty or null"); try { if (isSuperUser(authenticatedUGI)) { return; } if ("admin".equals(resource)) { if (!("version".equals(action) || "clearuser".equals(action) || "getuser".equals(action))) { authorizeAdminResource(authenticatedUGI, action); } } else if ("entities".equals(resource) || "instance".equals(resource)) { if ("entities".equals(resource) && LIST_OPERATION.equals(action)) { LOG.info("Skipping authorization for entity list operations"); } else { authorizeEntityResource(authenticatedUGI, entityName, entityType, action); } } else if ("metadata".equals(resource)) { authorizeMetadataResource(authenticatedUGI, action); } } catch (IOException e) { throw new AuthorizationException(e); } } protected Set<String> getGroupNames(UserGroupInformation proxyUgi) { return new HashSet<String>(Arrays.asList(proxyUgi.getGroupNames())); } /** * Determines if the authenticated user is authorized to execute the action on the entity. * Throws an exception if not authorized. * * @param entityName entity in question, applicable for entities and instance resource * @param entityType entity in question, applicable for entities and instance resource * @param acl entity ACL * @param action action being authorized on resource and entity if applicable * @param authenticatedUGI proxy ugi for the authenticated user * @throws org.apache.hadoop.security.authorize.AuthorizationException */ @Override public void authorizeEntity(String entityName, String entityType, AccessControlList acl, String action, UserGroupInformation authenticatedUGI) throws AuthorizationException { try { LOG.info("Authorizing authenticatedUser={}, action={}, entity={}, type{}", authenticatedUGI.getShortUserName(), action, entityName, entityType); if (isSuperUser(authenticatedUGI)) { return; } checkUser(entityName, acl.getOwner(), acl.getGroup(), action, authenticatedUGI); } catch (IOException e) { throw new AuthorizationException(e); } } /** * Validate if the entity owner is the logged-in authenticated user. * * @param entityName entity name. * @param aclOwner entity ACL Owner. * @param aclGroup entity ACL group. * @param action action being authorized on resource and entity if applicable. * @param authenticatedUGI proxy ugi for the authenticated user. * @throws AuthorizationException */ protected void checkUser(String entityName, String aclOwner, String aclGroup, String action, UserGroupInformation authenticatedUGI) throws AuthorizationException { final String authenticatedUser = authenticatedUGI.getShortUserName(); if (isUserACLOwner(authenticatedUser, aclOwner) || isUserInGroup(aclGroup, authenticatedUGI)) { return; } StringBuilder message = new StringBuilder("Permission denied: authenticatedUser="); message.append(authenticatedUser); message.append(!authenticatedUser.equals(aclOwner) ? " not entity owner=" + aclOwner : " not in group=" + aclGroup); message.append(", entity=").append(entityName).append(", action=").append(action); LOG.error(message.toString()); throw new AuthorizationException(message.toString()); } /** * Determines if the authenticated user is the entity ACL owner. * * @param authenticatedUser authenticated user * @param aclOwner entity ACL owner * @return true if authenticated user is the entity acl owner, false otherwise. */ protected boolean isUserACLOwner(String authenticatedUser, String aclOwner) { return authenticatedUser.equals(aclOwner); } /** * Checks if the user's group matches the entity ACL group. * * @param group Entity ACL group. * @param proxyUgi proxy ugi for the authenticated user. * @return true if user groups contains entity acl group. */ protected boolean isUserInGroup(String group, UserGroupInformation proxyUgi) { Set<String> groups = getGroupNames(proxyUgi); return groups.contains(group); } /** * Check if the user has admin privileges. * * @param authenticatedUGI proxy ugi for the authenticated user. * @param action admin action on the resource. * @throws AuthorizationException if the user does not have admin privileges. */ protected void authorizeAdminResource(UserGroupInformation authenticatedUGI, String action) throws AuthorizationException { final String authenticatedUser = authenticatedUGI.getShortUserName(); LOG.debug("Authorizing user={} for admin, action={}", authenticatedUser, action); if (adminUsers.contains(authenticatedUser) || isUserInAdminGroups(authenticatedUGI)) { return; } LOG.error("Permission denied: user {} does not have admin privilege for action={}", authenticatedUser, action); throw new AuthorizationException("Permission denied: user=" + authenticatedUser + " does not have admin privilege for action=" + action); } protected boolean isUserInAdminGroups(UserGroupInformation proxyUgi) { final Set<String> groups = getGroupNames(proxyUgi); groups.retainAll(adminGroups); return !groups.isEmpty(); } protected void authorizeEntityResource(UserGroupInformation authenticatedUGI, String entityName, String entityType, String action) throws AuthorizationException, EntityNotRegisteredException { Validate.notEmpty(entityType, "Entity type cannot be empty or null"); LOG.debug("Authorizing authenticatedUser={} against entity/instance action={}, " + "entity name={}, entity type={}", authenticatedUGI.getShortUserName(), action, entityName, entityType); if (entityName != null) { // lifecycle actions Entity entity = getEntity(entityName, entityType); authorizeEntity(entity.getName(), entity.getEntityType().name(), entity.getACL(), action, authenticatedUGI); } else { // non lifecycle actions, lifecycle actions with null entity will validate later LOG.info("Authorization for action={} will be done in the API", action); } } private Entity getEntity(String entityName, String entityType) throws EntityNotRegisteredException, AuthorizationException { try { EntityType type = EntityType.getEnum(entityType); return EntityUtil.getEntity(type, entityName); } catch (FalconException e) { if (e instanceof EntityNotRegisteredException) { throw (EntityNotRegisteredException) e; } else { throw new AuthorizationException(e); } } } protected void authorizeMetadataResource(UserGroupInformation authenticatedUGI, String action) throws AuthorizationException { LOG.debug("User {} authorized for action {} ", authenticatedUGI.getShortUserName(), action); // todo - read-only for all metadata but needs to be implemented } }