湖仓访问控制与权限治理

湖仓把数据放进对象存储后,权限不再由数据库统一把守,任何人都可能绕过引擎直读文件。本文拆解湖仓权限的三层模型(存储层、表层、行列表级)、Iceberg 与 Delta 的表级授权、统一 Catalog 与凭证下发机制、行过滤与列掩码、标签驱动的访问控制、跨引擎权限一致性、审计与合规落地,并给出最小权限的推进路径与踩坑清单。

引言

数据仓库时代,权限由数据库统一把守:用户没有账号就进不来,没有 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_access13812345678
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

治理措施:

  1. 存储桶策略拒绝直接访问,只允许 Catalog 的服务角色访问数据前缀。
  2. 敏感数据分桶/分前缀,高敏数据单独前缀并叠加 KMS 密钥策略。
  3. 网络隔离:只有引擎集群能访问存储端点,分析师的开发机走引擎代理。
  4. 静态加密 + 密钥分级:不同敏感级用不同 KMS Key,无密钥即使拿到文件也读不出内容。

五、跨引擎一致性

湖仓里同一张表可能被 Spark、Trino、Flink、BI 工具访问,权限必须一致:

风险表现对策
引擎绕过 CatalogTrino 直连 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,再逐层叠加表级与细粒度策略,最后用标签驱动实现规模化治理。权限治理没有终点,它是随着数据资产增长持续运营的过程。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「data-engineering」更多文章

  1. 非结构化文档 ETL 与多模态数据
  2. 流式 SQL:Flink SQL 与 ksqlDB
  3. Flink 状态后端与 Checkpoint 调优