文章目录
一、前景引入
在本系列的前篇文章里【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 方法将实现类启动起来。
voidstartCommonServices(Configuration conf,HAContext haContext)throwsIOException{...// 从配置文件读取验证器并调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 启动起来了。
publicclassSentryAuthorizationProviderextendsAuthorizationProviderimplementsConfigurable{...@Overridepublicsynchronizedvoidstart(){...// 偷懒用了原来的校验器
defaultAuthzProvider =newDefaultAuthorizationProvider();
defaultAuthzProvider.start();...if(authzInfo ==null){
authzInfo =newSentryAuthorizationInfo(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() 方法中
publicvoidstart(){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: ",newObject[]{retryWaitMillisec, ex.getMessage(), ex});}if(!success){
waitUntil =System.currentTimeMillis()+ retryWaitMillisec;}ThreadFactory sentryAuthInfoRefresherThreadFactory =newThreadFactoryBuilder().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() 的逻辑。
privatebooleanupdate(){//Looks like getting same updates multiple timesSentryAuthzUpdate updates = updater.getUpdates();// Updates can be null if Sentry Service is un-reachableif(updates !=null){if(updates.isEmpty()){returntrue;// 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;...}......returntrue;}returnfalse;}
其实逻辑很清晰,一共有 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 方法
@OverridepublicvoidupdatePartial(Iterable<PathsUpdate> updates,ReadWriteLock lock){...for(PathsUpdate update : updates){applyPartialUpdate(update);...
seqNum.set(update.getSeqNum());...}...}privatevoidapplyPartialUpdate(PathsUpdate update){// Handle alter table rename : will have exactly 2 path changes// 1 is add path and the other is del path and oldName != newNameif(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);}elseif(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
@OverridepublicFsPermissiongetFsPermission(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 =newSentryAclFeature(ImmutableList.copyOf(aclMap.values()));`
addToACLMap 和 SentryAclFeature 都是封装,直接看 getAclEntries
publicList<AclEntry>getAclEntries(String[] pathElements){...Set<String> authzObjs = authzPaths.findAuthzObject(pathElements);...Set<AclEntry> retSet =newHashSet<>();...// 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 组件的对接上有更深入的了解,同上一篇结合起来看的话效果会更好。
如果可以的话希望给个一键三连(。•̀ᴗ-)✧
版权归原作者 猫语大数据 所有, 如有侵权,请联系我们删除。