一、前景引入
在本系列的第一篇文章里【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 的权限粒度对比。
Hive | Sentry |
---|---|
ALL | ALL |
SELECT | SELECT |
UPDATE | INSERT |
ALTER | 无单独管控能力,集成在ALL权限 |
CREATE | 无单独管控能力,集成在ALL权限 |
DROP | 无单独管控能力,集成在ALL权限 |
INDEX | 无单独管控能力,集成在ALL权限 |
LOCK | 无单独管控能力,集成在ALL权限 |
SHOW_DATABASE | 无单独管控能力,集成在ALL权限 |
当 Sentry 接管了 Hive 之后,原先 Hive 的权限语法就不能用了,不过大部分两者用法都差不多,具体的使用可以直接参考官方的文档🦋 cloudera sentry 权限语法🦋
除了以上原生的赋权操作,还有一部分操作是 Sentry 需要维护的,那就是库表的名称信息,试想一下,当 tom
有对表 test
的 select
权限,当执行 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
的内部类Client
的drop_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;
然后很方便的跳转到其实现方法,这里即服务端的SentryPolicyStoreProcessor
的drop_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
方法,所以直接看MetastoreAuthzBinding
的onEvent
方法:
/**
* 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;
}
}
还是以删表为例,发现所有操作其实都被封装成了校验对象,走到MetastoreAuthzBinding
的authorize
方法,这里用到了 Sentry 封装的一个校验模块:sentry-provider
,其整个校验被抽象为三部分组件:AuthorizationProvider
、PolicyEngine
、ProviderBackend
,调用关系大致如下
通过这种方式,将校验的通用部分抽离,差异部分实现不同实现类,主要分为 DB、搜索引擎、Kafka等类型。最后的ProviderBackend
集成了 Sentry 的 Thrift Client,根据要校验的信息,从 Sentry Server 中拉取数据实现鉴权,如果校验不通过,则通过抛异常的方式被捕获处理后返回给用户端。至此整个鉴权的逻辑完成闭环√文章来源:https://www.toymoban.com/news/detail-795585.html
四、收尾
本篇从 Sentry 收集 Hive 权限信息,到 Sentry 的鉴权插件怎么在 Hive 和 Sentry 之间实现鉴权,进行了梳理,对关键代码进行了解析,能从感性和理性的角度都对 Sentry 接管 Hive 权限有更深入的认识。
如果对在读的你有帮助,希望能给个一键三连(。•̀ᴗ-)✧文章来源地址https://www.toymoban.com/news/detail-795585.html
到了这里,关于【sentry 到 ranger 系列】一、Sentry 的 Hive 鉴权插件的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!