/*
* The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
* (the "License"). You may not use this work except in compliance with the License, which is
* available at www.apache.org/licenses/LICENSE-2.0
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied, as more fully set forth in the License.
*
* See the NOTICE file distributed with this work for information regarding copyright ownership.
*/
package alluxio.master.file;
import alluxio.Configuration;
import alluxio.PropertyKey;
import alluxio.exception.AccessControlException;
import alluxio.exception.ExceptionMessage;
import alluxio.exception.InvalidPathException;
import alluxio.exception.PreconditionMessage;
import alluxio.master.file.meta.Inode;
import alluxio.master.file.meta.InodeTree;
import alluxio.master.file.meta.LockedInodePath;
import alluxio.security.authentication.AuthenticatedClientUser;
import alluxio.security.authorization.Mode;
import alluxio.util.CommonUtils;
import alluxio.util.io.PathUtils;
import com.google.common.base.Preconditions;
import java.io.IOException;
import java.util.List;
import javax.annotation.concurrent.NotThreadSafe;
/**
* Base class to provide permission check logic.
*/
// TODO(peis): Migrate this class to a set of static functions.
@NotThreadSafe // TODO(jiri): make thread-safe (c.f. ALLUXIO-1664)
public final class PermissionChecker {
/** The file system inode structure. */
private final InodeTree mInodeTree;
/** Whether the permission check is enabled. */
private final boolean mPermissionCheckEnabled;
/** The super group of Alluxio file system. All users in this group have super permission. */
private final String mFileSystemSuperGroup;
/**
* Constructs a {@link PermissionChecker} instance for Alluxio file system.
*
* @param inodeTree inode tree of the file system master
*/
public PermissionChecker(InodeTree inodeTree) {
mInodeTree = Preconditions.checkNotNull(inodeTree);
mPermissionCheckEnabled =
Configuration.getBoolean(PropertyKey.SECURITY_AUTHORIZATION_PERMISSION_ENABLED);
mFileSystemSuperGroup =
Configuration.get(PropertyKey.SECURITY_AUTHORIZATION_PERMISSION_SUPERGROUP);
}
/**
* Checks whether a user has permission to perform a specific action on the parent of the given
* path; if parent directory does not exist, treats the closest ancestor directory of the path as
* its parent and checks permission on it. This check will pass if the path is invalid, or path
* has no parent (e.g., root).
*
* @param bits bits that capture the action {@link Mode.Bits} by user
* @param inodePath the path to check permission on
* @throws AccessControlException if permission checking fails
* @throws InvalidPathException if the path is invalid
*/
public void checkParentPermission(Mode.Bits bits, LockedInodePath inodePath)
throws AccessControlException, InvalidPathException {
if (!mPermissionCheckEnabled) {
return;
}
// root "/" has no parent, so return without checking
if (PathUtils.isRoot(inodePath.getUri().getPath())) {
return;
}
// collects existing inodes info on the path. Note that, not all the components of the path have
// corresponding inodes.
List<Inode<?>> inodeList = inodePath.getInodeList();
// collects user and groups
String user = AuthenticatedClientUser.getClientUser();
List<String> groups = getGroups(user);
// remove the last element if all components of the path exist, since we only check the parent.
if (inodePath.fullPathExists()) {
inodeList.remove(inodeList.size() - 1);
}
checkInodeList(user, groups, bits, inodePath.getUri().getPath(), inodeList, false);
}
/**
* Checks whether a user has permission to perform a specific action on a path. This check will
* pass if the path is invalid.
*
* @param bits bits that capture the action {@link Mode.Bits} by user
* @param inodePath the path to check permission on
* @throws AccessControlException if permission checking fails
* @throws InvalidPathException if the path is invalid
*/
public void checkPermission(Mode.Bits bits, LockedInodePath inodePath)
throws AccessControlException, InvalidPathException {
if (!mPermissionCheckEnabled) {
return;
}
// collects inodes info on the path
List<Inode<?>> inodeList = inodePath.getInodeList();
// collects user and groups
String user = AuthenticatedClientUser.getClientUser();
List<String> groups = getGroups(user);
checkInodeList(user, groups, bits, inodePath.getUri().getPath(), inodeList, false);
}
/**
* Gets the permission to access inodePath for the current client user.
*
* @param inodePath the inode path
* @return the permission
*/
public Mode.Bits getPermission(LockedInodePath inodePath) {
if (!mPermissionCheckEnabled) {
return Mode.Bits.NONE;
}
// collects inodes info on the path
List<Inode<?>> inodeList = inodePath.getInodeList();
// collects user and groups
try {
String user = AuthenticatedClientUser.getClientUser();
List<String> groups = getGroups(user);
return getPermissionInternal(user, groups, inodePath.getUri().getPath(), inodeList);
} catch (AccessControlException e) {
return Mode.Bits.NONE;
}
}
/**
* Checks whether a user has permission to edit the attribute of a given path.
*
* @param inodePath the path to check permission on
* @param superuserRequired indicates whether it requires to be the superuser
* @param ownerRequired indicates whether it requires to be the owner of this path
* @throws AccessControlException if permission checking fails
* @throws InvalidPathException if the path is invalid
*/
public void checkSetAttributePermission(LockedInodePath inodePath, boolean superuserRequired,
boolean ownerRequired) throws AccessControlException, InvalidPathException {
if (!mPermissionCheckEnabled) {
return;
}
// For chown, superuser is required
if (superuserRequired) {
checkSuperUser();
}
// For chgrp or chmod, owner or superuser (supergroup) is required
if (ownerRequired) {
checkOwner(inodePath);
}
checkPermission(Mode.Bits.WRITE, inodePath);
}
/**
* @param user the user to get groups for
* @return the groups for the given user
* @throws AccessControlException if the group service information cannot be accessed
*/
private List<String> getGroups(String user) throws AccessControlException {
try {
return CommonUtils.getGroups(user);
} catch (IOException e) {
throw new AccessControlException(
ExceptionMessage.PERMISSION_DENIED.getMessage(e.getMessage()));
}
}
/**
* Checks whether the client user is the owner of the path.
*
* @param inodePath path to be checked on
* @throws AccessControlException if permission checking fails
* @throws InvalidPathException if the path is invalid
*/
private void checkOwner(LockedInodePath inodePath)
throws AccessControlException, InvalidPathException {
// collects inodes info on the path
List<Inode<?>> inodeList = inodePath.getInodeList();
// collects user and groups
String user = AuthenticatedClientUser.getClientUser();
List<String> groups = getGroups(user);
if (isPrivilegedUser(user, groups)) {
return;
}
checkInodeList(user, groups, null, inodePath.getUri().getPath(), inodeList, true);
}
/**
* Checks whether the user is a super user or in super group.
*
* @throws AccessControlException if the user is not a super user
*/
private void checkSuperUser() throws AccessControlException {
// collects user and groups
String user = AuthenticatedClientUser.getClientUser();
List<String> groups = getGroups(user);
if (!isPrivilegedUser(user, groups)) {
throw new AccessControlException(ExceptionMessage.PERMISSION_DENIED
.getMessage(user + " is not a super user or in super group"));
}
}
/**
* This method provides basic permission checking logic on a list of inodes. The input includes
* user and its group, requested action and inode list (by traversing the path). Then user,
* group, and the requested action will be evaluated on each of the inodes. It will return if
* check passed, and throw exception if check failed.
*
* @param user who requests access permission
* @param groups in which user belongs to
* @param bits bits that capture the action {@link Mode.Bits} by user
* @param path the path to check permission on
* @param inodeList file info list of all the inodes retrieved by traversing the path
* @param checkIsOwner indicates whether to check the user is the owner of the path
* @throws AccessControlException if permission checking fails
*/
private void checkInodeList(String user, List<String> groups, Mode.Bits bits,
String path, List<Inode<?>> inodeList, boolean checkIsOwner) throws AccessControlException {
int size = inodeList.size();
Preconditions
.checkArgument(size > 0, PreconditionMessage.EMPTY_FILE_INFO_LIST_FOR_PERMISSION_CHECK);
// bypass checking permission for super user or super group of Alluxio file system.
if (isPrivilegedUser(user, groups)) {
return;
}
// traverses from root to the parent dir to all inodes included by this path are executable
for (int i = 0; i < size - 1; i++) {
checkInode(user, groups, inodeList.get(i), Mode.Bits.EXECUTE, path);
}
Inode inode = inodeList.get(inodeList.size() - 1);
if (checkIsOwner) {
if (inode == null || user.equals(inode.getOwner())) {
return;
}
throw new AccessControlException(ExceptionMessage.PERMISSION_DENIED
.getMessage("user=" + user + " is not the owner of path=" + path));
}
checkInode(user, groups, inode, bits, path);
}
/**
* This method checks requested permission on a given inode, represented by its fileInfo.
*
* @param user who requests access permission
* @param groups in which user belongs to
* @param inode whose attributes used for permission check logic
* @param bits requested {@link Mode.Bits} by user
* @param path the path to check permission on
* @throws AccessControlException if permission checking fails
*/
private void checkInode(String user, List<String> groups, Inode<?> inode, Mode.Bits bits,
String path) throws AccessControlException {
if (inode == null) {
return;
}
short permission = inode.getMode();
if (user.equals(inode.getOwner()) && Mode.extractOwnerBits(permission).imply(bits)) {
return;
}
if (groups.contains(inode.getGroup()) && Mode.extractGroupBits(permission).imply(bits)) {
return;
}
if (Mode.extractOtherBits(permission).imply(bits)) {
return;
}
throw new AccessControlException(ExceptionMessage.PERMISSION_DENIED
.getMessage(toExceptionMessage(user, bits, path, inode)));
}
/**
* Gets the permission to access an inode path given a user and its groups.
*
* @param user the user
* @param groups the groups this user belongs to
* @param path the inode path
* @param inodeList the list of inodes in the path
* @return the permission
*/
private Mode.Bits getPermissionInternal(String user, List<String> groups, String path,
List<Inode<?>> inodeList) {
int size = inodeList.size();
Preconditions
.checkArgument(size > 0, PreconditionMessage.EMPTY_FILE_INFO_LIST_FOR_PERMISSION_CHECK);
// bypass checking permission for super user or super group of Alluxio file system.
if (isPrivilegedUser(user, groups)) {
return Mode.Bits.ALL;
}
// traverses from root to the parent dir to all inodes included by this path are executable
for (int i = 0; i < size - 1; i++) {
try {
checkInode(user, groups, inodeList.get(i), Mode.Bits.EXECUTE, path);
} catch (AccessControlException e) {
return Mode.Bits.NONE;
}
}
Inode inode = inodeList.get(inodeList.size() - 1);
if (inode == null) {
return Mode.Bits.NONE;
}
Mode.Bits mode = Mode.Bits.NONE;
short permission = inode.getMode();
if (user.equals(inode.getOwner())) {
mode = mode.or(Mode.extractOwnerBits(permission));
}
if (groups.contains(inode.getGroup())) {
mode = mode.or(Mode.extractGroupBits(permission));
}
mode = mode.or(Mode.extractOtherBits(permission));
return mode;
}
private boolean isPrivilegedUser(String user, List<String> groups) {
return user.equals(mInodeTree.getRootUserName()) || groups.contains(mFileSystemSuperGroup);
}
private static String toExceptionMessage(String user, Mode.Bits bits, String path,
Inode<?> inode) {
StringBuilder stringBuilder =
new StringBuilder().append("user=").append(user).append(", ").append("access=").append(bits)
.append(", ").append("path=").append(path).append(": ").append("failed at ")
.append(inode.getName().equals("") ? "/" : inode.getName()).append(", inode owner=")
.append(inode.getOwner()).append(", inode group=").append(inode.getGroup())
.append(", inode mode=").append(new Mode(inode.getMode()).toString());
return stringBuilder.toString();
}
}