blob: db5c55576381367e6ebf9965bf6668f96c475d31 [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.impala.util;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.Map;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.permission.AclEntry;
import org.apache.hadoop.fs.permission.AclEntryType;
import org.apache.hadoop.fs.permission.AclStatus;
import org.apache.hadoop.fs.permission.AclEntryScope;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.permission.FsAction;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.hdfs.protocol.AclException;
import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_PERMISSIONS_SUPERUSERGROUP_DEFAULT;
import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_PERMISSIONS_SUPERUSERGROUP_KEY;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.collect.Lists;
/**
* Singleton class that can check whether the current user has permission to access paths
* in a FileSystem.
*/
public class FsPermissionChecker {
private final static Logger LOG = LoggerFactory.getLogger(FsPermissionChecker.class);
private final static FsPermissionChecker instance_;
private final static Configuration CONF;
protected final String user_;
private final Set<String> groups_ = new HashSet<String>();
private final String supergroup_;
static {
CONF = new Configuration();
try {
instance_ = new FsPermissionChecker();
} catch (IOException e) {
throw new RuntimeException(
"Error initializing FsPermissionChecker: " + e.getMessage(), e);
}
}
private FsPermissionChecker() throws IOException {
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
groups_.addAll(Arrays.asList(ugi.getGroupNames()));
supergroup_ = CONF.get(DFS_PERMISSIONS_SUPERUSERGROUP_KEY,
DFS_PERMISSIONS_SUPERUSERGROUP_DEFAULT);
user_ = ugi.getShortUserName();
}
private boolean isSuperUser() { return groups_.contains(supergroup_); }
private static List<AclEntryType> ACL_TYPE_PRIORITY =
ImmutableList.of(AclEntryType.USER, AclEntryType.GROUP, AclEntryType.OTHER);
/**
* Allows checking different access permissions of a file without repeatedly accessing
* the underlying filesystem by caching the results of a status call at construction.
*/
public class Permissions {
private final FileStatus fileStatus_;
private final FsPermission permissions_;
private final AclStatus aclStatus_;
private Map<AclEntryType, List<AclEntry>> entriesByTypes_ = Maps.newHashMap();
private AclEntry mask_;
/**
* If aclStatus is null, ACL permissions are not checked.
*/
protected Permissions(FileStatus fileStatus, AclStatus aclStatus) {
Preconditions.checkNotNull(fileStatus);
fileStatus_ = fileStatus;
permissions_ = fileStatus.getPermission();
aclStatus_ = aclStatus;
if (aclStatus_ == null) return;
// Group the ACLs by type, so that we can apply them in correct priority order. Not
// clear from documentation whether aclStatus_.getEntries() guarantees this
// ordering, so this is defensive.
for (AclEntryType t: ACL_TYPE_PRIORITY) {
entriesByTypes_.put(t, Lists.<AclEntry>newArrayList());
}
List<AclEntry> fullAclList =
getAclFromPermAndEntries(permissions_, aclStatus_.getEntries());
for (AclEntry e: fullAclList) {
if (e.getType() == AclEntryType.MASK && e.getScope() != AclEntryScope.DEFAULT) {
mask_ = e;
} else if (isApplicableAcl(e)) {
entriesByTypes_.get(e.getType()).add(e);
}
}
}
/**
* Returns true if the mask should apply. The mask ACL applies only to unnamed user
* ACLs (e.g. user::r-x), and all group ACLs.
*/
private boolean shouldApplyMask(AclEntry acl) {
if (mask_ == null) return false;
switch (acl.getType()) {
case USER:
return acl.getName() != null;
case GROUP:
return true;
}
return false;
}
/**
* Returns true if this ACL applies to the current user and / or group
*/
private boolean isApplicableAcl(AclEntry e) {
// Default ACLs are not used for permission checking, but instead control the
// permissions received by child directories
if (e.getScope() == AclEntryScope.DEFAULT) return false;
switch (e.getType()) {
case USER:
String aclUser = e.getName() == null ? aclStatus_.getOwner() : e.getName();
return FsPermissionChecker.this.user_.equals(aclUser);
case GROUP:
String aclGroup = e.getName() == null ? aclStatus_.getGroup() : e.getName();
return FsPermissionChecker.this.groups_.contains(aclGroup);
case OTHER:
return true;
case MASK:
return false;
default:
LOG.warn("Unknown Acl type: " + e.getType());
return false;
}
}
/**
* Returns true if ACLs allow 'action', false if they explicitly disallow 'action',
* and 'null' if no ACLs are available.
* See http://users.suse.com/~agruen/acl/linux-acls/online for more details about
* acl access check algorithm.
*/
private Boolean checkAcls(FsAction action) {
// ACLs may not be enabled, so we need this ternary logic. If no ACLs are available,
// returning null causes us to fall back to standard ugo permissions.
if (aclStatus_ == null) return null;
// Remember if there is an applicable ACL entry, including owner user, named user,
// owning group, named group.
boolean foundMatch = false;
for (AclEntryType t: ACL_TYPE_PRIORITY) {
for (AclEntry e: entriesByTypes_.get(t)) {
if (t == AclEntryType.OTHER) {
// Processed all ACL entries except the OTHER entry.
// If found applicable ACL entries but none of them contain requested
// permission, deny access. Otherwise check OTHER entry.
return foundMatch ? false : e.getPermission().implies(action);
}
// If there is an applicable mask, 'action' is allowed iff both the mask and
// the underlying ACL permit it.
if (e.getPermission().implies(action)) {
if (shouldApplyMask(e)) {
if (mask_.getPermission().implies(action)) return true;
} else {
return true;
}
}
// User ACL entry has priority, no need to continue check.
if (t == AclEntryType.USER) return false;
foundMatch = true;
}
}
return false;
}
/**
* Returns true if the current user can perform the given action given these
* permissions.
*/
public boolean checkPermissions(FsAction action) {
if (FsPermissionChecker.this.isSuperUser()) return true;
Boolean aclPerms = checkAcls(action);
if (aclPerms != null) return aclPerms;
// Check user, group and then 'other' permissions in turn.
if (FsPermissionChecker.this.user_.equals(fileStatus_.getOwner())) {
// If the user matches, we must return their access rights whether or not the user
// is allowed to access without checking the group. This is counter-intuitive if
// the user cannot access the file, but the group permissions would allow it, but
// is consistent with UNIX behaviour.
return permissions_.getUserAction().implies(action);
}
if (FsPermissionChecker.this.groups_.contains(fileStatus_.getGroup())) {
return permissions_.getGroupAction().implies(action);
}
return permissions_.getOtherAction().implies(action);
}
public boolean canRead() { return checkPermissions(FsAction.READ); }
public boolean canWrite() { return checkPermissions(FsAction.WRITE); }
public boolean canReadAndWrite() { return canRead() && canWrite(); }
// This was originally lifted from Hadoop. Won't need it if HDFS-7177 is resolved.
// getAclStatus() returns just extended ACL entries, the default file permissions
// like "user::,group::,other::" are not included. We need to combine them together
// to get full logic ACL list.
private List<AclEntry> getAclFromPermAndEntries(FsPermission perm,
List<AclEntry> entries) {
// File permission always have 3 items.
List<AclEntry> aclEntries = Lists.newArrayListWithCapacity(entries.size() + 3);
// Owner entry implied by owner permission bits.
aclEntries.add(new AclEntry.Builder()
.setScope(AclEntryScope.ACCESS)
.setType(AclEntryType.USER)
.setPermission(perm.getUserAction())
.build());
// All extended access ACL entries add by "-setfacl" other than default file
// permission.
boolean hasAccessAcl = false;
for (AclEntry entry: entries) {
// AclEntry list should be ordered, all ACCESS one are in first half, DEFAULT one
// are in second half, so no need to continue here.
if (entry.getScope() == AclEntryScope.DEFAULT) break;
hasAccessAcl = true;
aclEntries.add(entry);
}
// Mask entry implied by group permission bits, or group entry if there is
// no access ACL (only default ACL).
aclEntries.add(new AclEntry.Builder()
.setScope(AclEntryScope.ACCESS)
.setType(hasAccessAcl ? AclEntryType.MASK : AclEntryType.GROUP)
.setPermission(perm.getGroupAction())
.build());
// Other entry implied by other bits.
aclEntries.add(new AclEntry.Builder()
.setScope(AclEntryScope.ACCESS)
.setType(AclEntryType.OTHER)
.setPermission(perm.getOtherAction())
.build());
return aclEntries;
}
}
/**
* Returns a Permissions object that can answer all access permission queries for the
* given path.
*/
public Permissions getPermissions(FileSystem fs, Path path) throws IOException {
Preconditions.checkNotNull(fs);
Preconditions.checkNotNull(path);
return getPermissions(fs, fs.getFileStatus(path));
}
/**
* Returns a Permissions object for the given FileStatus object. In the common
* case that ACLs are not in use, this does not require any additional round-trip
* to the FileSystem. This allows batch construction using APIs like
* FileSystem.listStatus(...).
*/
public Permissions getPermissions(FileSystem fs, FileStatus fileStatus)
throws IOException {
AclStatus aclStatus = null;
if (fileStatus.getPermission().getAclBit()) {
try {
aclStatus = fs.getAclStatus(fileStatus.getPath());
} catch (AclException ex) {
if (LOG.isTraceEnabled()) {
LOG.trace(
"No ACLs retrieved, skipping ACLs check (HDFS will enforce ACLs)", ex);
}
} catch (UnsupportedOperationException ex) {
if (LOG.isTraceEnabled()) LOG.trace("No ACLs retrieved, unsupported", ex);
}
}
return new Permissions(fileStatus, aclStatus);
}
/**
* Returns the FsPermissionChecker singleton.
*/
public static FsPermissionChecker getInstance() { return instance_; }
}