引言
数据仓库时代,权限由数据库统一把守:用户没有账号就进不来,没有 GRANT 就查不到表。湖仓(Lakehouse)把数据落到对象存储后,这个前提被打破了——文件就在 S3/HDFS 上,只要有存储凭证,spark.read.parquet("s3://lake/db/table/") 就能把整张表读走,绕过所有表级授权。
于是湖仓权限治理要回答两个新问题:权限记在哪(表级授权如何与开放表格式协同)、凭证怎么发(用户拿到的到底是引擎代理的临时凭证,还是能直读文件的长期密钥)。本文按"三层权限模型 → 授权机制 → 细粒度控制 → 凭证与审计"的顺序展开。
一、湖仓权限的三层模型
1.1 三个层次
┌──────────────────────────────────────────────────────────┐
│ 行/列级(Fine-grained) │
│ Row Filter:只返回满足条件的行(如 tenant_id = 当前租户) │
│ Column Mask:敏感列按角色脱敏(如手机号打码) │
├──────────────────────────────────────────────────────────┤
│ 表层(Table-level) │
│ SELECT / INSERT / UPDATE / DELETE / ALTER 授权 │
│ 由 Catalog(Unity / Polaris / Lake Formation)管理 │
├──────────────────────────────────────────────────────────┤
│ 存储层(Storage-level) │
│ 对象存储 IAM:s3:GetObject 等,是最终的兜底 │
└──────────────────────────────────────────────────────────┘
三层必须协同才有意义。只配表层授权而存储层开放,等于给门上了锁但窗户大开;只配存储层前缀权限而表层不控,则无法表达"同一张表不同用户看不同列"。
1.2 为什么比数仓难
| 维度 | 传统数仓 | 湖仓 |
|---|---|---|
| 权限主体 | 数据库引擎 | 多个引擎(Spark/Trino/Flink) |
| 数据落点 | 引擎私有存储格式 | 开放文件(Parquet/ORC) |
| 权限边界 | 引擎是唯一入口 | 对象存储可被直接访问 |
| 元数据 | 引擎 Catalog | 外部 Catalog + 文件系统 |
| 细粒度 | 视图 + 权限 | 需引擎侧执行行/列策略 |
核心矛盾:开放格式带来解耦,也带走了唯一的权限关卡。解决方向是把授权重新收敛到一个统一 Catalog,并让所有引擎通过它拿凭证。
二、表级权限:Catalog 与 ACL
2.1 Iceberg 与 Delta 的授权能力
开放表格式本身不管理权限——它们管理的是元数据与快照。表级授权依赖 Catalog 层:
| 组件 | 授权能力 | 说明 |
|---|---|---|
| Hive Metastore + Ranger | 库/表/列级 ACL | 传统方案,Spark 兼容好 |
| Iceberg REST Catalog | 依赖实现 | Polaris、Tabular 等提供 |
| Unity Catalog | 表/行/列级 + 凭证下发 | Databricks 生态,跨引擎 |
| AWS Lake Formation | 表/列/行级 + 凭证下发 | 与 Glue/Athena/EMR 联动 |
| Apache Ranger | 表/列/行级 | 需引擎插件支持 |
2.2 统一 Catalog 的价值
┌─────────────────┐
│ 统一 Catalog │ ← 权限、血缘、凭证的唯一来源
└────────┬────────┘
┌───────────┼───────────┐
▼ ▼ ▼
Spark Trino Flink
│ │ │
└───────────┴───────────┘
▼
┌─────────────────┐
│ 对象存储(S3) │ ← IAM 只信任 Catalog 的服务角色
└─────────────────┘
要点是存储层只信任 Catalog 的服务身份,用户不直接持有存储凭证。这样"改权限"只需改 Catalog,存储层不必逐个用户调整——这是湖仓权限可维护性的关键。
2.3 配置示例
Lake Formation 的表级授权(把表注册到 LF 后,存储层权限由 LF 代理):
# 授予角色对某张表的 SELECT 权限(含数据位置凭证下发)
aws lakeformation grant-permissions \
--principal DataLakePrincipalIdentifier=arn:aws:iam::123456789012:role/AnalystRole \
--resource '{"Table":{"DatabaseName":"sales","Name":"fct_orders"}}' \
--permissions SELECT DESCRIBE \
--permissions-with-grant-option ""
# 关键:撤销 IAM 对该前缀的直接访问,改为只走 LF 凭证
aws lakeformation revoke-permissions \
--principal DataLakePrincipalIdentifier=arn:aws:iam::123456789012:role/AnalystRole \
--resource '{"DataLocation":{"ResourceArn":"arn:aws:s3:::lake/sales/"}}' \
--permissions DATA_LOCATION_ACCESS
Ranger 的列级策略(Hive/Spark 插件模式):
{
"service": "hive",
"name": "pii-mask-customer",
"resources": { "database": {"values": ["sales"]}, "table": {"values": ["dim_customer"]},
"column": {"values": ["phone", "id_card"]} },
"policyItems": [
{ "groups": ["pii_full_access"], "accesses": [{"type": "select", "isAllowed": true}] },
{ "groups": ["analyst"], "accesses": [{"type": "select", "isAllowed": true}],
"dataMaskInfo": {"dataMaskType": "MASK_SHOW_LAST_4"} }
]
}
注意两点:Ranger 的行过滤与掩码依赖引擎插件(Spark/Trino 需各自安装),而 Lake Formation 由凭证下发机制保证——后者更难绕过,但绑定云厂商生态。
三、细粒度权限:行过滤与列掩码
3.1 行级过滤
行过滤(Row Filter)在引擎侧改写查询,追加谓词:
-- 为多租户表建立带谓词的行过滤策略
CREATE FUNCTION tenant_filter(tenant_col STRING)
RETURNS BOOLEAN
RETURN tenant_col = current_user_tenant();
ALTER TABLE sales.fct_orders
SET ROW FILTER tenant_filter ON (tenant_id);
之后任何用户查 fct_orders,引擎都会自动加上 tenant_id = 该用户租户,无需改应用代码。关键前提是过滤在引擎侧强制执行——若用户绕过引擎直读文件,过滤失效,所以必须配合凭证下发。
3.2 列掩码
列掩码(Column Mask)按角色返回不同精度的值:
CREATE FUNCTION mask_phone(phone STRING)
RETURNS STRING
RETURN CASE
WHEN is_account_group_member('pii_full_access') THEN phone
WHEN is_account_group_member('pii_partial') THEN concat('***', right(phone, 4))
ELSE '***'
END;
ALTER TABLE sales.dim_customer
ALTER COLUMN phone SET MASK mask_phone;
| 角色 | 看到的值 |
|---|---|
pii_full_access | 13812345678 |
pii_partial | ***5678 |
| 其他 | *** |
掩码对所有查询路径生效(含 SELECT *、COUNT(DISTINCT phone)),比"建脱敏视图"更可靠——视图方案一旦用户拿到基表权限就失效。
3.3 标签驱动的访问控制
逐个表配置权限不可扩展。标签驱动(Tag-based / ABAC)的做法是给列打标签,权限策略绑定标签:
-- 1. 给敏感列打标签
ALTER TABLE sales.dim_customer ALTER COLUMN phone SET TAGS ('pii' = 'high');
ALTER TABLE sales.dim_customer ALTER COLUMN id_card SET TAGS ('pii' = 'high');
-- 2. 策略绑定标签,而非具体列
CREATE POLICY pii_mask ON SCHEMA sales
FOR SELECT USING (has_tag('pii', 'high')) APPLY mask_pii;
新增一张含 pii=high 列的表时,策略自动生效,无需人工补授权。这是从"逐表授权"走向"规模化治理"的必经一步,与数据分级体系一脉相承(见 https://plumephp.com/data-security-privacy-compliance/)。
3.4 策略的测试与灰度
权限策略是生产变更,改错了会直接导致越权或大面积查询失败。上线前应有两类验证:
-- 1. 正向验证:目标角色能且只能看到该看的数据
SET ROLE analyst_tenant_a;
SELECT count(*) FROM sales.fct_orders; -- 应只返回本租户行数
SELECT DISTINCT phone FROM sales.dim_customer LIMIT 5; -- 应为掩码后格式
-- 2. 反向验证:越权访问必须被拒绝
SET ROLE analyst_tenant_b;
SELECT * FROM sales.fct_orders WHERE tenant_id = 'tenant_a'; -- 应返回 0 行
工程上把这类断言做成自动化测试,纳入策略仓库的 CI:任何策略变更都要跑一遍"预期可见/预期不可见"用例集。灰度上先在小范围角色试点,确认无查询失败与性能退化(行过滤会改写查询,可能影响谓词下推)后再全量铺开。
四、凭证下发与代理访问
4.1 Credential Vending
凭证下发(Credential Vending)是湖仓权限的枢纽:引擎在执行查询时,向 Catalog 换取短时、限定范围的存储凭证,而不是让用户长期持有对象存储密钥。
用户 → 引擎(带身份)→ Catalog:我要读 sales.fct_orders
Catalog → 校验表级/行级/列级权限
Catalog → STS 申请临时凭证(限该表前缀、有效期 15 分钟)
Catalog → 返回凭证 + 过滤后的元数据
引擎 → 用临时凭证读文件 → 执行行过滤与列掩码 → 返回结果
三个安全属性:
| 属性 | 作用 |
|---|---|
| 短时 | 凭证 15 分钟过期,泄漏窗口小 |
| 限定范围 | 只能访问该表的存储前缀,不能越界 |
| 可审计 | 每次换取凭证都记录"谁在何时读了什么" |
4.2 直读文件为什么必须堵住
若用户能直读文件,所有表层策略形同虚设:
# 绕过引擎,直接读走整张表(含被掩码的敏感列)
aws s3 cp s3://lake/sales/dim_customer/ ./dump/ --recursive
治理措施:
- 存储桶策略拒绝直接访问,只允许 Catalog 的服务角色访问数据前缀。
- 敏感数据分桶/分前缀,高敏数据单独前缀并叠加 KMS 密钥策略。
- 网络隔离:只有引擎集群能访问存储端点,分析师的开发机走引擎代理。
- 静态加密 + 密钥分级:不同敏感级用不同 KMS Key,无密钥即使拿到文件也读不出内容。
五、跨引擎一致性
湖仓里同一张表可能被 Spark、Trino、Flink、BI 工具访问,权限必须一致:
| 风险 | 表现 | 对策 |
|---|---|---|
| 引擎绕过 Catalog | Trino 直连 Hive Metastore,不受新策略约束 | 强制所有引擎接入统一 Catalog |
| 策略实现差异 | Spark 生效、Trino 不生效 | 策略下沉到 Catalog,引擎只做执行 |
| 缓存元数据过期 | 权限变更后旧会话仍可访问 | 缩短元数据缓存 TTL,支持强制失效 |
| 绕过路径 | 用 spark.read.parquet(path) 读路径而非表 | 存储层只信任 Catalog 角色 |
原则:策略定义在 Catalog,执行在引擎,兜底在存储。三处缺一不可,任何一处漏掉都是绕过路径。
六、审计与合规
6.1 审计什么
必须记录的事件:
- 表/列级授权变更(谁、何时、给谁、什么权限)
- 数据访问(用户、表、列、行数、时间、来源 IP)
- 凭证下发(换取范围、有效期)
- 策略变更(行过滤/掩码规则的增删改)
- 敏感数据导出(下载、导出到外部系统)
审计日志本身要防篡改:写入只追加(append-only)的存储,或开启对象锁(Object Lock)与版本控制,避免事后被删改。
6.2 合规检查清单
| 检查项 | 要求 |
|---|---|
| 最小权限 | 无通配符 * 的表级授权;生产只读账号无写权限 |
| 敏感列覆盖 | 所有标记为 PII 的列必须有掩码策略 |
| 越权检测 | 定期比对实际访问与授权范围,告警异常访问 |
| 权限评审 | 每季度复核授权,清理离职与转岗账号 |
| 删除权 | 删除请求沿血缘级联执行并留痕 |
| 审计留存 | 满足法定期限(如金融 5 年) |
权限模型与数据分级、脱敏、删除权的完整合规体系,可对照 https://plumephp.com/data-security-privacy-compliance/;血缘在权限影响分析中的作用见 https://plumephp.com/data-governance-quality/。
6.3 审计数据的用法
审计日志不只是合规留档,它是权限治理的反馈回路:
-- 1. 僵尸授权:授权了但从未访问(候选回收对象)
SELECT principal, table_name, granted_at, last_access_at
FROM access_audit
WHERE last_access_at IS NULL OR last_access_at < now() - INTERVAL '180 days';
-- 2. 越权尝试:被拒绝的访问(可能是攻击,也可能是授权缺失)
SELECT principal, table_name, count(*) AS denied_cnt
FROM access_audit
WHERE result = 'DENIED' AND ts > now() - INTERVAL '7 days'
GROUP BY 1, 2 HAVING count(*) > 10 ORDER BY denied_cnt DESC;
-- 3. 敏感列访问异常:平时不碰 PII 的账号突然大量查询
SELECT principal, count(*) AS pii_queries
FROM access_audit
WHERE column_tag = 'pii' AND ts > now() - INTERVAL '1 day'
GROUP BY 1 HAVING count(*) > 1000;
三类查询分别支撑权限回收、越权告警与异常行为检测。把它们接入 SIEM 并设置告警阈值,权限治理才从"配置一次"变成"持续运营"。
七、最小权限的推进路径
不要试图一次性做到完美,按以下顺序推进:
第 1 步:收敛入口
所有引擎接入统一 Catalog,存储桶策略拒绝直接访问
第 2 步:表级授权基线
按域/团队建角色,杜绝个人直授;生产读写分离
第 3 步:敏感数据识别
完成数据分级与 PII 打标,产出敏感列清单
第 4 步:细粒度策略
对高敏表上行列级策略;用标签驱动批量覆盖
第 5 步:审计与评审
访问审计接入 SIEM,季度权限复核,越权自动告警
第 6 步:持续运营
新表默认继承标签策略;策略变更走代码评审
第 1 步是一切的前提:只要还存在绕过 Catalog 的访问路径,后续所有策略都可被规避。很多团队跳过第 1 步直接做细粒度策略,结果发现"配了没用"。
八、踩坑清单
| 坑 | 表现 | 修法 |
|---|---|---|
| 只配表级授权 | 用户直读 S3 拿全量数据 | 存储桶拒绝直读 + 凭证下发 |
| 用视图做脱敏 | 拿到基表权限即失效 | 用列掩码,引擎侧强制执行 |
| 逐表人工授权 | 新表漏配,权限漂移 | 标签驱动 + 默认继承策略 |
| 引擎绕过 Catalog | 权限不一致,审计缺失 | 强制统一入口,禁用直连 |
| 凭证长期有效 | 泄漏后长期可用 | 短时凭证 + 限定前缀 |
| 无访问审计 | 事后无法追责 | 全量访问日志 + 防篡改存储 |
| 元数据缓存过长 | 权限回收不生效 | 缩短 TTL,支持强制失效 |
| 忽略非 SQL 路径 | Notebook 直读路径绕过策略 | 统一用表 API,禁用裸路径读取 |
小结
湖仓访问控制的本质是把权限从"文件系统的附带属性"重新变成"平台的强制约束"。三层模型(存储层兜底、表层授权、行列表级执行)缺一不可,其中凭证下发是枢纽:它让引擎以用户身份、按最小范围、在限定时间内访问数据,同时留下完整审计痕迹。落地顺序上,先把所有访问入口收敛到统一 Catalog,再逐层叠加表级与细粒度策略,最后用标签驱动实现规模化治理。权限治理没有终点,它是随着数据资产增长持续运营的过程。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。