☰
openGauss对象权限管理源码解析:从ACL建模到授权检查链路
2026/10/20 10:20:38 网站建设 项目流程

做数据库内核源码分析,很多人第一反应是先扑到代码细节里,但“对象权限管理”这个主题我建议反过来看:先理解权限在系统里到底是怎么被建模的,再看代码就会清楚很多。openGauss 的权限管理继承了经典的关系型数据库思路,用角色(OID)加 ACL(访问控制列表)来描述“谁能对哪个对象做什么”,这套模型在源码里的体现非常统一。这篇文章我从头把对象权限管理的源码链路拆一遍,会涉及数据结构、授权执行流程、运行期权限检查入口,以及一些在源码里容易被忽略的边界细节。适合正在啃 openGauss 源码的开发者,也适合排查“为什么这个用户有权限却执行不了”这类问题的 DBA 参考。

1. 先搞清楚对象权限在安全体系里的位置

1.1 权限的三层模型

openGauss 里的权限不是一个扁平的开关集合。从源码层面看,整套安全模型可以明显分成三层:

  • 角色层:对应 pg_authid 和 pg_auth_members 两张系统表,解决“你是谁、你属于哪些组”的问题。角色本身有 LOGIN、SYSADMIN、CREATEDB 等属性,这些属于系统级权限。
  • 系统权限层:控制你能不能做全局性操作,比如创建数据库、管理用户、执行某些维护命令。
  • 对象权限层:精确到某个具体对象,比如这张表能不能 SELECT、那个函数能不能 EXECUTE、这个模式里能不能 CREATE。

对象权限管理主要管的就是第三层。它要维护的内容很纯粹:角色到对象之间的操作许可。但“纯粹”不等于“简单”,因为数据库里的对象类型实在太多了——表、视图、序列、函数、存储过程、模式、类型、数据库、表空间、外部服务器,每种对象的可执行操作集合都不一样,存储权限的位置也不一样。源码层就需要对这些差异做统一抽象,而答案就是 ACL。

1.2 权限管理相关源码的分布

初读 openGauss 源码时容易犯一个错误:以为权限管理都集中在一个目录里。实际上相关代码是分散在几个模块中的,需要先建立一张源码地图。基于常见源码目录布局,我建议按下面这张表去对照:

模块文件主要职责
utils/adt/acl.cACL 数据类型实现,包含 aclitem 的输入输出、数组解析、aclinsert、aclremove、aclmask 等基础操作
commands/aclchk.c权限检查的门面,封装了各种对象类型的权限校验函数
commands/grant.cGRANT / REVOKE 命令的实现,负责对象展开、ACL 合并与系统表更新
utils/acl.h核心数据结构与权限位宏定义
utils/syscache.c系统表缓存,权限检查时大量依赖这里获取对象信息和权限列

这几个文件不是孤立的。grant.c 负责“写权限”,aclchk.c 负责“读权限”,两者都操作 ACL 数据结构,而 ACL 的底层实现都在 acl.c 和 acl.h 里。我把这条依赖链称为“权限管理的三件套”,看代码时按这个依赖方向走,顺序不会乱。

另外强调一点,执行器里还有一个 ExecCheckRTPerms 之类的统一检查入口,它不是命令处理器,而是 SQL 执行准备阶段对范围表(range table)做的批量权限校验。很多人找权限检查代码时只盯着 aclchk.c,却忽略了执行器这层,导致看漏了实际拦截动作发生的位置。

2. ACL:对象权限管理绕不开的核心数据结构

2.1 aclitem 的结构与权限位枚举

ACL 的核心结构是 AclItem,定义在 utils/acl.h 里,它记录一条权限授予关系。我摘出最主要的内容来看:

typedef struct AclItem { Oid ai_grantor; /* 授权者 OID */ Oid ai_grantee; /* 被授权者 OID */ AclMode ai_privs; /* 权限位图 */ } AclItem;

ai_privs 的类型是 AclMode,本质是 uint32 位图。每一位代表一种操作权限,权限位的定义在 acl.h 里非常直接,例如:

#define ACL_INSERT (1 << 0) #define ACL_SELECT (1 << 1) #define ACL_UPDATE (1 << 2) #define ACL_DELETE (1 << 3) #define ACL_TRUNCATE (1 << 4) #define ACL_REFERENCES (1 << 5) #define ACL_TRIGGER (1 << 6) #define ACL_EXECUTE (1 << 7) #define ACL_USAGE (1 << 8) #define ACL_CREATE (1 << 9) #define ACL_CONNECT (1 << 10) #define ACL_ALTER (1 << 11) #define ACL_DROP (1 << 12) #define ACL_COMMENT (1 << 13)

这里有个值得注意的点:openGauss 的权限位不止包含传统 DML 操作,还把 ALTER、DROP、COMMENT 这类结构级操作也纳入了 ACL 体系。这意味着如果你做细粒度访问控制,可以单独限制某个角色只能查询某张表但不能改表结构,这在安全合规场景下非常有用。也是“对象权限管理”这个标题下容易被低估的部分——对象权限不仅要管数据访问,还要管对象本身的结构变更操作。

2.2 权限位图的文本表示:aclitemin / aclitemout

位图在内存里是数字,但 DBA 在 psql 里看到的是一串文本,比如sam=arwdDxt/postgres。这个转换由 aclitemin/aclitemout 负责。aclitemout 把权限位翻译成权限缩写字母,常用映射大约如下:

权限位文本字母含义
ACL_SELECTr读取
ACL_INSERTa追加
ACL_UPDATEw修改
ACL_DELETEd删除
ACL_TRUNCATED清空
ACL_REFERENCESx引用外键
ACL_TRIGGERt触发器
ACL_EXECUTEX执行
ACL_USAGEU使用
ACL_CREATEC创建
ACL_CONNECTc连接
ACL_ALTERA修改结构
ACL_DROPD删除对象
ACL_COMMENTO注释

注意,同一个字母在不同对象上语义可能不同,比如 D 在表上代表 TRUNCATE,在别的场景里可能代表 DROP。原因在于权限枚举是全局统一的,而具体含义依赖对象类型。阅读 aclitemout 的源码时,你会发现它通过一个权限字符映射表加循环逐位拼出文本,逻辑并不复杂。

2.3 ACL 在系统表中的存储形态

ACL 数组在系统表里以 aclitem[] 类型存储,常见的位置包括:

  • pg_class.relacl —— 表、视图、序列的对象权限
  • pg_proc.proacl —— 函数、存储过程的对象权限
  • pg_namespace.nspacl —— 模式对象权限
  • pg_type.typacl —— 类型对象权限
  • pg_database.datacl —— 数据库对象权限
  • pg_tablespace.spcacl —— 表空间对象权限

当对象没有被显式授权时,这些字段通常是 NULL。NULL 的语义是“未设置,走默认规则”,也就是所有权限归对象 owner,且非 owner 没有额外权限。要注意它和空数组{}并不等价,空数组在某些代码路径里会被当作“已经显式清空权限的列表”,起点不同可能导致后续合并行为不同。这个细节后续专门讲。

在内存中,ACL 被包装成与 varlena 兼容的 Acl 结构,简化定义如下:

typedef struct Acl { int32 ndim; /* 数组维度 */ int32 dataoffset; /* 数据偏移量 */ AclItem aclitems[FLEXIBLE_ARRAY_MEMBER]; } Acl;

系统缓存中保存的也是这个结构,权限检查不需要频繁重新解析磁盘上的数组文本,而是直接读取缓存中的二进制结构,性能上高效很多。

3. 授权链路拆解:GRANT 语句是如何一步步生效的

3.1 SQL 入口:ExecuteGrantStmt 的调用背景

一条GRANT SELECT ON t_user TO role_app从前端进来后,会经过词法/语法解析,生成 GrantStmt 节点。GrantStmt 里包含三个关键信息:对象列表(表、函数、模式等)、权限列表(SELECT、INSERT 或带列名的列权限)、被授权者列表(角色名或 PUBLIC)。这个节点最终会进入 ExecuteGrantStmt 处理。

把授权逻辑单独放在 grant.c 而不是塞进通用 DDL 处理器,是有工程考虑的。因为 GRANT 涉及的对象类型分支太多,每种对象的权限集合、存储字段和系统表更新方式都不一样,混杂在一起会让代码非常难维护。ExecuteGrantStmt 一进来会先做几个前置动作:校验权限位是否合法、把角色名翻译成 OID、检查被授权角色是否存在。这些看起来琐碎,却是后续一切更新的前提。

3.2 授权目标的展开与权限集合的组装

对表授权时,一次GRANT SELECT ON table1, table2 TO x很常见。源码会把对象列表展开,逐个获取对象 OID,再根据对象类型走到不同的展开函数。例如表对象会走 expandTableOrViewOrSequence,函数会走 expandFunctions,模式走 expandNamespaces。

展开过程中还有一个重要分支:列权限。如果 GRANT 后面跟了(col1, col2),那么列级权限要单独记录,表级权限存到 pg_class.relacl,列级权限存到 pg_attribute.attacl。两者互相独立,检查时也需要分别判断。这就是为什么“给表授了权,但查某些列仍然报权限不足”这种问题会成为现实——列权限和表权限并不是简单的包含关系。

权限集合的组装也不只是按位或。源码要判断被授予的权限是否带 GRANT OPTION,即被授权者能不能再转授。GRANT OPTION 在内存中通过高位标记实现,代码里有 ACLITEM_SET_GOPTIONS 之类的宏专门负责打标。把授权选项放在同一变量的高位,而不是单独拆一个字段,是为了保持 AclItem 结构的紧凑性,也让序列化更简单。

3.3 ACL 合并、追加与系统表更新

拿到目标对象 OID 和权限集合后,核心逻辑进入合并阶段。这个过程可以用四步概括:

  1. 从系统表读取对象当前的 ACL,如果为 NULL,按“owner 拥有全部权限”的语义构造基础列表。
  2. 遍历被授权者:如果 ACL 中已有该角色的条目,就把新权限按位或进去;如果没有,则追加一个 AclItem。
  3. 如果新权限带 GRANT OPTION,还要校验当前授权者自身是否持有对应权限的授权选项,否则报权限不足。
  4. 合并完成后,调用系统表更新接口写回新的 ACL 数组。

写回系统表并不是普通 UPDATE 那么简单,代码还会触发系统缓存失效,让后续权限检查能看到新权限。这里给一个排查提示:如果你用 prepared statement 或者某个连接里缓存了执行计划,权限修改后旧计划可能还会短暂生效,这属于计划缓存机制的设计选择,不是权限管理本身的缺陷。

3.4 REVOKE 的执行逻辑和“对称”背后的不对称

REVOKE 表面上是 GRANT 的逆操作,但源码实现并不完全对称。一个重要的区别是,REVOKE 要区分两种语义:回收权限本身,还是只回收 GRANT OPTION 但保留使用权限。这两个分支在源码里是分开处理的。

在 removeAclItem 这类函数里,回收会先找到匹配的 aclitem,按位去掉对应权限。如果某个条目上所有权限位都被清空,代码会删除整个条目,而不会留下一个权限位全 0 的空条目。这样做的道理很朴素:ACL 数组越精简,检查路径越快,也越不容易出现“看起来有条目但其实没有权限”的中间态。

此外,REVOKE 还要考虑权限叠加来源。假设某个角色同时从 PUBLIC 和另一个父角色继承到 SELECT,那么只 REVOKE 它本身的 SELECT 并不能真正收回权限,因为另外两条来源还在。这也是权限排查里最常见的问题,读源码时一定要把“权限来源”当作和“权限是否存在”同等重要的维度。

4. 执行期权限检查:每个 DML 是怎么被“卡”住的

4.1 从 object_aclcheck 走到 pg_class_aclmask

授权只是第一步,真正决定 SQL 能不能执行的是检查逻辑。openGauss 中比较底层的入口是 object_aclcheck,它根据对象类型分流:对象属于 pg_class 就检查表权限,属于 pg_proc 就检查函数权限,属于 pg_namespace 就检查模式权限。实际日常遇到最多的还是pg_class_aclcheck(relOid, roleid, ACL_SELECT)这类封装。

封装之下是 pg_class_aclmask。这个函数的名字很容易让人误解,它不是“直接返回某个对象的 ACL”,而是“返回指定角色在给定权限掩码约束下实际持有的权限位”。入参里有一个 mask 和 how,mask 表示调用方关心哪几位权限,how 表示是“必须全部拥有”还是“至少拥有其中任何一个”。这个设计非常实用:检查 SELECT 时 mask 只带一个 ACL_SELECT,检查“能否访问这张表”时 mask 可以带多个位,再用 ACLMASK_ANY 判断任一即可。

4.2 超级用户与 owner 的短路设计

去看 pg_class_aclmask 的源码,会发现开头有几个提前返回的分支。第一个分支是超级用户判断:如果是系统管理员角色,直接返回 mask 全集,也就是“不做权限限制”。第二个分支是 owner 判断:如果角色是对象的 owner,也直接返回 mask 全集。

这个 short-circuit 设计非常关键。owner 不需要在 relacl 里显式列出来,这是数据库权限模型的默示语义。你创建一张表后用SELECT relacl FROM pg_class看到 NULL,却依然能对表做任何操作,就是因为 owner 短路逻辑在工作。从性能角度看,高频访问者往往就是对象所有者,如果每次操作都遍历 ACL 数组,成本没有必要。把 owner 和超管在前置位过滤掉,正常路径几乎零开销。

读源码时习惯性问一句“为什么这里要先 return”,往往能发现这些工程上的取舍。

4.3 aclmask 的核心矛盾:位与运算与角色扩展

当对象不是 owner、用户也不是超管时,才进入完整的 ACL 位图计算。简化后的核心逻辑思路大致是这样:

static inline AclMode aclmask(const Acl *acl, Oid roleid, Oid ownerId, AclMode mask, AclMaskHow how) { AclMode result = 0; /* 遍历 ACL 数组,先处理角色直接对应的条目 */ for (i = 0; i < acl->ndim; i++) { if (acl->aclitems[i].ai_grantee == roleid) result |= ACLITEM_GET_PRIVS(&acl->aclitems[i]); } /* 再处理 PUBLIC 条目 */ for (i = 0; i < acl->ndim; i++) { if (acl->aclitems[i].ai_grantee == ACL_ID_PUBLIC) result |= ACLITEM_GET_PRIVS(&acl->aclitems[i]); } /* 最后递归合并角色继承链上的权限 */ ... return result & mask; }

这里有几个必须记住的细节。第一,ACL 里的 grantee 是角色 OID 而不是用户名,所以角色继承关系会直接影响检查结果。第二,PUBLIC 是个特殊 grantee,所有角色都隐式拥有它对应的权限,排查问题时要格外留意,很多“为什么所有人都有权限”的疑问都源于 PUBLIC。第三,每一步取的是 ACLITEM_GET_PRIVS 后的值,也就是说已经去掉了 GRANT OPTION 标记,避免把“能转授权限”误当成“能直接使用权限”。

角色扩展是 aclmask 里较绕的部分。openGauss 会通过 pg_auth_members 找到当前角色的父角色,递归聚合父角色的权限。为了避免同一 SQL 里反复遍历角色树,代码会利用成员关系缓存优化。对用户来说,这个机制解释了一个常见现象:给父角色授权后,继承它的子角色通常立刻就能使用该权限,不需要额外授权。

4.4 执行计划生成阶段的统一检查

权限检查并不只在算子内部发生。当你执行一条多表 JOIN 的 SELECT 时,执行器在打开表之前,会先通过 ExecCheckRTPerms 一类入口对范围表(range table)逐一检查。这个函数遍历 SQL 中出现的所有关系对象,对每张表调用权限检查,如果权限不足直接抛错。

这也是为什么 openGauss 里“对表执行 SELECT”会在很早的阶段就被拦截,而不是等到扫描算子真正去读页面才发现。早期拦截的好处很明显:避免底层存储已经打开文件、读取页面后才拒绝,既降低无效 IO,也降低信息泄漏风险。权限检查路径概括起来就是:

SQL -> ExecCheckRTPerms -> pg_class_aclcheck -> pg_class_aclmask -> aclmask

理解这条主线之后,排查权限问题就有方向了。

5. 源码分析中容易踩到的几个细节坑

5.1 空 ACL 与 NULL ACL 语义完全不同

这个坑在源码阅读和实际运维中都很常见。context 里看到pg_class.relacl为空,很多人第一反应是“这张表没有设置任何权限”,接着就不知道该怎么判断了。正确的理解是:relacl 为 NULL 时,访问权限完全由 owner 机制决定,表对所有非 owner 没有显式权限;但如果存在一条对 PUBLIC 的授权,则所有角色都有对应权限。而 relacl 为{}时,代码通常会把它当作显式空 ACL,合并和检查的起点与 NULL 不同。源码里许多函数开头的判断都是先看是否为 NULL,再决定要不要走 owner 短路或默认权限逻辑。

5.2 列级权限藏在 pg_attribute.attacl 里

对象权限一个容易被忽略的隐藏点是列权限。GRANT SELECT(col1) ON t TO u修改的不是 pg_class.relacl,而是 pg_attribute.attacl 中对应列的权限数组。查询时,如果用户只拥有列级权限而没有表级 SELECT,那么 SQL 中只能访问这些列,访问其他列会被拒绝。attacl 的解析逻辑和 relacl 基本一致,但目标对象从表变成了属性。

调试时如果发现“明明有表级权限,查某些列还是报权限不足”,可以先查 pg_attribute 里的 attacl 列,看看列上是否叠加了更严格的 ACL。列权限和表权限在某些情况下会做合并,但一旦列上出现显式 ACL,并不会自动放宽到表级全权限。

5.3 权限修改与缓存失效的联动

更新 ACL 后,源码会做系统表行更新并触发缓存失效。但你观察线上环境时,可能发现同一连接里的后续语句偶尔仍拿着旧权限。这通常不是权限更新逻辑的问题,而是计划缓存或 prepared statement 导致。对于源码分析,要记住一条规则:ACL 变更依赖系统缓存失效机制传播,而失效是按对象 OID 触发的。如果某个对象的 OID 缓存未清除,权限就可能出现“滞后”。排查这类问题,先确认缓存是否正常失效,再怀疑权限标志本身。

5.4 调试 ACL 代码的常用手段

最后分享几个我实际用起来比较顺手的调试方法。

第一,用系统提供的 ACL 拆分函数把数组展开成多行,逐条看 grantee、权限位和 grantor,对照源码位图非常直观。第二,在 aclmask 入口临时加日志,打印入参的 roleid、mask、对象 OID,能快速定位“检查到底卡在哪一层”。第三,如果是角色继承链导致的权限问题,直接查 pg_auth_members 把角色祖先关系列出来,通常立刻就能找到来源。

这三个手段配合起来,大多数权限问题可以快速定位到具体函数。我自己在分析权限管理这类基础安全代码时最大的体会是,不要急着追求读遍每一个分支,先把“数据结构、授权过程、检查过程”这条主线走通,再回头补细节,效率会高很多。希望这篇文章能帮你少走一些弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询