文章目录
一、前景引入
在本系列的前篇文章里【sentry 到 ranger 系列】一、Sentry 的 Hive 鉴权插件 ,已经对最重要的 Sentry 的 Hive 插件做了一些说明,回顾一下这张 Sentry 和 Hive 交互的关系图:
以上在当 Sentry 只对 Hive 进行权限管控的时候是符合的,但是如果 Sentry 也对 Hadoop 进行了权限接管,那么权限的流转还需要再补充一些部分。可以先看下图:
二、NN插件定时拉取Sentry权限和路径信息
2.1、数据流转概述
在前一篇文章中提到了,使用 Sentry 的一个便捷的地方就是同时上了 Hive 和 Hadoop 插件,能够在 Hive 这边自动完成对应路径的权限同步。当 Sentry 拿到了 Hive 的和权限相关的 3 组数据:
- 库表名和hdfs路径的映射关系
- (sentry)角色和(linux)组的关系
- 角色 - 库表 - 权限的关系
保存到了数据库里面,Namenode 的 Sentry 插件会在启动的时候向 Sentry 请求一次这些全量的数据缓存在内存中维护起来,之后每隔 500ms 向 Sentry 请求一次增量数据用来更新数据,其中的权限数据会被转换成对应路径的权限。在有 HDFS 客户端向 NN 请求读写数据时,就会校验内存中的权限数据从而实现鉴权。
所以当有一个比如 Hive SQL 任务开始执行,首先是在 SQL 的语法解析阶段就会通过 Hive 的 Sentry 插件进行鉴权,通过后,在读取 HDFS上的文件时,还会对该任务的用户进行一次文件权限的校验,而这里就像前面说的一样,是从 Sentry 同步过来的权限,是和前面 Sentry 的权限是一致的,所以在 SQL 校验通过的基础上,这里一般也不会有权限问题。
2.2、插件源码跟踪
2.2.1、DefaulAuthorizationProvider
在讲到 NN 的 Sentry 插件之前,要先说一下 NN 默认的权限管理类 DefaulAuthorizationProvider
DefaulAuthorizationProvider 随着 FSNamesystem 的启动而启动。FSNamesystem 是 NameNode 中非常重要的一个类,其囊括了全部的关于文件系统的能力,这里主要涉及到 FSNamesystem 下构建的 FSDirectory,其本质是就是一个文件系统的树形结构实现;以及 DefaulAuthorizationProvider,是 FSNamesystem 下的默认的用来做权限校验的类。DefaulAuthorizationProvider 继承的是 AuthorizationProvider,通过调用该类的 start 方法将实现类启动起来。
void startCommonServices(Configuration conf, HAContext haContext) throws IOException {
...
// 从配置文件读取验证器并调start()方法启动
authzProvider = ReflectionUtils.newInstance(conf.getClass(
DFS_NAMENODE_AUTHORIZATION_PROVIDER_KEY,
DefaultAuthorizationProvider.class,
AuthorizationProvider.class), conf);
authzProvider.start();
AuthorizationProvider.initUsersToBypassExtProvider(conf);
AuthorizationProvider.set(authzProvider);
snapshotManager.initAuthorizationProvider();
}
2.2.2、SentryAuthorizationProvider
经过上面的剖析,很明显,Sentry 的插件的实现方式自然也是去继承并实现 AuthorizationProvider,并将配置文件里的类名改为 SentryAuthorizationProvider 的全类名,这样就实现了插件的加载了。
有意思的是,AuthorizationProvider 要做的事情其实很多,Sentry 为了不做重复的事情,在 SentryAuthorizationProvider 的 start() 方法里面,还是把 DefaulAuthorizationProvider 启动起来了。
public class SentryAuthorizationProvider
extends AuthorizationProvider implements Configurable {
...
@Override
public synchronized void start() {
...
// 偷懒用了原来的校验器
defaultAuthzProvider = new DefaultAuthorizationProvider();
defaultAuthzProvider.start();
...
if (authzInfo == null) {
authzInfo = new SentryAuthorizationInfo(conf); //提供校验的核心类
}
authzInfo.start();
...
}
这里因为实际上存在两个 AuthorizationProvider 在共同起作用,他们肯定是要分工的,这里就涉及到这个参数 sentry.hdfs.integration.path.prefixes,一般来讲其值为/user/hive/warehouse,也就是说,Sentry 会负责管理这个 Hive 路径下的权限,而其他的路径的权限,就还是 DefaulAuthorizationProvider 来管理,这一点在接下来的剖析中还会证明到。
2.2.2.1、SentryAuthorizationInfo
从上面代码中可以看到,SentryAuthorizationProvider 又启动了一个 SentryAuthorizationInfo,这个 SentryAuthorizationInfo 内部就维护了 Sentry 这边管理的权限信息,包含了权限更新和鉴权的能力。首先看下权限同步的逻辑,也就是在 SentryAuthorizationInfo 的 start() 方法中
public void start() {
if (authzPaths != null || authzPermissions != null) {
boolean success = false;
try {
success = update();
} catch (Exception ex) {
success = false;
LOG.warn("Failed to do initial update, will retry in [{}]ms, error: ",
new Object[]{retryWaitMillisec, ex.getMessage(), ex});
}
if (!success) {
waitUntil = System.currentTimeMillis() + retryWaitMillisec;
}
ThreadFactory sentryAuthInfoRefresherThreadFactory = new ThreadFactoryBuilder()
.setNameFormat(SENTRY_AUTHORIZATION_INFO_THREAD_NAME)
.setDaemon(true)
.build();
executor = Executors.newSingleThreadScheduledExecutor(sentryAuthInfoRefresherThreadFactory);
executor.scheduleWithFixedDelay(this, refreshIntervalMillisec,
refreshIntervalMillisec, TimeUnit.MILLISECONDS);
}
}
这里的 update() 方法就是同步并且更新权限的方法,一般来讲,这里是第一次调 update(),会做一次全量权限的拉取,在 HDFS路径和权限数据比较大的时候,这个地方会拖慢 Namenode 的启动速度。下面可以看到是起了一个固定线程,refreshIntervalMillisec默认值就是500ms,所以其实就是每隔 500ms(默认)调一次 update() 方法持续更新权限。所以解析来看一下 update() 的逻辑。
private boolean update() {
//Looks like getting same updates multiple times
SentryAuthzUpdate updates = updater.getUpdates();
// Updates can be null if Sentry Service is un-reachable
if (updates != null) {
if (updates.isEmpty()) {
return true; // no updates is a norm, it's still success
}
UpdateableAuthzPaths newAuthzPaths = processUpdates(
updates.getPathUpdates(), authzPaths);
UpdateableAuthzPermissions newAuthzPerms = processUpdates(
updates.getPermUpdates(), authzPermissions);
// processUpdates() should return different newAuthzPaths and newAuthzPerms object references
// if FULL updates were fetched from the Sentry server, otherwise, the same authzPaths and authzPermissions
// objects will be returned.
if (newAuthzPaths != authzPaths || newAuthzPerms != authzPermissions) {
...
if (newAuthzPaths != authzPaths) {
...
authzPaths = newAuthzPaths;
...
}
if (newAuthzPerms != authzPermissions) {
...
authzPermissions = newAuthzPerms;
...
}
...
...
return true;
}
return false;
}
其实逻辑很清晰,一共有 3 步:
- 获取更新的权限数据
- 处理更细的数据和当前的数据,也就是merge出新的版本
- 替换当前版本
2.2.2.2、获取权限更新
这里就是插件请求 Sentry Server 数据了,同样还是 SentryClient 调的 thrift 接口,这里的接口参数是上一次请求数据的 id 最大值,Sentry Server 根据 id,去查 sentry_perm_change 和 sentry_path_change 这两张表中 id 请求参数的变更数据然后组装返回。这两张表的数据是已 json 格式存储的,样例如下:
{
"1": {
"tf": 0
},
"2": {
"i64": 0
},
"3": {
"map": [ --表示内嵌的结构体为一个map类型
"str", --表示内嵌结构的key是字符串
"rec", --表示内嵌结构的value是一个结构体
1, --表示内嵌结构的元素个数为1
{
"test2.ctable": {
"1": {
"str": "test2.ctable"
},
"2": {
"map": [
"str",
"str",
0,
{}
]
},
"3": {
"map": [
"str",
"str", --表示内嵌结构的value是字符串
1,
{
"__ALL_ROLES__": "__ALL_ROLES__"
}
]
}
}
}
]
},
"4": {
"map": [
"str",
"rec",
0,
{}
]
}
}
上面这条变更信息就是一个 Sentry 定义的 thrift 对象 json 序列化后的字符串,其含义就是删除表 test2.ctable 对所有角色的权限。再简单对上述json的含义做一个说明:
- “1” : 忽略
- “2” : 忽略
- “3” : 权限变更信息
- “1” : 验证对象(authzObject)
- “2” : 添加的权限
- “3” : 删除的权限
- “4” : 路径变更信息
json 里面带元素类型和元素个数这些信息是 thrift 消息体的风格,对序列化和反序列化的效率有帮助;哪个数字后面的信息是属于什么类型是由 Sentry 定义的,这个结构的定义不用太在意。最终返回给 SentryClient 的权限变更信息就是 PermissionsUpdate 对象,路径变更信息就是 PathsUpdate 对象,他们都被以一个 list 集合封装在 SentryAuthzUpdate 里返回给客户端,就是在上述 update() 里的 updates。
还有一个点需要注意,Sentry 每隔 12 小时清理一次 sentry_perm_change 和 sentry_path_change 表中的变更数据,只保留最近 200 条。如果变更数据比较大的情况下,又刚好遇到了 Sentry 处理生命周期,可能导致 Sentry 发现 id 已经不存在表中,从而返回给客户端全量的数据。
2.2.2.3、处理权限更新
在上一步获取到了权限更新数据后,update() 方法中就分别对路径更新和权限更新做合并,以路径更新为例,最终调到 UpdateableAuthzPaths 类的 updatePartial 方法
@Override
public void updatePartial(Iterable<PathsUpdate> updates, ReadWriteLock lock) {
...
for (PathsUpdate update : updates) {
applyPartialUpdate(update);
...
seqNum.set(update.getSeqNum());
...
}
...
}
private void applyPartialUpdate(PathsUpdate update) {
// Handle alter table rename : will have exactly 2 path changes
// 1 is add path and the other is del path and oldName != newName
if (update.getPathChanges().size() == 2) {
List<TPathChanges> pathChanges = update.getPathChanges();
TPathChanges newPathInfo = null;
TPathChanges oldPathInfo = null;
if (pathChanges.get(0).getAddPathsSize() == 1
&& pathChanges.get(1).getDelPathsSize() == 1) {
newPathInfo = pathChanges.get(0);
oldPathInfo = pathChanges.get(1);
} else if (pathChanges.get(1).getAddPathsSize() == 1
&& pathChanges.get(0).getDelPathsSize() == 1) {
newPathInfo = pathChanges.get(1);
oldPathInfo = pathChanges.get(0);
}
if (newPathInfo != null && oldPathInfo != null &&
!newPathInfo.getAuthzObj().equalsIgnoreCase(oldPathInfo.getAuthzObj())) {
paths.renameAuthzObject(
oldPathInfo.getAuthzObj(), oldPathInfo.getDelPaths(),
newPathInfo.getAuthzObj(), newPathInfo.getAddPaths());
return;
}
}
这里的 pathChanges 就是前面的 json 反序列化后的对象,前面列举的是权限变更 json,路径变更的 json 差不多的样子。最后就是根据得到的信息,去更新 paths 对象;同理,权限信息维护的就是 perms 对象,这两个对象就是 SentryAuthorizationProvider 向外提供鉴权能力的基础。
三、NN插件鉴权能力
SentryAuthorizationProvider 本身只是维护权限信息,不做权限检查,只是向外界提供数据,对 NN 暴露的方法为 getAclFeature 和 getFsPermission。其调用链路为:
FSDirectory#checkPermission ->
FSPermissionChecker#checkPermission ->
AuthorizationProvider#checkPermission ->
SentryAuthorizationProvider#checkPermission ->
DefaultAuthorizationProvider#checkPermission ->
DefaultAuthorizationProvider#check ->
INode#getFsPermission和INode#getAclFeature
分别就调到了SentryAuthorizationProvider的getAclFeature和getFsPermission。
FsPermission 就是形如 777 这样的权限,AclFeature 就是对 HDFS使用 getfacl 这样的命令后出现的更细粒度的访问权限,定义了用户、组、超级组、mask 等。
3.1、getFsPermission
@Override
public FsPermission getFsPermission(
INodeAuthorizationInfo node, int snapshotId) {
Preconditions.checkNotNull(node, "node");
FsPermission permission;
String[] pathElements = getPathElements(node);
if (!isSentryManaged(pathElements)) {
try {
permission = defaultAuthzProvider.getFsPermission(node, snapshotId);
} catch (RuntimeException e) {
LOG.error("### getFsPermission for " + nodePath(node, snapshotId, true) + " failed", e);
throw e;
}
} else {
FsPermission returnPerm = this.permission;
// Handle case when prefix directory is itself associated with an
// authorizable object (default db directory in hive)
// An executable permission needs to be set on the the prefix directory
// in this case.. else, subdirectories (which map to other dbs) will
// not be travesible.
for (String [] prefixPath : authzInfo.getPathPrefixes()) {
if (Arrays.equals(prefixPath, pathElements)) {
returnPerm = FsPermission.createImmutable((short)(returnPerm.toShort() | 0x01));
break;
}
}
permission = returnPerm;
}
if (LOG.isDebugEnabled()) {
LOG.debug("### getFsPermission for {} : {}", nodePath(node, snapshotId), permission);
}
return permission;
}
可以从 isSentryManaged 这个条件看到,是 Sentry 进行管理的使用 Sentry 插件维护的权限,否则就使用 DefaulAuthorizationProvider 的权限,而 prefixPath,就是前面提到的/user/hive/warehouse 路径,返回的就是默认的这个路径的权限。主要看的还是 getAclFeature 返回的权限。
3.2、getAclFeature
这里也是和 getFsPermission 一样,非 Hive 路径下的由 DefaulAuthorizationProvider 管理,而 Sentry 管理的权限核心就是这两行
addToACLMap(aclMap, authzInfo.getAclEntries(pathElements));
f = new SentryAclFeature(ImmutableList.copyOf(aclMap.values()));`
addToACLMap 和 SentryAclFeature 都是封装,直接看 getAclEntries
public List<AclEntry> getAclEntries(String[] pathElements) {
...
Set<String> authzObjs = authzPaths.findAuthzObject(pathElements);
...
Set<AclEntry> retSet = new HashSet<>();
...
// No duplicate acls should be added.
for (String authzObj: authzObjs) {
retSet.addAll(authzPermissions.getAcls(authzObj));
}
...
}
这里的 pathElements 就是 HDFS路径,根据这个路径,找到验证对象 authzObjs,也就是 Hive 库、表、udf 等,然后每一个验证对象去那对应的 acl 权限。这里就用到的是前面从 Sentry 获取到并一直更新的路径信息和权限信息。
至此整体流程已经串起来了,其中还有一些细节,比如权限是怎么从 Hive 权限映射成 HDFS权限等插件维护权限信息之类的问题,可以在需要的时候再关注就好了。
四、收尾
本篇从 NN 的 Sentry 插件怎么同步 Sentry 的权限信息,到如何提供鉴权能力做了整体的追踪,对关键代码进行了解析,能对 Sentry 在最核心的 Hive 和 Hadoop 组件的对接上有更深入的了解,同上一篇结合起来看的话效果会更好。
如果可以的话希望给个一键三连(。•̀ᴗ-)✧