FlinkSQL的行级权限解决方案及源码,支持面向用户级别的行级数据访问控制,即特定用户只能访问授权过的行,隐藏未授权的行数据。此方案是实时领域Flink的解决方案,类似离线数仓Hive中Ranger Row-level Filter方案。
源码地址: https://github.com/HamaWhiteGG/flink-sql-security
注: 此方案已产品化集成到实时计算平台Dinky,欢迎试用。
一、基础知识
1.1 行级权限
行级权限即横向数据安全保护,可以解决不同人员只允许访问不同数据行的问题。例如针对订单表,用户A只能查看到北京区域的数据,用户B只能查看到杭州区域的数据。

1.2 业务流程
1.2.1 设置行级权限
管理员配置用户、表、行级权限条件,例如下面的配置。
| 序号 | 用户名 | 表名 | 行级权限条件 |
|---|---|---|---|
| 1 | 用户A | orders | region = ‘beijing’ |
| 2 | 用户B | orders | region = ‘hangzhou’ |
1.2.2 用户查询数据
用户在系统上查询orders表的数据时,系统在底层查询时会根据该用户的行级权限条件来自动过滤数据,即让行级权限生效。
当用户A和用户B在执行下面相同的SQL时,会查看到不同的结果数据。
SELECT * FROM orders;
用户A查看到的结果数据是:
| order_id | order_date | customer_name | price | product_id | order_status | region |
|---|---|---|---|---|---|---|
| 10001 | 2020-07-30 10:08:22 | Jack | 50.50 | 102 | false | beijing |
| 10002 | 2020-07-30 10:11:09 | Sally | 15.00 | 105 | false | beijing |
注: 系统底层最终执行的SQL是:
SELECT * FROM orders WHERE region = 'beijing'。
用户B查看到的结果数据是:
| order_id | order_date | customer_name | price | product_id | order_status | region |
|---|---|---|---|---|---|---|
| 10003 | 2020-07-30 12:00:30 | Edward | 25.25 | 106 | false | hangzhou |
| 10004 | 2022-12-15 12:11:09 | John | 78.00 | 103 | false | hangzhou |
注: 系统底层最终执行的SQL是:
SELECT * FROM orders WHERE region = 'hangzhou'。
1.3 组件版本
| 组件名称 | 版本 | 备注 |
|---|---|---|
| Flink | 1.16.0 | |
| Flink-connector-mysql-cdc | 2.3.0 |
二、Hive行级权限解决方案
在离线数仓工具Hive领域,由于发展多年已有Ranger来支持表数据的行级权限控制,详见参考文献[2]。下图是在Ranger里配置Hive表行级过滤条件的页面,供参考。

但由于Flink实时数仓领域发展相对较短,Ranger还不支持FlinkSQL,以及要依赖Ranger会导致系统部署和运维过重,因此开始自研实时数仓的行级权限解决工具。
三、FlinkSQL行级权限解决方案
3.1 解决方案
3.1.1 FlinkSQL执行流程
可以参考作者文章[FlinkSQL字段血缘解决方案及源码],本文根据Flink1.16修正和简化后的执行流程如下图所示。

在CalciteParser.parse()处理后会得到一个SqlNode类型的抽象语法树(Abstract Syntax Tree,简称AST),本文会在Parse阶段,通过组装行级过滤条件生成新的AST来实现行级权限控制。
3.1.2 Calcite对象继承关系
下面章节要用到Calcite中的SqlNode、SqlCall、SqlIdentifier、SqlJoin、SqlBasicCall和SqlSelect等类,此处进行简单介绍以及展示它们间继承关系,以便读者阅读本文源码。
| 序号 | 类 | 介绍 |
|---|---|---|
| 1 | SqlNode | A SqlNode is a SQL parse tree. |
| 2 | SqlCall | A SqlCall is a call to an SqlOperator operator. |
| 3 | SqlIdentifier | A SqlIdentifier is an identifier, possibly compound. |
| 4 | SqlJoin | Parse tree node representing a JOIN clause. |
| 5 | SqlBasicCall | Implementation of SqlCall that keeps its operands in an array. |
| 6 | SqlSelect | A SqlSelect is a node of a parse tree which represents a select statement. |

3.1.3 解决思路
在Parser阶段,如果执行的SQL包含对表的查询操作,则一定会构建Calcite SqlSelect对象。因此限制表的行级权限,只要在构建Calcite SqlSelect对象时对Where条件进行拦截即可,而不需要解析用户执行的各种SQL来查找配置过行级权限条件约束的表。
在SqlSelect对象构造Where条件时,要通过执行用户和表名来查找配置的行级权限条件,系统会把此条件用CalciteParser提供的parseExpression(String sqlExpression)方法解析生成一个SqlBacicCall再返回。然后结合用户执行的SQL和配置的行级权限条件重新组装Where条件,即生成新的带行级过滤条件Abstract Syntax Tree,最后基于新的AST再执行后续的Validate、Convert、Optimize和Execute阶段。

以上整个过程对执行SQL的用户都是透明和无感知的,还是调用Flink自带的TableEnvironment.executeSql(String statement)方法即可。
注: 要通过技术手段把执行用户传递到Calcite SqlSelect中。
3.2 重写SQL
主要在org.apache.calcite.sql.SqlSelect的构造方法中完成。
3.2.1 主要流程
主流程如下图所示,根据From的类型进行不同的操作,例如针对SqlJoin类型,要分别遍历其left和right节点,而且要支持递归操作以便支持三张表及以上JOIN;针对SqlIdentifier类型,要额外判断下是否来自JOIN,如果是的话且JOIN时且未定义表别名,则用表名作为别名;针对SqlBasicCall类型,如果来自于子查询,说明已在子查询中组装过行级权限条件,则直接返回当前Where即可,否则分别取出表名和别名。
然后再获取行级权限条件解析后生成SqlBacicCall类型的Permissions,并给Permissions增加别名,最后把已有Where和Permissions进行组装生成新的Where,来作为SqlSelect对象的Where约束。

上述流程图的各个分支,都会在下面的用例测试章节中会举例说明。
3.2.2 核心源码
核心源码位于SqlSelect中新增的addCondition()、addPermission()、buildWhereClause()三个方法,下面只给出控制主流程addCondition()的源码。
/**
* The main process of controlling row-level permissions
*/
private SqlNode addCondition(SqlNode from, SqlNode where, boolean fromJoin) {
if (from instanceof SqlIdentifier) {
String tableName = from.toString();
// the table name is used as an alias for join
String tableAlias = fromJoin ? tableName : null;
return addPermission(where, tableName, tableAlias);
} else if (from instanceof SqlJoin) {
SqlJoin sqlJoin = (SqlJoin) from;
// support recursive processing, such as join for three tables, process left sqlNode
where = addCondition(sqlJoin.getLeft(), where, true);
// process right sqlNode
return addCondition(sqlJoin.getRight(), where, true

文章介绍了FlinkSQL在实时数仓场景下的行级权限解决方案,通过源码分析展示了如何在查询时动态添加行级过滤条件,以实现用户级别的数据访问控制。此方案已集成到Dinky平台,支持类似Hive的RangerRow-levelFilter功能,但无需依赖Ranger,降低了系统部署和运维复杂性。
最低0.47元/天 解锁文章
9762





