【sentry 到 ranger 系列】二、Sentry 的 Hadoop 鉴权插件

一、前景引入

  在本系列的前篇文章里【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 步:

  1. 获取更新的权限数据
  2. 处理更细的数据和当前的数据,也就是merge出新的版本
  3. 替换当前版本
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 组件的对接上有更深入的了解,同上一篇结合起来看的话效果会更好。
  如果可以的话希望给个一键三连(。•̀ᴗ-)✧

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫语大数据

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值