文章目录
一、前景引入
在本系列的第一篇文章里【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 的权限粒度对比。
HiveSentryALLALLSELECTSELECTUPDATEINSERTALTER无单独管控能力,集成在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.
*/@OverridepublicvoidonDropDatabase(DropDatabaseEvent dbEvent)throwsMetaException{...
也就是说 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
为例,方法代码如下:
@OverridepublicvoidonDropTable(DropTableEvent tableEvent)throwsMetaException{// don't sync paths/privileges if the operation has failedif(!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 tableif(!syncWithPolicyStore(AuthzConfVars.AUTHZ_SYNC_DROP_WITH_POLICY_STORE)){return;}if(!tableEvent.getStatus()){return;}dropSentryTablePrivilege(tableEvent.getTable().getDbName(),
tableEvent.getTable().getTableName());}
正常情况下就会走到方法
dropSentryTablePrivilege
privatevoiddropSentryTablePrivilege(String dbName,String tabName)throwsMetaException{List<Authorizable> authorizableTable =newArrayList<Authorizable>();
authorizableTable.add(server);
authorizableTable.add(newDatabase(dbName));
authorizableTable.add(newTable(tabName));try{dropSentryPrivileges(authorizableTable);}catch(SentryUserException e){thrownewMetaException("Failed to remove Sentry policies for drop table "+ dbName +"."+ tabName +" Error: "+ e.getMessage());}catch(IOException e){thrownewMetaException("Failed to find local user "+ e.getMessage());}}privatevoiddropSentryPrivileges(List<?extendsAuthorizable> authorizableTable)throwsSentryUserException,IOException,MetaException{String requestorUserName =UserGroupInformation.getCurrentUser().getShortUserName();try(SentryPolicyServiceClient sentryClient =SentryServiceClientFactory.create(authzConf)){
sentryClient.dropPrivileges(requestorUserName, authorizableTable);}catch(Exception e){thrownewMetaException("Failed to connect to Sentry service "+ e.getMessage());}}
dropPrivileges
是抽象方法,实现在
SentryPolicyServiceClientDefaultImpl
:
@OverridepublicvoiddropPrivileges(String requestorUserName,List<?extendsAuthorizable> authorizableObjects)throwsSentryUserException{TSentryAuthorizable tSentryAuthorizable =setupSentryAuthorizable(authorizableObjects);TDropPrivilegesRequest request =newTDropPrivilegesRequest(ThriftConstants.TSENTRY_SERVICE_VERSION_CURRENT, requestorUserName,
tSentryAuthorizable);try{TDropPrivilegesResponse response = client.drop_sentry_privilege(request);Status.throwIfNotOk(response.getStatus());}catch(TException e){thrownewSentryUserException(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
方法:
publicTDropPrivilegesResponsedrop_sentry_privilege(TDropPrivilegesRequest request)throwsorg.apache.thrift.TException{send_drop_sentry_privilege(request);returnrecv_drop_sentry_privilege();}
这个地方不用着急点进去,因为再点进去就是 Thrift 内部实现了,重点关注
SentryPolicyService
,看看它的签名:
@Generated(value ="Autogenerated by Thrift Compiler (0.9.3)", date ="2017-04-26")publicclassSentryPolicyService{...}
可以看到代码是被生成出来的,在使用 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")publicclassSentryPolicyService{publicinterfaceIface{...}publicinterfaceAsyncIface{...}publicstaticclassClientextendsorg.apache.thrift.TServiceClientimplementsIface{...}publicstaticclassAsyncClientextendsorg.apache.thrift.async.TAsyncClientimplementsAsyncIface{...}publicstaticclassProcessor<IextendsIface>extendsorg.apache.thrift.TBaseProcessor<I>implementsorg.apache.thrift.TProcessor{...}publicstaticclassAsyncProcessor<IextendsAsyncIface>extendsorg.apache.thrift.TBaseAsyncProcessor<I>{...}publicstaticclass create_sentry_role_args implementsorg.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
的接口里面找到服务侧的同名方法:
publicTDropPrivilegesResponsedrop_sentry_privilege(TDropPrivilegesRequest request)throwsorg.apache.thrift.TException;
然后很方便的跳转到其实现方法,这里即服务端的
SentryPolicyStoreProcessor
的
drop_sentry_privilege
方法:
@OverridepublicTDropPrivilegesResponsedrop_sentry_privilege(TDropPrivilegesRequest request)throwsTException{finalTimer.Context timerContext = sentryMetrics.dropPrivilegeTimer.time();TDropPrivilegesResponse response =newTDropPrivilegesResponse();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 中实际实现如下:
privatebooleandrop_table_core(finalRawStore ms,finalString dbname,finalString name,finalboolean deleteData,finalEnvironmentContext envContext,finalString indexName)throwsNoSuchObjectException,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(newPreDropTableEvent(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;thrownewMetaException(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,newDropTableEvent(tbl,true, deleteData,this),
envContext);}
success = ms.commitTransaction();}}finally{if(!success){
ms.rollbackTransaction();}elseif(deleteData &&!isExternal){// Data needs deletion. Check if trash may be skipped.// Delete the data in the partitions which have other locationsdeletePartitionData(partPaths, ifPurge);// Delete the data in the tabledeleteTableData(tblPath, ifPurge);// ok even if the data is not deleted}if(!listeners.isEmpty()){MetaStoreListenerNotifier.notifyEvent(listeners,EventType.DROP_TABLE,newDropTableEvent(tbl, success, deleteData,this),
envContext,
transactionalListenerResponses, ms);}}return success;}
其中
firePreEvent
实现如下:
privatevoidfirePreEvent(PreEventContext event)throwsMetaException{for(MetaStorePreEventListener listener : preListeners){try{
listener.onEvent(event);}catch(NoSuchObjectException e){thrownewMetaException(e.getMessage());}catch(InvalidOperationException e){thrownewMetaException(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.
*/publicstaticMap<String,String>notifyEvent(List<MetaStoreEventListener> listeners,EventType eventType,ListenerEvent event)throwsMetaException{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
*/@OverridepublicvoidonEvent(PreEventContext context)throwsMetaException,NoSuchObjectException,InvalidOperationException{if(!needsAuthorization(getUserName())){return;}switch(context.getEventType()){caseCREATE_TABLE:authorizeCreateTable((PreCreateTableEvent) context);break;caseDROP_TABLE:authorizeDropTable((PreDropTableEvent) context);break;caseALTER_TABLE:authorizeAlterTable((PreAlterTableEvent) context);break;caseADD_PARTITION:authorizeAddPartition((PreAddPartitionEvent) context);break;caseDROP_PARTITION:authorizeDropPartition((PreDropPartitionEvent) context);break;caseALTER_PARTITION:authorizeAlterPartition((PreAlterPartitionEvent) context);break;caseCREATE_DATABASE:authorizeCreateDatabase((PreCreateDatabaseEvent) context);break;caseDROP_DATABASE:authorizeDropDatabase((PreDropDatabaseEvent) context);break;caseLOAD_PARTITION_DONE:// noop for nowbreak;default:break;}}
还是以删表为例,发现所有操作其实都被封装成了校验对象,走到
MetastoreAuthzBinding
的
authorize
方法,这里用到了 Sentry 封装的一个校验模块:
sentry-provider
,其整个校验被抽象为三部分组件:
AuthorizationProvider
、
PolicyEngine
、
ProviderBackend
,调用关系大致如下
通过这种方式,将校验的通用部分抽离,差异部分实现不同实现类,主要分为 DB、搜索引擎、Kafka等类型。最后的
ProviderBackend
集成了 Sentry 的 Thrift Client,根据要校验的信息,从 Sentry Server 中拉取数据实现鉴权,如果校验不通过,则通过抛异常的方式被捕获处理后返回给用户端。至此整个鉴权的逻辑完成闭环√
四、收尾
本篇从 Sentry 收集 Hive 权限信息,到 Sentry 的鉴权插件怎么在 Hive 和 Sentry 之间实现鉴权,进行了梳理,对关键代码进行了解析,能从感性和理性的角度都对 Sentry 接管 Hive 权限有更深入的认识。
如果对在读的你有帮助,希望能给个一键三连(。•̀ᴗ-)✧
版权归原作者 猫语大数据 所有, 如有侵权,请联系我们删除。