【sentry 到 ranger 系列】一、Sentry 的 Hive 鉴权插件

一、前景引入

  在本系列的第一篇文章里【sentry 到 ranger 系列】sentry 的开篇 ,已经对 Sentry 所处的一个整体的位置有了了解,如下图所示 在这里插入图片描述
  接下来,从 Hive 的鉴权开始看一下 Sentry 究竟怎么实现的权限管理和提供的鉴权能力。

二、Sentry 对 Hive 【授权】的接管

2.1、权限数据的产生

  在了解权限的接管细节前,可以先了解下 Hive 的权限数据是由什么场景产生的。
  首先,Hive 本身是有一套权限管理的,甚至 Hive 本身的权限管理从权限粒度上来看比 Sentry 的粒度更细更好用。而使用 Sentry 的一个原因就是当 Hive 和 HDFS 都使用了 Sentry 之后,能自动完成 Hive 的权限到 HDFS 路径的映射,不用再在 HDFS 上考虑怎么维护权限信息甚至干脆不做权限验证(后续会做 HDFS 的这部分逻辑分析)。
  Sentry 接管 Hive 后,一共只有三种权限可配置:ALL、SELECT、INSERT。后面两个很好理解,而 ALL 权限,包含了查询和写入这两种使用以外的所有权限,比如 ALTER、CREATE 等等。这里简单做一下 Hive 和 Sentry 的权限粒度对比。

HiveSentry
ALLALL
SELECTSELECT
UPDATEINSERT
ALTER无单独管控能力,集成在ALL权限
CREATE无单独管控能力,集成在ALL权限
DROP无单独管控能力,集成在ALL权限
INDEX无单独管控能力,集成在ALL权限
LOCK无单独管控能力,集成在ALL权限
SHOW_DATABASE无单独管控能力,集成在ALL权限

  当 Sentry 接管了 Hive 之后,原先 Hive 的权限语法就不能用了,不过大部分两者用法都差不多,具体的使用可以直接参考官方的文档🦋 cloudera sentry 权限语法🦋
  除了以上原生的赋权操作,还有一部分操作是 Sentry 需要维护的,那就是库表的名称信息,试想一下,当 tom 有对表 testselect 权限,当执行 drop table 操作的时候,Sentry 应该怎么维护原先的权限关系呢?我们直接看一下 Sentry 源码中的一段描述,位置在 class SentryMetastorePostEventListener

  /**
   * Drop the privileges on the database. Note that child tables will be
   * dropped individually by client, so we just need to handle the removing
   * the db privileges. The table drop should cleanup the table privileges.
   */
  @Override
  public void onDropDatabase(DropDatabaseEvent dbEvent) throws MetaException {
  ...

  也就是说 Sentry 的做法是要解除原先表 test 的所有权限关系。同理可验证,当执行 alter table rename 操作的时候,Sentry 会更新 tom 的权限为更新后的表的权限。

2.2、插件源码跟踪

  从前面可以看到,Sentry 权限的更新包含两部分,一部分是授权语句,另一部分来自于库表的部分 DDL 操作。
  在前文【sentry 到 ranger 系列】sentry 的开篇中讲到的SentryHiveAuthorizationTaskFactoryImply就是同步授权语句的,而对 DDL 的权限处理就交给的是SentryMetastorePostEventListener。因此现在应该就能区分开,为什么这里有两个类都在做权限同步操作了。前文对SentryMetastorePostEventListener的细节聊得比较少,本篇重点聊聊SentryMetastorePostEventListener的实现,关键走通之后,SentryHiveAuthorizationTaskFactoryImply的细节也是同理的。
  下面我们看一下SentryMetastorePostEventListener做了哪些事情,这能从其实现方法中见微知著:

onConfigChange(ConfigChangeEvent): void
onCreateTable(CreateTableEvent): void
onDropTable(DropTableEvent): void
onAlterTable(AlterTableEvent): void
onAddPartition(AddPartitionEvent): void
onDropPartition(DropPartitionEvent): void
onAlterPartition(AlterPartitionEvent): void
onCreateDatabase(CreateDatabaseEvent): void
onDropDatabase(DropDatabaseEvent): void
onLoadPartitionDone(LoadPartitionDoneEvent): void
onAddIndex(AddIndexEvent): void
onDroplndex(DroplndexEvent): voidi
onAlterindex(AlterindexEvent): void
onCreateFunction(CreateFunctionEvent): void.
onDropFunction(DropFunctionEvent): void.
onlnsert(InsertEvent): void

  这也符合我们对其定位的推测,其方法都是对 DDL 的处理。以onDropTable为例,方法代码如下:


  @Override
  public void onDropTable(DropTableEvent tableEvent) throws MetaException {

    // don't sync paths/privileges if the operation has failed
    if (!tableEvent.getStatus()) {
      LOGGER.debug("Skip syncing paths/privileges with Sentry server for onDropTable event," +
        " since the operation failed. \n");
      return;
    }

    if (tableEvent.getTable().getSd().getLocation() != null) {
      String authzObj = tableEvent.getTable().getDbName() + "."
          + tableEvent.getTable().getTableName();
      for (SentryMetastoreListenerPlugin plugin : sentryPlugins) {
        plugin.removeAllPaths(authzObj, null);
      }
    }
    // drop the privileges on the given table
    if (!syncWithPolicyStore(AuthzConfVars.AUTHZ_SYNC_DROP_WITH_POLICY_STORE)) {
      return;
    }

    if (!tableEvent.getStatus()) {
      return;
    }

    dropSentryTablePrivilege(tableEvent.getTable().getDbName(),
        tableEvent.getTable().getTableName());
  }

  正常情况下就会走到方法dropSentryTablePrivilege


  private void dropSentryTablePrivilege(String dbName, String tabName)
      throws MetaException {
    List<Authorizable> authorizableTable = new ArrayList<Authorizable>();
    authorizableTable.add(server);
    authorizableTable.add(new Database(dbName));
    authorizableTable.add(new Table(tabName));

    try {
      dropSentryPrivileges(authorizableTable);
    } catch (SentryUserException e) {
      throw new MetaException(
          "Failed to remove Sentry policies for drop table " + dbName + "."
              + tabName + " Error: " + e.getMessage());
    } catch (IOException e) {
      throw new MetaException("Failed to find local user " + e.getMessage());
    }

  }
  private void dropSentryPrivileges(
      List<? extends Authorizable> authorizableTable)
      throws SentryUserException, IOException, MetaException {
    String requestorUserName = UserGroupInformation.getCurrentUser()
        .getShortUserName();
    try (SentryPolicyServiceClient sentryClient = SentryServiceClientFactory.create(authzConf)) {
      sentryClient.dropPrivileges(requestorUserName, authorizableTable);
    } catch (Exception e) {
      throw new MetaException("Failed to connect to Sentry service "
              + e.getMessage());
    }
  }

  dropPrivileges是抽象方法,实现在SentryPolicyServiceClientDefaultImpl:

  @Override
  public void dropPrivileges(String requestorUserName,
                             List<? extends Authorizable> authorizableObjects)
    throws SentryUserException {
    TSentryAuthorizable tSentryAuthorizable = setupSentryAuthorizable(authorizableObjects);

    TDropPrivilegesRequest request = new TDropPrivilegesRequest(
      ThriftConstants.TSENTRY_SERVICE_VERSION_CURRENT, requestorUserName,
      tSentryAuthorizable);
    try {
      TDropPrivilegesResponse response = client.drop_sentry_privilege(request);
      Status.throwIfNotOk(response.getStatus());
    } catch (TException e) {
      throw new SentryUserException(THRIFT_EXCEPTION_MESSAGE, e);
    }
  }

  这里的 Client 就是 Thfit 协议的客户端了,就能发送给 Sentry Server 的 Thrift 服务端。Thrift 的代码是挺好追踪的,我们就来看看怎么发送给 Thrift 服务端的。

2.3、Thrift 接口跟踪

  点进 client.drop_sentry_privilege(request) 方法里,就进入到了 Thrift 的接口定义,SentryPolicyService的内部类Clientdrop_sentry_privilege方法:

    public TDropPrivilegesResponse drop_sentry_privilege(TDropPrivilegesRequest request) throws org.apache.thrift.TException
    {
      send_drop_sentry_privilege(request);
      return recv_drop_sentry_privilege();
    }

  这个地方不用着急点进去,因为再点进去就是 Thrift 内部实现了,重点关注SentryPolicyService,看看它的签名:

@Generated(value = "Autogenerated by Thrift Compiler (0.9.3)", date = "2017-04-26")
public class SentryPolicyService {...}

  可以看到代码是被生成出来的,在使用 Thrift 的时候,会先对协议编写一个协议文件,以.thrift结尾,再由 Thrift 通过代码生成技术来生成各类 java 文件,这里像 SentryPolicyService 就是一个 Sentry 和 Hive 的协议定义的 java 文件体现,里面包含了对客户端的定义和服务端的定义以及接口方法处理的定义。其他还会生成接口消息的实体类等等。

@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked"})
@Generated(value = "Autogenerated by Thrift Compiler (0.9.3)", date = "2017-04-26")
public class SentryPolicyService {
  public interface Iface {...}
  public interface AsyncIface {...}
  public static class Client extends org.apache.thrift.TServiceClient implements Iface {...}
  public static class AsyncClient extends org.apache.thrift.async.TAsyncClient implements AsyncIface {...}
  public static class Processor<I extends Iface> extends org.apache.thrift.TBaseProcessor<I> implements org.apache.thrift.TProcessor {...}
  public static class AsyncProcessor<I extends AsyncIface> extends org.apache.thrift.TBaseAsyncProcessor<I> {...}
  public static class create_sentry_role_args implements org.apache.thrift.TBase<create_sentry_role_args, create_sentry_role_args._Fields>, java.io.Serializable, Cloneable, Comparable<create_sentry_role_args> {...}
}

  对每一个客户端Client的方法,如上面的drop_sentry_privilege,都能在名为 Iface 的接口里面找到服务侧的同名方法:

    public TDropPrivilegesResponse drop_sentry_privilege(TDropPrivilegesRequest request) throws org.apache.thrift.TException;

  然后很方便的跳转到其实现方法,这里即服务端的SentryPolicyStoreProcessordrop_sentry_privilege方法:

  @Override
  public TDropPrivilegesResponse drop_sentry_privilege(
      TDropPrivilegesRequest request) throws TException {
    final Timer.Context timerContext = sentryMetrics.dropPrivilegeTimer.time();
    TDropPrivilegesResponse response = new TDropPrivilegesResponse();
    try {
      validateClientVersion(request.getProtocol_version());
      authorize(request.getRequestorUserName(), adminGroups);

      // TODO: now only has SentryPlugin. Once add more SentryPolicyStorePlugins,
      // TODO: need to differentiate the updates for different Plugins.
      Preconditions.checkState(sentryPlugins.size() <= 1);
      Update update = null;
      for (SentryPolicyStorePlugin plugin : sentryPlugins) {
        update = plugin.onDropSentryPrivilege(request);
      }
      if (update != null) {
        sentryStore.dropPrivilege(request.getAuthorizable(), update);
      } else {
        sentryStore.dropPrivilege(request.getAuthorizable());
      }
      response.setStatus(Status.OK());
    } catch (SentryAccessDeniedException e) {
      LOGGER.error(e.getMessage(), e);
      response.setStatus(Status.AccessDenied(e.getMessage(), e));
    } catch (SentryThriftAPIMismatchException e) {
      LOGGER.error(e.getMessage(), e);
      response.setStatus(Status.THRIFT_VERSION_MISMATCH(e.getMessage(), e));
    } catch (Exception e) {
      String msg = "Unknown error for request: " + request + ", message: "
          + e.getMessage();
      LOGGER.error(msg, e);
      response.setStatus(Status.RuntimeError(msg, e));
    } finally {
      timerContext.stop();
    }
    return response;
  }

  这样就进入到服务端的逻辑了,如此一来,对于 Thrift 的接口,就能将客户端和服务端的处理逻辑串联起来,对于我们理解业务逻辑或者debug,就够用了。
  至于服务端的架构和逻辑,我们在后面再探索。

三、Sentry 对 Hive 【鉴权】的接管

  通过前面的跟踪,在忽略 Sentry Server 怎么维护数据的情况下,现在已经对 Sentry 怎么收集 Hive 的权限信息有了足够的认知。接下来,就是这些数据怎么被使用了,也就是鉴权的部分。

3.1、鉴权在 Hive 处理数据中的生命周期

  当用户尝试访问 Hive 表或执行 Hive 操作时,HiveServer2 会与 HiveMetastore 进行通信,验证用户的权限是否允许其进行所请求的操作,当权限校验通过,才会执行数据处理作业。还是以删表为例,来看一下 HiveMetastore(HMS) 会怎么处理请求。
  HMS 也是一个 Thrift 的服务端实现,我们已经对 Thrift 不陌生了。HMS 继承自ThriftHiveMetastore,在Iface中能找到drop_table方法,在 HMS 中实际实现如下:


    private boolean drop_table_core(final RawStore ms, final String dbname, final String name,
        final boolean deleteData, final EnvironmentContext envContext,
        final String indexName) throws NoSuchObjectException,
        MetaException, IOException, InvalidObjectException, InvalidInputException {
      boolean success = false;
      boolean isExternal = false;
      Path tblPath = null;
      List<Path> partPaths = null;
      Table tbl = null;
      boolean ifPurge = false;
      Map<String, String> transactionalListenerResponses = Collections.emptyMap();
      try {
        ms.openTransaction();
        // drop any partitions
        tbl = get_table_core(dbname, name);
		...
        firePreEvent(new PreDropTableEvent(tbl, deleteData, this));
		...
        // Drop the partitions and get a list of locations which need to be deleted
        partPaths = dropPartitionsAndGetLocations(ms, dbname, name, tblPath,
            tbl.getPartitionKeys(), deleteData && !isExternal);
        if (!ms.dropTable(dbname, name)) {
          String tableName = dbname + "." + name;
          throw new MetaException(indexName == null ? "Unable to drop table " + tableName:
              "Unable to drop index table " + tableName + " for index " + indexName);
        } else {
          if (!transactionalListeners.isEmpty()) {
            transactionalListenerResponses =
                MetaStoreListenerNotifier.notifyEvent(transactionalListeners,
                                                      EventType.DROP_TABLE,
                                                      new DropTableEvent(tbl, true, deleteData, this),
                                                      envContext);
          }
          success = ms.commitTransaction();
        }
      } finally {
        if (!success) {
          ms.rollbackTransaction();
        } else if (deleteData && !isExternal) {
          // Data needs deletion. Check if trash may be skipped.
          // Delete the data in the partitions which have other locations
          deletePartitionData(partPaths, ifPurge);
          // Delete the data in the table
          deleteTableData(tblPath, ifPurge);
          // ok even if the data is not deleted
        }

        if (!listeners.isEmpty()) {
          MetaStoreListenerNotifier.notifyEvent(listeners,
                                                EventType.DROP_TABLE,
                                                new DropTableEvent(tbl, success, deleteData, this),
                                                envContext,
                                                transactionalListenerResponses, ms);
        }
      }
      return success;
    }

  其中firePreEvent实现如下:

    private void firePreEvent(PreEventContext event) throws MetaException {
      for (MetaStorePreEventListener listener : preListeners) {
        try {
          listener.onEvent(event);
        } catch (NoSuchObjectException e) {
          throw new MetaException(e.getMessage());
        } catch (InvalidOperationException e) {
          throw new MetaException(e.getMessage());
        }
      }
    }

  MetaStoreListenerNotifier.notifyEvent实现如下:

  /**
   * Notify a list of listeners about a specific metastore event. Each listener notified might update
   * the (ListenerEvent) event by setting a parameter key/value pair. These updated parameters will
   * be returned to the caller.
   *
   * @param listeners List of MetaStoreEventListener listeners.
   * @param eventType Type of the notification event.
   * @param event The ListenerEvent with information about the event.
   * @return A list of key/value pair parameters that the listeners set. The returned object will return an empty
   *         map if no parameters were updated or if no listeners were notified.
   * @throws MetaException If an error occurred while calling the listeners.
   */
  public static Map<String, String> notifyEvent(List<MetaStoreEventListener> listeners,
                                                EventType eventType,
                                                ListenerEvent event) throws MetaException {

    Preconditions.checkNotNull(listeners, "Listeners must not be null.");
    Preconditions.checkNotNull(event, "The event must not be null.");

    for (MetaStoreEventListener listener : listeners) {
      notificationEvents.get(eventType).notify(listener, event);
    }

    // Each listener called above might set a different parameter on the event.
    // This write permission is allowed on the listener side to avoid breaking compatibility if we change the API
    // method calls.
    return event.getParameters();
  }

  也就是说,一次删表行为,在 HMS 中经历了大致如下的生命周期:

1、MetaStorePreEventListener 处理 -> 2、HMS 处理元数据(transactionalListeners) -> 3、MetaStoreEventListener 处理

  和前面的授权的接管联系起来可以看到,SentryMetastorePostEventListener是处于第3个阶段;那么用于鉴权的操作推测应该就是处于第1个阶段,应该是继承了MetaStorePreEventListener

3.2、MetastoreAuthzBinding

  如前面所言,MetastoreAuthzBinding 就是Sentry 插件继承了MetaStorePreEventListener 的实现,前面的firePreEvent方法中看到是直接调的 listener 的onEvent方法,所以直接看MetastoreAuthzBindingonEvent方法:

  /**
   * Main listener callback which is the entry point for Sentry
   */
  @Override
  public void onEvent(PreEventContext context) throws MetaException,
      NoSuchObjectException, InvalidOperationException {

    if (!needsAuthorization(getUserName())) {
      return;
    }
    switch (context.getEventType()) {
    case CREATE_TABLE:
      authorizeCreateTable((PreCreateTableEvent) context);
      break;
    case DROP_TABLE:
      authorizeDropTable((PreDropTableEvent) context);
      break;
    case ALTER_TABLE:
      authorizeAlterTable((PreAlterTableEvent) context);
      break;
    case ADD_PARTITION:
      authorizeAddPartition((PreAddPartitionEvent) context);
      break;
    case DROP_PARTITION:
      authorizeDropPartition((PreDropPartitionEvent) context);
      break;
    case ALTER_PARTITION:
      authorizeAlterPartition((PreAlterPartitionEvent) context);
      break;
    case CREATE_DATABASE:
      authorizeCreateDatabase((PreCreateDatabaseEvent) context);
      break;
    case DROP_DATABASE:
      authorizeDropDatabase((PreDropDatabaseEvent) context);
      break;
    case LOAD_PARTITION_DONE:
      // noop for now
      break;
    default:
      break;
    }
  }

  还是以删表为例,发现所有操作其实都被封装成了校验对象,走到MetastoreAuthzBindingauthorize方法,这里用到了 Sentry 封装的一个校验模块:sentry-provider,其整个校验被抽象为三部分组件:
AuthorizationProviderPolicyEngineProviderBackend,调用关系大致如下
在这里插入图片描述
  通过这种方式,将校验的通用部分抽离,差异部分实现不同实现类,主要分为 DB、搜索引擎、Kafka等类型。最后的ProviderBackend集成了 Sentry 的 Thrift Client,根据要校验的信息,从 Sentry Server 中拉取数据实现鉴权,如果校验不通过,则通过抛异常的方式被捕获处理后返回给用户端。至此整个鉴权的逻辑完成闭环√

四、收尾

  本篇从 Sentry 收集 Hive 权限信息,到 Sentry 的鉴权插件怎么在 Hive 和 Sentry 之间实现鉴权,进行了梳理,对关键代码进行了解析,能从感性和理性的角度都对 Sentry 接管 Hive 权限有更深入的认识。
  如果对在读的你有帮助,希望能给个一键三连(。•̀ᴗ-)✧

  • 29
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
SentryRanger是CDH中常用的细粒度授权管理工具,下面分别介绍它们的使用方法: 1. Sentry的使用 (1)安装和配置Sentry 首先需要在CDH集群中安装和配置Sentry服务。具体安装和配置步骤可以参考CDH文档中的说明。 (2)创建角色和授权 可以使用Sentry提供的命令行工具或Web界面来创建角色和授权。例如,以下命令可以创建一个名为“finance_analyst”的角色,并将其授权访问表“my_table”: ``` $ sentry --command create-role --role finance_analyst $ sentry --command grant --role finance_analyst --privilege "server=server1->db=my_database->table=my_table->action=select" ``` (3)验证授权 创建角色和授权之后,可以使用授权用户的身份来验证授权是否生效。例如,可以使用以下命令来验证用户“user1”是否可以访问表“my_table”: ``` $ beeline -u jdbc:hive2://localhost:10000 -n user1 -e "select * from my_table" ``` 2. Ranger的使用 (1)安装和配置Ranger 首先需要在CDH集群中安装和配置Ranger服务。具体安装和配置步骤可以参考CDH文档中的说明。 (2)创建策略和条件 可以使用Ranger提供的Web界面来创建策略和条件。例如,可以创建一个策略,仅允许特定用户访问表“my_table”。在创建策略时,可以指定访问条件,例如“user=user1”。 (3)验证授权 创建策略之后,可以使用授权用户的身份来验证授权是否生效。例如,可以使用以下命令来验证用户“user1”是否可以访问表“my_table”: ``` $ beeline -u jdbc:hive2://localhost:10000 -n user1 -e "select * from my_table" ``` 综上所述,SentryRanger的使用方法都比较简单,可以根据实际需求选择合适的工具来管理Hive数据表的访问权限。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

猫语大数据

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

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

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

打赏作者

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

抵扣说明:

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

余额充值