Skip to main content

hdfs 启用 ranger 鉴权流程

看了一遍hdfs和ranger权限部分的代码, 大概清楚ranger是如何嵌入hdfs的鉴权流程了.

hdfs在执行文件操作的时候, namenode里会获取permissionChecker, 然后调用permissionCheck的checkPermission判断是否有权限.

permissionChecker支持从外部加载实现的类, 具体通过参数dfs.namenode.inode.attributes.provider.class定义.

ranger的hdfs agent实现了这个类的方法, 配置dfs.namenode.inode.attributes.provider.classorg.apache.ranger.authorization.hadoop.RangerHdfsAuthorizer, hdfs就可以自动启用ranger作为permissionChecker.

hdfs在namenode启动的时候, 会调用一次permissionCheck的start方法, 可以把ranger的agent policy update线程启动, 用于定时加载更新权限;

hfds的namenode在停止的时候, 会调用permissionCheck的stop方法停止线程.

hdfs开启ranger鉴权的配置

Configuring Ranger with HDFS Transparency

https://www.ibm.com/docs/en/storage-scale-bda?topic=support-configuring-ranger-hdfs-transparency

Check that /etc/hadoop/conf/hdfs-site.xml contains the value org.apache.ranger.authorization.hadoop.RangerHdfsAuthorizer for the dfs.namenode.inode.attributes.provider.class.

    <property>
<name>dfs.namenode.inode.attributes.provider.class</name>
<value>org.apache.ranger.authorization.hadoop.RangerHdfsAuthorizer</value>
</property>

hdfs 源码操作

hdfs 加载外部类

hfds代码中, 支持从配置参数读取外部实现类


/**
* FSNamesystem is a container of both transient
* and persisted name-space state, and does all the book-keeping
* work on a NameNode.
*
* Its roles are briefly described below:
*
* 1) Is the container for BlockManager, DatanodeManager,
* DelegationTokens, LeaseManager, etc. services.
* 2) RPC calls that modify or inspect the name-space
* should get delegated here.
* 3) Anything that touches only blocks (eg. block reports),
* it delegates to BlockManager.
* 4) Anything that touches only file information (eg. permissions, mkdirs),
* it delegates to FSDirectory.
* 5) Anything that crosses two of the above components should be
* coordinated here.
* 6) Logs mutations to FSEditLog.
*
* This class and its contents keep:
*
* 1) Valid fsname --> blocklist (kept on disk, logged)
* 2) Set of all valid blocks (inverted #1)
* 3) block --> machinelist (kept in memory, rebuilt dynamically from reports)
* 4) machine --> blocklist (inverted #2)
* 5) LRU cache of updated-heartbeat machines
*/
@InterfaceAudience.Private
@Metrics(context="dfs")
public class FSNamesystem implements Namesystem, FSNamesystemMBean,
NameNodeMXBean, ReplicatedBlocksMBean, ECBlockGroupsMBean {


/**
* Create an FSNamesystem associated with the specified image.
*
* Note that this does not load any data off of disk -- if you would
* like that behavior, use {@link #loadFromDisk(Configuration)}
*
* @param conf configuration
* @param fsImage The FSImage to associate with
* @param ignoreRetryCache Whether or not should ignore the retry cache setup
* step. For Secondary NN this should be set to true.
* @throws IOException on bad configuration
*/
FSNamesystem(Configuration conf, FSImage fsImage, boolean ignoreRetryCache)
throws IOException {

...
Class<? extends INodeAttributeProvider> klass = conf.getClass(
DFS_NAMENODE_INODE_ATTRIBUTES_PROVIDER_KEY,
null, INodeAttributeProvider.class);
if (klass != null) {
inodeAttributeProvider = ReflectionUtils.newInstance(klass, conf);
LOG.info("Using INode attribute provider: " + klass.getName());
}

}
}

启动namenode的时候, 调用一次start和stop, 对于ranger hdfs agent而言, 启动了定期下载策略缓存的线程;

同时把外部的permission checker设置为directory 操作里的permission Checker.


/**
* Start services common to both active and standby states
*/
void startCommonServices(Configuration conf, HAContext haContext) throws IOException {
this.registerMBean(); // register the MBean for the FSNamesystemState
writeLock();
this.haContext = haContext;
try {
nnResourceChecker = new NameNodeResourceChecker(conf);
checkAvailableResources();
assert !blockManager.isPopulatingReplQueues();
StartupProgress prog = NameNode.getStartupProgress();
prog.beginPhase(Phase.SAFEMODE);
long completeBlocksTotal = getCompleteBlocksTotal();
prog.setTotal(Phase.SAFEMODE, STEP_AWAITING_REPORTED_BLOCKS,
completeBlocksTotal);
blockManager.activate(conf, completeBlocksTotal);
} finally {
writeUnlock("startCommonServices");
}

registerMXBean();
DefaultMetricsSystem.instance().register(this);
if (inodeAttributeProvider != null) {
inodeAttributeProvider.start();
dir.setINodeAttributeProvider(inodeAttributeProvider);
}
snapshotManager.registerMXBean();
InetSocketAddress serviceAddress = NameNode.getServiceAddress(conf, true);
this.nameNodeHostName = (serviceAddress != null) ?
serviceAddress.getHostName() : "";
}

/**
* Stop services common to both active and standby states
*/
void stopCommonServices() {
writeLock();
if (inodeAttributeProvider != null) {
dir.setINodeAttributeProvider(null);
inodeAttributeProvider.stop();
}
try {
if (blockManager != null) blockManager.close();
} finally {
writeUnlock("stopCommonServices");
}
RetryCache.clear(retryCache);
}

hdfs的INodeAttributeProvider和AccessControlEnforcer

实现了hdfs的INodeAttributeProvider和AccessControlEnforcer, 才能作为permissionChecker进行加载使用.


package org.apache.hadoop.hdfs.server.namenode;

import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.permission.FsAction;
import org.apache.hadoop.hdfs.DFSUtil;
import org.apache.hadoop.security.AccessControlException;
import org.apache.hadoop.security.UserGroupInformation;

@InterfaceAudience.Public
@InterfaceStability.Unstable
public abstract class INodeAttributeProvider {

/**
* The AccessControlEnforcer allows implementations to override the
* default File System permission checking logic enforced on a file system
* object
*/
public interface AccessControlEnforcer {

/**
* Checks permission on a file system object. Has to throw an Exception
* if the filesystem object is not accessessible by the calling Ugi.
* @param fsOwner Filesystem owner (The Namenode user)
* @param supergroup super user geoup
* @param callerUgi UserGroupInformation of the caller
* @param inodeAttrs Array of INode attributes for each path element in the
* the path
* @param inodes Array of INodes for each path element in the path
* @param pathByNameArr Array of byte arrays of the LocalName
* @param snapshotId the snapshotId of the requested path
* @param path Path String
* @param ancestorIndex Index of ancestor
* @param doCheckOwner perform ownership check
* @param ancestorAccess The access required by the ancestor of the path.
* @param parentAccess The access required by the parent of the path.
* @param access The access required by the path.
* @param subAccess If path is a directory, It is the access required of
* the path and all the sub-directories. If path is not a
* directory, there should ideally be no effect.
* @param ignoreEmptyDir Ignore permission checking for empty directory?
* @throws AccessControlException
*/
public abstract void checkPermission(String fsOwner, String supergroup,
UserGroupInformation callerUgi, INodeAttributes[] inodeAttrs,
INode[] inodes, byte[][] pathByNameArr, int snapshotId, String path,
int ancestorIndex, boolean doCheckOwner, FsAction ancestorAccess,
FsAction parentAccess, FsAction access, FsAction subAccess,
boolean ignoreEmptyDir)
throws AccessControlException;

}
/**
* Initialize the provider. This method is called at NameNode startup
* time.
*/
public abstract void start();

/**
* Shutdown the provider. This method is called at NameNode shutdown time.
*/
public abstract void stop();

...

/**
* Can be over-ridden by implementations to provide a custom Access Control
* Enforcer that can provide an alternate implementation of the
* default permission checking logic.
* @param defaultEnforcer The Default AccessControlEnforcer
* @return The AccessControlEnforcer to use
*/
public AccessControlEnforcer getExternalAccessControlEnforcer(
AccessControlEnforcer defaultEnforcer) {
return defaultEnforcer;
}
}

默认的空白 permissionChecker


package org.apache.hadoop.hdfs.server.namenode;

/**
* A default implementation of the INodeAttributesProvider
*
*/
public class DefaultINodeAttributesProvider extends INodeAttributeProvider {

public static INodeAttributeProvider DEFAULT_PROVIDER =
new DefaultINodeAttributesProvider();

@Override
public void start() {
// NO-OP
}

@Override
public void stop() {
// NO-OP
}

@Override
public INodeAttributes getAttributes(String[] pathElements,
INodeAttributes inode) {
return inode;
}

}

hdfs permissionChecker


private AccessControlEnforcer getAccessControlEnforcer() {
return (attributeProvider != null)
? attributeProvider.getExternalAccessControlEnforcer(this) : this;
}


/**
* Check whether current user have permissions to access the path.
* Traverse is always checked.
*
* Parent path means the parent directory for the path.
* Ancestor path means the last (the closest) existing ancestor directory
* of the path.
* Note that if the parent path exists,
* then the parent path and the ancestor path are the same.
*
* For example, suppose the path is "/foo/bar/baz".
* No matter baz is a file or a directory,
* the parent path is "/foo/bar".
* If bar exists, then the ancestor path is also "/foo/bar".
* If bar does not exist and foo exists,
* then the ancestor path is "/foo".
* Further, if both foo and bar do not exist,
* then the ancestor path is "/".
*
* @param doCheckOwner Require user to be the owner of the path?
* @param ancestorAccess The access required by the ancestor of the path.
* @param parentAccess The access required by the parent of the path.
* @param access The access required by the path.
* @param subAccess If path is a directory,
* it is the access required of the path and all the sub-directories.
* If path is not a directory, there is no effect.
* @param ignoreEmptyDir Ignore permission checking for empty directory?
* @throws AccessControlException
*
* Guarded by {@link FSNamesystem#readLock()}
* Caller of this method must hold that lock.
*/
void checkPermission(INodesInPath inodesInPath, boolean doCheckOwner,
FsAction ancestorAccess, FsAction parentAccess, FsAction access,
FsAction subAccess, boolean ignoreEmptyDir)
throws AccessControlException {
if (LOG.isDebugEnabled()) {
LOG.debug("ACCESS CHECK: " + this
+ ", doCheckOwner=" + doCheckOwner
+ ", ancestorAccess=" + ancestorAccess
+ ", parentAccess=" + parentAccess
+ ", access=" + access
+ ", subAccess=" + subAccess
+ ", ignoreEmptyDir=" + ignoreEmptyDir);
}
// check if (parentAccess != null) && file exists, then check sb
// If resolveLink, the check is performed on the link target.
final int snapshotId = inodesInPath.getPathSnapshotId();
final INode[] inodes = inodesInPath.getINodesArray();
final INodeAttributes[] inodeAttrs = new INodeAttributes[inodes.length];
final byte[][] components = inodesInPath.getPathComponents();
for (int i = 0; i < inodes.length && inodes[i] != null; i++) {
inodeAttrs[i] = getINodeAttrs(components, i, inodes[i], snapshotId);
}

String path = inodesInPath.getPath();
int ancestorIndex = inodes.length - 2;

AccessControlEnforcer enforcer = getAccessControlEnforcer();
enforcer.checkPermission(fsOwner, supergroup, callerUgi, inodeAttrs, inodes,
components, snapshotId, path, ancestorIndex, doCheckOwner,
ancestorAccess, parentAccess, access, subAccess, ignoreEmptyDir);
}


protected FSPermissionChecker(String fsOwner, String supergroup,
UserGroupInformation callerUgi,
INodeAttributeProvider attributeProvider) {
this.fsOwner = fsOwner;
this.supergroup = supergroup;
this.callerUgi = callerUgi;
this.groups = callerUgi.getGroups();
user = callerUgi.getShortUserName();
isSuper = user.equals(fsOwner) || groups.contains(supergroup);
this.attributeProvider = attributeProvider;
}

hdfs fsDirectory


private INodeAttributeProvider attributeProvider;

// A HashSet of principals of users for whom the external attribute provider
// will be bypassed
private HashSet<String> usersToBypassExtAttrProvider = null;

public void setINodeAttributeProvider(INodeAttributeProvider provider) {
attributeProvider = provider;
}

/**
* Check if a given user is configured to bypass external attribute provider.
* @param user user principal
* @return true if the user is to bypass external attribute provider
*/
private boolean isUserBypassingExtAttrProvider(final String user) {
return (usersToBypassExtAttrProvider != null) &&
usersToBypassExtAttrProvider.contains(user);
}

/**
* Return attributeProvider or null if ugi is to bypass attributeProvider.
* @param ugi
* @return configured attributeProvider or null
*/
private INodeAttributeProvider getUserFilteredAttributeProvider(
UserGroupInformation ugi) {
if (attributeProvider == null ||
(ugi != null && isUserBypassingExtAttrProvider(ugi.getUserName()))) {
return null;
}
return attributeProvider;
}


INodeAttributes getAttributes(INodesInPath iip)
throws IOException {
INode node = FSDirectory.resolveLastINode(iip);
int snapshot = iip.getPathSnapshotId();
INodeAttributes nodeAttrs = node.getSnapshotINode(snapshot);
UserGroupInformation ugi = NameNode.getRemoteUser();
INodeAttributeProvider ap = this.getUserFilteredAttributeProvider(ugi);
if (ap != null) {
// permission checking sends the full components array including the
// first empty component for the root. however file status
// related calls are expected to strip out the root component according
// to TestINodeAttributeProvider.
byte[][] components = iip.getPathComponents();
components = Arrays.copyOfRange(components, 1, components.length);
nodeAttrs = ap.getAttributes(components, nodeAttrs);
}
return nodeAttrs;
}

ranger源码操作

ranger hdfs agent

RangerHdfsAuthorizer继承了hdfs中的INodeAttributeProvider, 因此后面可以被hdfs通过文件类名进行conf.getClassnewInstance的初始化.

这里面重点是getExternalAccessControlEnforcer方法, 返回了hdfs agent的具体权限拦截实现, 提供了checkPermission的详细实现.


public class RangerHdfsAuthorizer extends INodeAttributeProvider {


public void start() {
if(LOG.isDebugEnabled()) {
LOG.debug("==> RangerHdfsAuthorizer.start()");
}

RangerHdfsPlugin plugin = new RangerHdfsPlugin(addlConfigFile);

plugin.init();

}



public void stop() {
if(LOG.isDebugEnabled()) {
LOG.debug("==> RangerHdfsAuthorizer.stop()");
}

RangerHdfsPlugin plugin = rangerPlugin;
rangerPlugin = null;

if(plugin != null) {
plugin.cleanup();
}

}

@Override
public INodeAttributes getAttributes(String[] pathElements, INodeAttributes inode) {
if(LOG.isDebugEnabled()) {
LOG.debug("==> RangerHdfsAuthorizer.getAttributes(pathElementsCount=" + (pathElements == null ? 0 : pathElements.length) + ")");
}

INodeAttributes ret = inode; // return default attributes

if(LOG.isDebugEnabled()) {
LOG.debug("<== RangerHdfsAuthorizer.getAttributes(pathElementsCount=" + (pathElements == null ? 0 : pathElements.length) + "): " + ret);
}

return ret;
}

@Override
public AccessControlEnforcer getExternalAccessControlEnforcer(AccessControlEnforcer defaultEnforcer) {
if(LOG.isDebugEnabled()) {
LOG.debug("==> RangerHdfsAuthorizer.getExternalAccessControlEnforcer()");
}

RangerAccessControlEnforcer rangerAce = new RangerAccessControlEnforcer(defaultEnforcer);

if(LOG.isDebugEnabled()) {
LOG.debug("<== RangerHdfsAuthorizer.getExternalAccessControlEnforcer()");
}

return rangerAce;
}


}

ranger hdfs agent的RangerAccessControlEnforcer

RangerAccessControlEnforcer提供了checkPermission的函数实现, 具体而言是构造请求函数, 然后调用本地的缓存进行查询, 判断是否有权限定义, 是否被权限拦截.

如果没有权限定义, 会切换走hadoop本身的acl权限. 在这个过程中, 还会判断是否是文件的owner; 对于父级目录的execute权限需求,也有特殊的处理逻辑.




class RangerAccessControlEnforcer implements AccessControlEnforcer {
private INodeAttributeProvider.AccessControlEnforcer defaultEnforcer = null;

public RangerAccessControlEnforcer(AccessControlEnforcer defaultEnforcer) {
if(LOG.isDebugEnabled()) {
LOG.debug("==> RangerAccessControlEnforcer.RangerAccessControlEnforcer()");
}

this.defaultEnforcer = defaultEnforcer;

if(LOG.isDebugEnabled()) {
LOG.debug("<== RangerAccessControlEnforcer.RangerAccessControlEnforcer()");
}
}

class SubAccessData {
final INodeDirectory dir;
final String resourcePath;

SubAccessData(INodeDirectory dir, String resourcePath) {
this.dir = dir;
this.resourcePath = resourcePath;
}
}

@Override
public void checkPermissionWithContext(AuthorizationContext authzContext) throws AccessControlException {
checkRangerPermission(authzContext.getFsOwner(), authzContext.getSupergroup(),
authzContext.getCallerUgi(), authzContext.getInodeAttrs(),
authzContext.getInodes(), authzContext.getPathByNameArr(),
authzContext.getSnapshotId(), authzContext.getPath(),
authzContext.getAncestorIndex(), authzContext.isDoCheckOwner(),
authzContext.getAncestorAccess(), authzContext.getParentAccess(),
authzContext.getAccess(), authzContext.getSubAccess(),
authzContext.isIgnoreEmptyDir(), authzContext.getOperationName(),
authzContext.getCallerContext());
}

@Override
public void checkPermission(String fsOwner, String superGroup, UserGroupInformation ugi,
INodeAttributes[] inodeAttrs, INode[] inodes, byte[][] pathByNameArr,
int snapshotId, String path, int ancestorIndex, boolean doCheckOwner,
FsAction ancestorAccess, FsAction parentAccess, FsAction access,
FsAction subAccess, boolean ignoreEmptyDir) throws AccessControlException {
checkRangerPermission(fsOwner, superGroup, ugi, inodeAttrs, inodes, pathByNameArr, snapshotId, path, ancestorIndex, doCheckOwner, ancestorAccess, parentAccess, access, subAccess, ignoreEmptyDir, null, null);
}

private void checkRangerPermission(String fsOwner, String superGroup, UserGroupInformation ugi,
INodeAttributes[] inodeAttrs, INode[] inodes, byte[][] pathByNameArr,
int snapshotId, String path, int ancestorIndex, boolean doCheckOwner,
FsAction ancestorAccess, FsAction parentAccess, FsAction access,
FsAction subAccess, boolean ignoreEmptyDir, String operationName, CallerContext callerContext ) throws AccessControlException {
AuthzStatus authzStatus = AuthzStatus.NOT_DETERMINED;
String resourcePath = path;
AuthzContext context = new AuthzContext(rangerPlugin, ugi, operationName, access == null && parentAccess == null && ancestorAccess == null && subAccess == null);

if(LOG.isDebugEnabled()) {
LOG.debug("==> RangerAccessControlEnforcer.checkPermission("
+ "fsOwner=" + fsOwner + "; superGroup=" + superGroup + ", inodesCount=" + (inodes != null ? inodes.length : 0)
+ ", snapshotId=" + snapshotId + ", user=" + context.user + ", provided-path=" + path + ", ancestorIndex=" + ancestorIndex
+ ", doCheckOwner="+ doCheckOwner + ", ancestorAccess=" + ancestorAccess + ", parentAccess=" + parentAccess
+ ", access=" + access + ", subAccess=" + subAccess + ", ignoreEmptyDir=" + ignoreEmptyDir + ", operationName=" + operationName
+ ", callerContext=" + callerContext +")");
}

RangerPerfTracer perf = null;

if(RangerPerfTracer.isPerfTraceEnabled(PERF_HDFSAUTH_REQUEST_LOG)) {
perf = RangerPerfTracer.getPerfTracer(PERF_HDFSAUTH_REQUEST_LOG, "RangerHdfsAuthorizer.checkPermission(provided-path=" + path + ")");
}

try {
INode ancestor = null;
INode parent = null;
INode inode = null;
String providedPath = path;

boolean useDefaultAuthorizerOnly = false;
boolean doNotGenerateAuditRecord = false;

if (context.plugin != null && !ArrayUtils.isEmpty(inodes)) {
int sz = inodeAttrs.length;
if (LOG.isDebugEnabled()) {
LOG.debug("Size of INodeAttrs array:[" + sz + "]");
LOG.debug("Size of INodes array:[" + inodes.length + "]");
}
byte[][] components = new byte[sz][];

int i = 0;
for (; i < sz; i++) {
if (inodeAttrs[i] != null) {
components[i] = inodeAttrs[i].getLocalNameBytes();
} else {
break;
}
}
if (i != sz) {
if (LOG.isDebugEnabled()) {
LOG.debug("Input INodeAttributes array contains null at position " + i);
LOG.debug("Will use only first [" + i + "] components");
}
}

if (sz == 1 && inodes.length == 1 && inodes[0].getParent() != null) {

doNotGenerateAuditRecord = true;

if (LOG.isDebugEnabled()) {
LOG.debug("Using the only inode in the array to figure out path to resource. No audit record will be generated for this authorization request");
}

resourcePath = inodes[0].getFullPathName();

if (snapshotId != Snapshot.CURRENT_STATE_ID) {

useDefaultAuthorizerOnly = true;

if (LOG.isDebugEnabled()) {
LOG.debug("path:[" + resourcePath + "] is for a snapshot, id=[" + snapshotId +"], default Authorizer will be used to authorize this request");
}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("path:[" + resourcePath + "] is not for a snapshot, id=[" + snapshotId +"]. It will be used to authorize this request");
}
}
} else {

if (snapshotId != Snapshot.CURRENT_STATE_ID) {
resourcePath = DFSUtil.byteArray2PathString(pathByNameArr);

if (LOG.isDebugEnabled()) {
LOG.debug("pathByNameArr array is used to figure out path to resource, resourcePath:[" + resourcePath +"]");
}
} else {
resourcePath = DFSUtil.byteArray2PathString(components, 0, i);

if (LOG.isDebugEnabled()) {
LOG.debug("INodeAttributes array is used to figure out path to resource, resourcePath:[" + resourcePath +"]");
}
}
}

if(ancestorIndex >= inodes.length) {
ancestorIndex = inodes.length - 1;
}

for(; ancestorIndex >= 0 && inodes[ancestorIndex] == null; ancestorIndex--);

authzStatus = useDefaultAuthorizerOnly ? AuthzStatus.NOT_DETERMINED : AuthzStatus.ALLOW;

ancestor = inodes.length > ancestorIndex && ancestorIndex >= 0 ? inodes[ancestorIndex] : null;
parent = inodes.length > 1 ? inodes[inodes.length - 2] : null;
inode = inodes[inodes.length - 1]; // could be null while creating a new file

context.auditHandler = doNotGenerateAuditRecord ? null : new RangerHdfsAuditHandler(providedPath, context.isTraverseOnlyCheck, context.plugin.getHadoopModuleName(), context.plugin.getExcludedUsers(), callerContext != null ? callerContext.toString() : null);

/* Hadoop versions prior to 2.8.0 didn't ask for authorization of parent/ancestor traversal for
* reading or writing a file. However, Hadoop version 2.8.0 and later ask traversal authorization for
* such accesses. This means 2 authorization calls are made to the authorizer for a single access:
* 1. traversal authorization (where access, parentAccess, ancestorAccess and subAccess are null)
* 2. authorization for the requested permission (such as READ for reading a file)
*
* For the first call, Ranger authorizer would:
* - Deny traversal if Ranger policies explicitly deny EXECUTE access on the parent or closest ancestor
* - Else, allow traversal
*
* There are no changes to authorization of the second call listed above.
*
* This approach would ensure that Ranger authorization will continue to work with existing policies,
* without requiring policy migration/update, for the changes in behaviour in Hadoop 2.8.0.
*/
if (authzStatus == AuthzStatus.ALLOW && context.isTraverseOnlyCheck) {
authzStatus = traverseOnlyCheck(inode, inodeAttrs, resourcePath, components, parent, ancestor, ancestorIndex, context);
}

// checkStickyBit
if (authzStatus == AuthzStatus.ALLOW && parentAccess != null && parentAccess.implies(FsAction.WRITE) && parent != null && inode != null) {
if (parent.getFsPermission() != null && parent.getFsPermission().getStickyBit()) {
// user should be owner of the parent or the inode
authzStatus = (StringUtils.equals(parent.getUserName(), context.user) || StringUtils.equals(inode.getUserName(), context.user)) ? AuthzStatus.ALLOW : AuthzStatus.NOT_DETERMINED;
}
}

// checkAncestorAccess
if(authzStatus == AuthzStatus.ALLOW && ancestorAccess != null && ancestor != null) {
INodeAttributes ancestorAttribs = inodeAttrs.length > ancestorIndex ? inodeAttrs[ancestorIndex] : null;
String ancestorPath = ancestorAttribs != null ? DFSUtil.byteArray2PathString(components, 0, ancestorIndex + 1) : null;

authzStatus = isAccessAllowed(ancestor, ancestorAttribs, ancestorPath, ancestorAccess, context);
if (authzStatus == AuthzStatus.NOT_DETERMINED) {
authzStatus = checkDefaultEnforcer(fsOwner, superGroup, ugi, inodeAttrs, inodes,
pathByNameArr, snapshotId, path, ancestorIndex, doCheckOwner,
ancestorAccess, null, null, null, ignoreEmptyDir,
ancestor, parent, inode, context);
}
}

// checkParentAccess
if(authzStatus == AuthzStatus.ALLOW && parentAccess != null && parent != null) {
INodeAttributes parentAttribs = inodeAttrs.length > 1 ? inodeAttrs[inodeAttrs.length - 2] : null;
String parentPath = parentAttribs != null ? DFSUtil.byteArray2PathString(components, 0, inodeAttrs.length - 1) : null;

authzStatus = isAccessAllowed(parent, parentAttribs, parentPath, parentAccess, context);
if (authzStatus == AuthzStatus.NOT_DETERMINED) {
authzStatus = checkDefaultEnforcer(fsOwner, superGroup, ugi, inodeAttrs, inodes,
pathByNameArr, snapshotId, path, ancestorIndex, doCheckOwner,
null, parentAccess, null, null, ignoreEmptyDir,
ancestor, parent, inode, context);
}
}

// checkINodeAccess
if(authzStatus == AuthzStatus.ALLOW && access != null && inode != null) {
INodeAttributes inodeAttribs = inodeAttrs.length > 0 ? inodeAttrs[inodeAttrs.length - 1] : null;

authzStatus = isAccessAllowed(inode, inodeAttribs, resourcePath, access, context);
if (authzStatus == AuthzStatus.NOT_DETERMINED) {
authzStatus = checkDefaultEnforcer(fsOwner, superGroup, ugi, inodeAttrs, inodes,
pathByNameArr, snapshotId, path, ancestorIndex, doCheckOwner,
null, null, access, null, ignoreEmptyDir,
ancestor, parent, inode, context);
}
}

// checkSubAccess
if(authzStatus == AuthzStatus.ALLOW && subAccess != null && inode != null && inode.isDirectory()) {
Stack<SubAccessData> directories = new Stack<>();

for(directories.push(new SubAccessData(inode.asDirectory(), resourcePath)); !directories.isEmpty(); ) {
SubAccessData data = directories.pop();
ReadOnlyList<INode> cList = data.dir.getChildrenList(snapshotId);

if (!(cList.isEmpty() && ignoreEmptyDir)) {
INodeAttributes dirAttribs = data.dir.getSnapshotINode(snapshotId);

authzStatus = isAccessAllowed(data.dir, dirAttribs, data.resourcePath, subAccess, context);

if(authzStatus != AuthzStatus.ALLOW) {
break;
}

AuthzStatus subDirAuthStatus = AuthzStatus.NOT_DETERMINED;

boolean optimizeSubAccessAuthEnabled = rangerPlugin.isOptimizeSubAccessAuthEnabled();

if (optimizeSubAccessAuthEnabled) {
subDirAuthStatus = isAccessAllowedForHierarchy(data.dir, dirAttribs, data.resourcePath, subAccess, context);
}

if (subDirAuthStatus != AuthzStatus.ALLOW) {
for(INode child : cList) {
if (child.isDirectory()) {
directories.push(new SubAccessData(child.asDirectory(), resourcePath + Path.SEPARATOR_CHAR + child.getLocalName()));
}
}
}
}
}
if (authzStatus == AuthzStatus.NOT_DETERMINED) {

authzStatus = checkDefaultEnforcer(fsOwner, superGroup, ugi, inodeAttrs, inodes,
pathByNameArr, snapshotId, path, ancestorIndex, doCheckOwner,
null, null, null, subAccess, ignoreEmptyDir,
ancestor, parent, inode, context);

}
}

// checkOwnerAccess
if(authzStatus == AuthzStatus.ALLOW && doCheckOwner) {
INodeAttributes inodeAttribs = inodeAttrs.length > 0 ? inodeAttrs[inodeAttrs.length - 1] : null;
String owner = inodeAttribs != null ? inodeAttribs.getUserName() : null;

authzStatus = StringUtils.equals(context.user, owner) ? AuthzStatus.ALLOW : AuthzStatus.NOT_DETERMINED;
}
}

if (authzStatus == AuthzStatus.NOT_DETERMINED) {
authzStatus = checkDefaultEnforcer(fsOwner, superGroup, ugi, inodeAttrs, inodes,
pathByNameArr, snapshotId, path, ancestorIndex, doCheckOwner,
ancestorAccess, parentAccess, access, subAccess, ignoreEmptyDir,
ancestor, parent, inode, context);
}

if(authzStatus != AuthzStatus.ALLOW) {
FsAction action = access;

if(action == null) {
if(parentAccess != null) {
action = parentAccess;
} else if(ancestorAccess != null) {
action = ancestorAccess;
} else {
action = FsAction.EXECUTE;
}
}

throw new RangerAccessControlException("Permission denied: user=" + context.user + ", access=" + action + ", inode=\"" + resourcePath + "\"");
}
} finally {
if (context.auditHandler != null) {
context.auditHandler.flushAudit();
}

RangerPerfTracer.log(perf);

if(LOG.isDebugEnabled()) {
LOG.debug("<== RangerAccessControlEnforcer.checkPermission(" + resourcePath + ", " + access + ", user=" + context.user + ") : " + authzStatus);
}
}
}

/*
Check if parent or ancestor of the file being accessed is denied EXECUTE permission. If not, assume that Ranger-acls
allowed EXECUTE access. Do not audit this authorization check if resource is a file unless access is explicitly denied
*/
private AuthzStatus traverseOnlyCheck(INode inode, INodeAttributes[] inodeAttrs, String path, byte[][] components, INode parent, INode ancestor, int ancestorIndex,
AuthzContext context) {

if (LOG.isDebugEnabled()) {
LOG.debug("==> RangerAccessControlEnforcer.traverseOnlyCheck("
+ "path=" + path + ", user=" + context.user + ", groups=" + context.userGroups + ")");
}
final AuthzStatus ret;

INode nodeToCheck = inode;
INodeAttributes nodeAttribs = inodeAttrs.length > 0 ? inodeAttrs[inodeAttrs.length - 1] : null;
boolean skipAuditOnAllow = false;

String resourcePath = path;
if (nodeToCheck == null || nodeToCheck.isFile()) {
skipAuditOnAllow = true;
if (parent != null) {
nodeToCheck = parent;
nodeAttribs = inodeAttrs.length > 1 ? inodeAttrs[inodeAttrs.length - 2] : null;
resourcePath = inodeAttrs.length > 0 ? DFSUtil.byteArray2PathString(components, 0, inodeAttrs.length - 1) : HDFS_ROOT_FOLDER_PATH;
} else if (ancestor != null) {
nodeToCheck = ancestor;
nodeAttribs = inodeAttrs.length > ancestorIndex ? inodeAttrs[ancestorIndex] : null;
resourcePath = nodeAttribs != null ? DFSUtil.byteArray2PathString(components, 0, ancestorIndex+1) : HDFS_ROOT_FOLDER_PATH;
}
}

if (nodeToCheck != null) {
if (resourcePath.length() > 1) {
if (resourcePath.endsWith(HDFS_ROOT_FOLDER_PATH)) {
resourcePath = resourcePath.substring(0, resourcePath.length()-1);
}
}
ret = isAccessAllowedForTraversal(nodeToCheck, nodeAttribs, resourcePath, skipAuditOnAllow, context);
} else {
ret = AuthzStatus.ALLOW;
}
if (LOG.isDebugEnabled()) {
LOG.debug("<== RangerAccessControlEnforcer.traverseOnlyCheck("
+ "path=" + path + ", resourcePath=" + resourcePath + ", user=" + context.user + ", groups=" + context.userGroups + ") : " + ret);
}
return ret;
}

}

ranger的check Traverse

hdfs权限体系里, 很多操作都需要判断上级路径是否有execute权限, 而且是递归判断一直要求到最上级根目录都有execute权限.

ranger针对这个, 做了额外的处理. 好像是为了兼容以前的旧版本, 只判断当前目录的权限. 至于上级目录, 只要上一个存在的父级目录没有deny execute的策略, 就放过通行.


AuthzContext context = new AuthzContext(rangerPlugin, ugi, operationName, access == null && parentAccess == null && ancestorAccess == null && subAccess == null);


public AuthzContext(RangerHdfsPlugin plugin, UserGroupInformation ugi, String operationName, boolean isTraverseOnlyCheck) {
this.plugin = plugin;
this.user = ugi != null ? ugi.getShortUserName() : null;
this.userGroups = ugi != null ? Sets.newHashSet(ugi.getGroupNames()) : null;
this.operationName = operationName;
this.isTraverseOnlyCheck = isTraverseOnlyCheck;
}


private void checkRangerPermission(String fsOwner, String superGroup, UserGroupInformation ugi,
INodeAttributes[] inodeAttrs, INode[] inodes, byte[][] pathByNameArr,
int snapshotId, String path, int ancestorIndex, boolean doCheckOwner,
FsAction ancestorAccess, FsAction parentAccess, FsAction access,
FsAction subAccess, boolean ignoreEmptyDir, String operationName, CallerContext callerContext ) throws AccessControlException {
AuthzStatus authzStatus = AuthzStatus.NOT_DETERMINED;
String resourcePath = path;
AuthzContext context = new AuthzContext(rangerPlugin, ugi, operationName, access == null && parentAccess == null && ancestorAccess == null && subAccess == null);

context.auditHandler = doNotGenerateAuditRecord ? null : new RangerHdfsAuditHandler(providedPath, context.isTraverseOnlyCheck, context.plugin.getHadoopModuleName(), context.plugin.getExcludedUsers(), callerContext != null ? callerContext.toString() : null);

/* Hadoop versions prior to 2.8.0 didn't ask for authorization of parent/ancestor traversal for
* reading or writing a file. However, Hadoop version 2.8.0 and later ask traversal authorization for
* such accesses. This means 2 authorization calls are made to the authorizer for a single access:
* 1. traversal authorization (where access, parentAccess, ancestorAccess and subAccess are null)
* 2. authorization for the requested permission (such as READ for reading a file)
*
* For the first call, Ranger authorizer would:
* - Deny traversal if Ranger policies explicitly deny EXECUTE access on the parent or closest ancestor
* - Else, allow traversal
*
* There are no changes to authorization of the second call listed above.
*
* This approach would ensure that Ranger authorization will continue to work with existing policies,
* without requiring policy migration/update, for the changes in behaviour in Hadoop 2.8.0.
*/
if (authzStatus == AuthzStatus.ALLOW && context.isTraverseOnlyCheck) {
authzStatus = traverseOnlyCheck(inode, inodeAttrs, resourcePath, components, parent, ancestor, ancestorIndex, context);
}

}

/*
Check if parent or ancestor of the file being accessed is denied EXECUTE permission. If not, assume that Ranger-acls
allowed EXECUTE access. Do not audit this authorization check if resource is a file unless access is explicitly denied
*/
private AuthzStatus traverseOnlyCheck(INode inode, INodeAttributes[] inodeAttrs, String path, byte[][] components, INode parent, INode ancestor, int ancestorIndex,
AuthzContext context) {

if (LOG.isDebugEnabled()) {
LOG.debug("==> RangerAccessControlEnforcer.traverseOnlyCheck("
+ "path=" + path + ", user=" + context.user + ", groups=" + context.userGroups + ")");
}
final AuthzStatus ret;

INode nodeToCheck = inode;
INodeAttributes nodeAttribs = inodeAttrs.length > 0 ? inodeAttrs[inodeAttrs.length - 1] : null;
boolean skipAuditOnAllow = false;

String resourcePath = path;
if (nodeToCheck == null || nodeToCheck.isFile()) {
skipAuditOnAllow = true;
if (parent != null) {
nodeToCheck = parent;
nodeAttribs = inodeAttrs.length > 1 ? inodeAttrs[inodeAttrs.length - 2] : null;
resourcePath = inodeAttrs.length > 0 ? DFSUtil.byteArray2PathString(components, 0, inodeAttrs.length - 1) : HDFS_ROOT_FOLDER_PATH;
} else if (ancestor != null) {
nodeToCheck = ancestor;
nodeAttribs = inodeAttrs.length > ancestorIndex ? inodeAttrs[ancestorIndex] : null;
resourcePath = nodeAttribs != null ? DFSUtil.byteArray2PathString(components, 0, ancestorIndex+1) : HDFS_ROOT_FOLDER_PATH;
}
}

if (nodeToCheck != null) {
if (resourcePath.length() > 1) {
if (resourcePath.endsWith(HDFS_ROOT_FOLDER_PATH)) {
resourcePath = resourcePath.substring(0, resourcePath.length()-1);
}
}
ret = isAccessAllowedForTraversal(nodeToCheck, nodeAttribs, resourcePath, skipAuditOnAllow, context);
} else {
ret = AuthzStatus.ALLOW;
}
if (LOG.isDebugEnabled()) {
LOG.debug("<== RangerAccessControlEnforcer.traverseOnlyCheck("
+ "path=" + path + ", resourcePath=" + resourcePath + ", user=" + context.user + ", groups=" + context.userGroups + ") : " + ret);
}
return ret;
}



private AuthzStatus isAccessAllowedForTraversal(INode inode, INodeAttributes inodeAttribs, String path, boolean skipAuditOnAllow, AuthzContext context) {
final AuthzStatus ret;
String pathOwner = inodeAttribs != null ? inodeAttribs.getUserName() : null;
FsAction access = FsAction.EXECUTE;

if (pathOwner == null) {
pathOwner = inode.getUserName();
}

if (RangerHadoopConstants.HDFS_ROOT_FOLDER_PATH_ALT.equals(path)) {
path = HDFS_ROOT_FOLDER_PATH;
}

if (LOG.isDebugEnabled()) {
LOG.debug("==> RangerAccessControlEnforcer.isAccessAllowedForTraversal(" + path + ", " + access + ", " + context.user + ", " + skipAuditOnAllow + ")");
}

RangerHdfsAccessRequest request = new RangerHdfsAccessRequest(inode, path, pathOwner, access, EXECUTE_ACCCESS_TYPE, context.operationName, context.user, context.userGroups);

RangerAccessResult result = context.plugin.isAccessAllowed(request, null);

context.saveResult(result);

if (result != null && result.getIsAccessDetermined() && !result.getIsAllowed()) {
ret = AuthzStatus.DENY;
} else {
ret = AuthzStatus.ALLOW;
}

if (ret == AuthzStatus.DENY || (!skipAuditOnAllow && result != null && result.getIsAccessDetermined())) {
if (context.auditHandler != null) {
context.auditHandler.processResult(result);
}
}

if (LOG.isDebugEnabled()) {
LOG.debug("<== RangerAccessControlEnforcer.isAccessAllowedForTraversal(" + path + ", " + access + ", " + context.user + ", " + skipAuditOnAllow + "): " + ret);
}

return ret;
}

看报错日志了解鉴权流程

直接看源码理出调用路径工作量是巨大的, 但是可以直接从执行过程的错误拦截报错堆栈入手, 函数调用链明明白白, 看起来就方便多了.


0: jdbc:hive2://172.16.16.14:7001> select * from g002;
Error: Error while compiling statement: FAILED: SemanticException Unable to determine if hdfs://172.16.16.14:4007/usr/hive/warehouse/g01.db/g002 is encrypted: org.apache.hadoop.ipc.RemoteException(org.apache.ranger.authorization.hadoop.exceptions.RangerAccessControlException): Permission denied: user=gee, access=EXECUTE, inode="/usr/hive/warehouse/g01.db/g002"
at org.apache.ranger.authorization.hadoop.RangerHdfsAuthorizer$RangerAccessControlEnforcer.checkRangerPermission(RangerHdfsAuthorizer.java:501)
at org.apache.ranger.authorization.hadoop.RangerHdfsAuthorizer$RangerAccessControlEnforcer.checkPermission(RangerHdfsAuthorizer.java:248)
at org.apache.hadoop.hdfs.server.namenode.FSPermissionChecker.checkPermission(FSPermissionChecker.java:217)
at org.apache.hadoop.hdfs.server.namenode.FSPermissionChecker.checkTraverse(FSPermissionChecker.java:644)
at org.apache.hadoop.hdfs.server.namenode.FSDirectory.checkTraverse(FSDirectory.java:1845)
at org.apache.hadoop.hdfs.server.namenode.FSDirectory.checkTraverse(FSDirectory.java:1863)
at org.apache.hadoop.hdfs.server.namenode.FSDirectory.resolvePath(FSDirectory.java:686)
at org.apache.hadoop.hdfs.server.namenode.FSDirStatAndListingOp.getFileInfo(FSDirStatAndListingOp.java:112)
at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.getFileInfo(FSNamesystem.java:3214)
at org.apache.hadoop.hdfs.server.namenode.NameNodeRpcServer.getFileInfo(NameNodeRpcServer.java:1294)
at org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolServerSideTranslatorPB.getFileInfo(ClientNamenodeProtocolServerSideTranslatorPB.java:830)
at org.apache.hadoop.hdfs.protocol.proto.ClientNamenodeProtocolProtos$ClientNamenodeProtocol$2.callBlockingMethod(ClientNamenodeProtocolProtos.java)
at org.apache.hadoop.ipc.ProtobufRpcEngine$Server$ProtoBufRpcInvoker.call(ProtobufRpcEngine.java:529)
at org.apache.hadoop.ipc.RPC$Server.call(RPC.java:1073)
at org.apache.hadoop.ipc.Server$RpcCall.run(Server.java:1039)
at org.apache.hadoop.ipc.Server$RpcCall.run(Server.java:963)
at java.security.AccessController.doPrivileged(Native Method)
at javax.security.auth.Subject.doAs(Subject.java:422)
at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:2059)
at org.apache.hadoop.ipc.Server$Handler.run(Server.java:3047) (state=42000,code=40000)

created at 2023-08-07