报表账号没有直接授权,为什么还能 UPDATE?一次 KingbaseES 角色继承排查
权限复核时碰到过一种很容易误判的情况:表的授权清单里找不到报表账号的 UPDATE,应用配置也明确写着“只读”,可这个账号确实能修改订单状态。
如果只查谁被 GRANT UPDATE,结论会是“权限正常”。如果直接把表上的写权限全部收走,又可能影响真正承担数据维护任务的账号。问题藏在两者之间:报表账号没有直接拿到写权限,却继承了一个拥有写权限的角色。
这类遗留通常来自临时处置。某次数据修复需要让报表账号短时间参与核对,为了省去逐张表授权,管理员把它加入维护角色;事情处理完以后,表上的临时授权清掉了,角色成员关系却没有一起回收。几个月后再看权限表,报表账号没有出现在写权限名单中,看起来干干净净,实际能力已经越过了职责边界。
下面在 KingbaseES V009R001C010 / V9R1C10 的 app_db 中还原这个现场。演示对象放在 app_schema,表名为 t_acl_order_export。所有角色均为 NOLOGIN,不会产生能够从外部登录的测试账号;管理员通过 SET ROLE 切换身份,验证它们在 KES 中的实际权限。
直接授权清单看起来没有问题
现场里有三个角色:
acl_report_user:代表应该只读的报表角色;acl_order_reader:订单查询角色,只拥有SELECT;acl_order_maintainer:订单维护角色,拥有SELECT、INSERT、UPDATE、DELETE。
管理员查询 information_schema.table_privileges,先看表上的直接授权:
SELECT grantee, privilege_type, is_grantable
FROM information_schema.table_privileges
WHERE table_schema = 'app_schema'
AND table_name = 't_acl_order_export'
ORDER BY grantee, privilege_type;
结果符合角色设计:acl_order_reader 只有 SELECT,acl_order_maintainer 有四项读写权限,而且都不能继续转授。acl_report_user 没有出现在结果中,至少可以确认它没有直接获得这张表的 UPDATE。

结果中还有一组 system 记录。因为测试表由 system 创建,它是对象所有者,拥有该表的完整控制能力,并且可以转授,所以 is_grantable 显示为 YES。这些记录不能与普通业务角色混在一起判断;对象 owner 本来就是权限体系中的特殊身份。
权限审计经常只盯着 INSERT、UPDATE、DELETE,却把 owner 当成表结构里的一个普通字段。实际上 owner 不仅影响读写,还能修改对象、继续授权,必要时甚至可以重新拿回被撤销的普通权限。业务交接、账号下线或 schema 调整时,如果只处理 GRANT/REVOKE 而没有检查对象归属,原 owner 仍可能保留超出预期的控制能力。因此权限清单里既要区分普通 grantee,也要单独列出表、schema、序列和函数的 owner。
is_grantable 也不能理解成“当前权限更大一点”这么简单。它表示该角色能否把这项权限继续授给其他角色。一个普通查询账号即使只有 SELECT,如果同时带有转授权能力,权限就可能在没有经过统一角色设计的情况下继续扩散。本次两个业务角色都是 NO,只有对象所有者显示 YES,这与演示现场的授权方式一致。
到这里如果结束盘点,就会漏掉真正的问题。information_schema.table_privileges 回答的是“权限直接授给了谁”,并不能单独回答“某个角色最终能不能执行 UPDATE”。角色成员关系、对象所有者身份以及其他继承路径,都可能改变最终结果。
没有直接授权,有效权限却是 true
KES 提供的 has_table_privilege 会按指定角色计算最终有效权限。把直接授权查询和权限函数放到一起,差异很快就出现了:
SELECT
EXISTS (
SELECT 1
FROM information_schema.table_privileges
WHERE grantee = 'acl_report_user'
AND table_schema = 'app_schema'
AND table_name = 't_acl_order_export'
AND privilege_type = 'UPDATE'
) AS direct_update_grant,
has_table_privilege(
'acl_report_user',
'app_schema.t_acl_order_export',
'SELECT'
) AS effective_select,
has_table_privilege(
'acl_report_user',
'app_schema.t_acl_order_export',
'UPDATE'
) AS effective_update;
返回值是 f、t、t:没有直接 UPDATE,但查询和更新的有效权限都成立。随后执行 \du acl_report_user,Member of 一列给出了原因,报表角色属于 acl_order_maintainer。

KES 的角色既可以代表登录身份,也可以作为一组权限的容器。将维护角色授予报表角色后,只要报表角色启用了 INHERIT,维护角色持有的对象权限就会成为它的有效权限。表的直接 ACL 没有变化,acl_report_user 仍不会出现在那张直接授权清单里,但权限检查会沿角色成员关系继续计算。
本次创建 acl_report_user 时显式使用了 INHERIT。在真实应用连接中,它不需要先执行 SET ROLE acl_order_maintainer,普通 UPDATE 就会自动使用继承到的权限。实验里的 SET ROLE acl_report_user 只是让管理员会话切换到报表角色的安全上下文,用来模拟应用请求,并不是越权成立的前提。
如果角色被设置成 NOINHERIT,排查方式也不能退回到只看表授权。成员关系仍然存在,只是权限不会自动并入当前执行身份;是否可以切换到被授予角色,还要结合角色配置继续确认。权限报告中最好同时保留角色属性和 Member of,否则同一条成员关系在不同继承设置下会表现出不同的执行结果。
这种角色设计本身没有问题。把查询、维护、审计等权限封装成 NOLOGIN 角色,再分配给应用账号,通常比逐账号、逐表授权更容易维护。风险来自成员关系与实际职责不一致:只读账号一旦被加入维护角色,它继承的就不是一个名称,而是这个角色当前以及将来拥有的全部能力。
权限函数为 true,还要用实际语句确认
权限函数已经指出异常,但权限整改不能只停在元数据判断。为了验证该角色确实可以修改表,同时不留下测试数据,管理员开启事务后切换到 acl_report_user,将订单 1 的状态暂时改为 REVIEWING:
BEGIN;
SET ROLE acl_report_user;
SELECT current_user, session_user;
UPDATE app_schema.t_acl_order_export
SET order_status = 'REVIEWING'
WHERE order_id = 1
RETURNING order_id, order_status;
ROLLBACK;
RESET ROLE;
身份查询返回 current_user = acl_report_user、session_user = system。前者表示后续 SQL 按报表角色做权限检查,后者保留最初建立会话的管理员身份。UPDATE 实际命中一行并返回 REVIEWING,证明写权限不是权限函数的误报。
事务回滚后重新查询,订单状态恢复为 PAID,实验没有污染表中数据。

这里没有使用 UPDATE ... WHERE 1 = 0。空条件同样会触发权限检查,但它不命中数据,容易把“数据库允许执行”和“只是没有改到记录”混在一起。命中一行、看到 UPDATE 1、再明确回滚,三个结果放在同一个会话里,证据要完整得多。
SET ROLE 也比创建一个带密码的测试用户更合适。演示角色保持 NOLOGIN,管理员仍能在受控会话中模拟它的权限,不需要临时放开认证配置,也不会在实验结束后遗留可登录凭据。
不动维护角色,只收回错误的成员关系
问题已经定位到 acl_report_user -> acl_order_maintainer,整改范围也随之缩小。acl_order_maintainer 仍然是合法的维护角色,其他运维账号可能依赖它;如果为了修复报表账号而撤销维护角色在表上的 UPDATE,会把正常的数据维护一起打断。
管理员只修改报表角色的成员关系:
REVOKE acl_order_maintainer FROM acl_report_user;
GRANT acl_order_reader TO acl_report_user;
再次执行 \du,Member of 已从 acl_order_maintainer 变为 acl_order_reader。随后分别检查 SELECT、INSERT、UPDATE、DELETE 的有效权限,返回结果为 t、f、f、f。

这个结果比“REVOKE ROLE 执行成功”更可靠。角色命令成功只代表成员关系发生了变化,不能自动证明账号还保留业务所需的查询权限,也不能证明三类写权限全部消失。四个权限函数把整改后的状态一次固定下来:查询保留,新增、更新和删除全部关闭。
这里采用的是角色替换,而不是简单撤销。只收回维护角色会让报表账号失去 schema 和表的访问入口;继续授予读角色,才能让权限收敛后仍符合报表业务的使用要求。最小权限不是“权限越少越好”,而是保留完成职责所必需的权限,不多给,也不少给。
整改对象的选择同样值得注意。acl_order_maintainer 是共享能力角色,可能同时服务于数据修复任务、后台管理程序和受控运维账号。直接执行 REVOKE UPDATE ON TABLE ... FROM acl_order_maintainer,会让它的所有成员一起失去更新权限,影响范围远大于当前问题。异常发生在“报表角色被加入维护角色”这条关系上,撤销这条关系才是范围最小的修复。
生产变更前还应先列出维护角色的全部成员,确认报表账号是否通过其他角色再次间接继承同一能力。只处理一条可见关系,但另一条嵌套路径仍然存在,has_table_privilege 会继续返回 true。这也是整改后必须再次检查最终有效权限的原因:成员清单用于找路径,权限函数用于确认所有路径合并后的结果。
最后一次验证必须让 UPDATE 真正失败
元数据已经收敛,最后再以 acl_report_user 身份执行实际语句。查询订单导出表时,三行数据正常返回,说明只读访问没有被整改误伤。接着更新订单 2,它原本是 PENDING,条件能够命中一行;KES 在执行前返回:
ERROR: permission denied for table t_acl_order_export

这次失败来自数据库权限检查,不是 WHERE 条件没有找到数据,也不是客户端主动拦截。随后执行 RESET ROLE,current_user 恢复为 system,会话身份也完成了收尾。
has_table_privilege 检查的是表级能力,实际查询还会经过其他权限检查。访问 app_schema.t_acl_order_export 至少需要 schema 的 USAGE 和表的 SELECT;包含序列、函数或视图的业务 SQL,还可能依赖额外对象。本次 acl_order_reader 同时持有 schema USAGE 和表 SELECT,最终的三行查询成功,证明组合权限可以支撑报表读取。单独看到 effective_select = t,还不能在所有场景里替代一次真实查询验证。
到这里,权限整改有了两类相互印证的证据:has_table_privilege 表明最终有效权限为只读,实际 SELECT 和 UPDATE 分别证明允许的操作仍能执行、越界操作已经被拒绝。只看其中一类都不够稳妥。系统视图适合批量盘点,真实语句适合确认最终执行边界。
权限盘点不能只导出一张 GRANT 清单
生产环境里的角色关系通常比这个实验复杂。应用账号可能直接持有权限,也可能通过一个或多个组角色继承;对象 owner 天然具有控制权;schema 的 USAGE、数据库的 CONNECT、序列和函数权限还分布在不同层级。只导出 table_privileges 做表格对比,最多覆盖其中一部分。
一份可落地的权限盘点至少要回答三件事:
- 对象权限直接授给了哪些角色;
- 账号通过哪些成员关系获得了间接权限;
- 目标账号对关键对象的最终有效权限是什么。
直接授权适合追溯权限来源,角色成员关系负责把来源连接到账号,has_table_privilege 则回答数据库最终会不会放行。出现异常后,再使用事务和 SET ROLE 做一次受控验证,才能排除视图口径、角色继承属性和操作对象选择带来的误判。
临时授权也不应只留一条 GRANT。至少要记录授权原因、审批人、目标角色和失效时间;维护结束时,回收成员关系应和业务验证一起完成。角色权限以后发生变化时,成员会同步继承新增能力,因此一条被遗忘的成员关系可能在创建时没有明显风险,却在后续扩权时变成越权入口。
实际工作中可以把权限复核拆成两张表:一张记录角色成员关系和角色属性,另一张记录关键对象的有效权限矩阵。前者回答权限从哪里来,后者回答账号最终能做什么。每次上线临时授权时记录变更前后值,到期后再次生成矩阵;如果 UPDATE、DELETE、TRUNCATE 等高风险列没有恢复为预期状态,变更单就不应关闭。
默认权限也要放进后续检查范围。今天创建的表已经收敛为只读,不代表明天新建的表会自动采用同样规则。schema owner 设置过宽的默认授权后,新表可能在创建时就把权限发给某个共享角色。账号再通过成员关系继承,单独检查旧表不会发现这个入口。表级整改解决当前故障,默认权限决定同类问题会不会继续出现。
这次处理最终没有修改表结构,也没有撤销维护角色本身的合法权限,只修改了一条错误的角色成员关系。直接授权清单、有效权限函数和实际语句放在一起后,“没有被直接授权,为什么还能更新”就不再矛盾:数据库检查的是账号最终拥有的权限,而不是某一张清单里有没有它的名字。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)