Skip to content

角色继承(INHERIT / NOINHERIT) ​

上一页埋了一个伏笔:成员角色获得组角色的权限有两条路——自动继承(INHERIT)与主动切换(SET ROLE)。这一页专门把这两条路的语义、边界与组合讲清楚。继承是角色体系里最容易被误解的部分:「我是组角色的成员,为什么用不了它的权限?」「为什么 SET ROLE 之后反而少了东西?」「SUPERUSER 能不能通过加入超级用户组获得?」——答案都在这里。

从 PostgreSQL 16 开始,继承的管理方式发生了一次重要演进:INHERIT 从一个纯粹的角色级属性,变成了每条成员关系各自携带的选项(inherit_option / set_option / admin_option)。这既是本页的重点,也是理解 PG16 之后 GRANT 语法变化的前提。

前置知识

建议先阅读:GRANT / REVOKE 权限管理

基础概念 ​

两条使用权限的路径 ​

设 app_readwrite 组角色拥有对 orders 表的写权限,alice 是它的成员。alice 什么时候能写 orders?取决于成员关系的 INHERIT 与 SET 选项:

路径条件行为
继承(USAGE)成员关系带 INHERIT TRUE组角色的对象权限自动可用,语句里直接写 INSERT INTO orders ... 就能通过权限检查
切换(SET)成员关系带 SET TRUE会话执行 SET ROLE app_readwrite 后「变成」组角色,使用它的权限;RESET ROLE 回到登录角色

两条路径可以同时开启(默认就是如此),也可以只开其中一条。只开 INHERIT 关掉 SET:成员自动用权限,但永远不能切换身份;只开 SET 关掉 INHERIT:成员平时无权,必须显式 SET ROLE 才能使用组权限——这正是「职责分离」场景的基石。

把两条路径在运维视角下的差异摊开对比:

维度继承(INHERIT TRUE)切换(SET TRUE,通常配 INHERIT FALSE)
权限检查身份current_user 仍是登录角色,检查时并入组权限SET ROLE 之后 current_user 变为组角色
使用前提无动作,语句直接通过必须显式执行 SET ROLE
观测性与普通授权无异,难以区分「谁用了组权限」SET ROLE 动作可被日志、pg_stat_activity 捕捉
新建对象属主登录角色本人组角色(除非切回来)
组上的 ALTER ROLE ... SET 参数不生效同样不生效(SET ROLE 不应用角色级参数)
回收权限后的影响下一次语句即失败已 SET ROLE 的会话在下次权限检查时失败

最后两行值得展开:组角色身上的会话默认参数(如 ALTER ROLE app_readwrite SET statement_timeout = '5s')只在以该角色直接登录时应用——官方文档明确:SET ROLE 不会处理目标角色的 ALTER ROLE 设置,这只发生在登录时。因此想给一组用户统一注入会话参数,把参数挂在组角色上、再让大家 SET ROLE 过去是无效配置;可行的做法是把参数直接设在每个成员的登录角色上,或者让应用连接池直接以组角色账号建立连接(此时角色级参数随登录生效)。这也是「人 = 登录角色、组 = 权限容器」模型下最容易踩的配置盲区。

成员关系三选项(PG16 起) ​

PG16 起,每条成员关系携带三个独立开关,存放在 pg_auth_members 中:

选项含义新建成员关系时的默认值
ADMIN成员可以把该角色的成员关系授予/回收给他人FALSE(WITH ADMIN OPTION 等价 TRUE)
INHERIT成员是否自动继承该角色的权限跟随成员角色自身的 INHERIT/NOINHERIT 属性
SET成员能否 SET ROLE 切换到该角色TRUE

在 PG16 之前,INHERIT 是角色级属性:它一次性控制该角色所有成员关系的运行时检查。PG16 起改为逐条成员关系控制,角色级 INHERIT 属性退化为「新成员关系的默认值」。老版本升级到 16+ 后,既有成员关系会按旧语义自动换算,无需手工迁移,但新写的授权脚本应当显式指定选项以避免歧义。

SQL 标准视角 ​

官方文档特别指出:SQL 标准里「用户不自动继承、角色自动继承」。PostgreSQL 默认给所有角色 INHERIT 是历史兼容性选择。想贴近标准语义,可以约定「人 = NOINHERIT 角色,组 = INHERIT 角色」——不少严格安全团队采用这种风格。

语法与用法 ​

角色级属性:INHERIT / NOINHERIT ​

sql
-- 创建时声明(默认 INHERIT)
CREATE ROLE alice LOGIN INHERIT;
CREATE ROLE bob   LOGIN NOINHERIT;

-- 随时修改;只影响之后新建的成员关系默认值,不改变既有成员关系
ALTER ROLE bob INHERIT;
ALTER ROLE bob NOINHERIT;

PG16 前后的语义差异

PG15 及更早:ALTER ROLE ... INHERIT/NOINHERIT 立即改变该角色全部成员关系的运行时行为。PG16 起:它只决定「以后以 GRANT 把该角色加进组时的默认 inherit_option」,既有的每条成员关系要用 GRANT ... WITH INHERIT TRUE/FALSE 重设。

成员关系级:GRANT 的三个选项 ​

sql
GRANT group_role TO member_role
    [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ];

-- 典型组合
GRANT app_readwrite TO bob;                             -- 全默认:可继承、可切换、不可转授
GRANT app_readwrite TO carol WITH INHERIT FALSE, SET TRUE;   -- 只能 SET ROLE 使用
GRANT app_admin     TO dave WITH ADMIN TRUE;            -- 可转授成员关系
GRANT app_readonly  TO eve  WITH SET FALSE;             -- 只继承、不可切换

修改既有成员关系的选项:对同一条关系重新执行 GRANT 并写上新值,未提及的选项保持原样。回收成员关系仍是 REVOKE group FROM member;只回收管理权用 REVOKE ADMIN OPTION FOR group FROM member。

SET ROLE / RESET ROLE ​

sql
SET [ SESSION | LOCAL ] ROLE role_name;
RESET ROLE;        -- 等价于 SET ROLE NONE,回到 session_user

详细说明 ​

INHERIT 语义:成员权限自动可用 ​

成员关系带 INHERIT TRUE 时,组角色的对象权限直接并入成员的有效权限,无需任何动作。这条规则有两个精确的边界:

  • 只对对象权限成立。组角色身上的角色属性(SUPERUSER、CREATEDB、CREATEROLE、REPLICATION、BYPASSRLS、LOGIN)不通过继承传播,详见下文。
  • 逐跳生效。多级嵌套时,权限沿成员关系链传递,但任何一跳缺 INHERIT,链条就在那里断掉,更上层的权限不再自动可用。

NOINHERIT:权限要用 SET ROLE 拿 ​

成员关系带 INHERIT FALSE(且 SET TRUE)时,成员平时完全用不了组权限,必须显式切换:

sql
SET ROLE app_readwrite;   -- 此刻起,会话以 app_readwrite 的身份做权限检查
INSERT INTO orders ...;   -- 组权限生效
RESET ROLE;               -- 回到登录角色,组权限消失

这个模式的价值在于默认安全:应用连接、批处理脚本平时跑在「几乎无权」的登录角色上,只有在明确需要执行某类操作时才切换。一旦被注入,攻击者拿到的会话也拿不到组权限,除非注入的 SQL 里恰好包含正确的 SET ROLE。

继承链会中途「断」:一个完整的例子 ​

官方文档用下面这个例子精确演示了逐跳判定,这里完整复现(名字与注释本地化):

sql
CREATE ROLE joe    LOGIN;      -- 普通用户
CREATE ROLE admin  NOLOGIN;    -- 一级组
CREATE ROLE wheel  NOLOGIN;    -- 二级组(高权)
CREATE ROLE island NOLOGIN;    -- 独立岛(只继承、不可切换)

GRANT admin  TO joe   WITH INHERIT TRUE;
GRANT wheel  TO admin WITH INHERIT FALSE;          -- 链条在 admin → wheel 断开
GRANT island TO joe   WITH INHERIT TRUE, SET FALSE;

连接为 joe 之后,权限状态如下:

  • 立即可用:joe 自己的权限 + admin 的权限 + island 的权限(两条成员关系都可继承)。
  • 不可用:wheel 的权限——joe 虽然经由 admin 间接成为 wheel 的成员,但中间那一跳(admin → wheel)是 INHERIT FALSE,继承链在此中断。
  • SET ROLE admin; 之后:会话只拥有 admin 的权限,不再拥有 joe 自己的与 island 的权限——SET ROLE 是「替换身份」而不是「叠加身份」。
  • SET ROLE wheel; 之后:只拥有 wheel 的权限。注意这里不需要先 SET ROLE admin:只要从登录角色到目标角色的成员关系链上每一跳都是 SET TRUE,就可以一步直达。
  • 永远不能 SET ROLE island;:这条成员关系 SET FALSE,joe 只能通过继承使用 island 的权限。
  • 恢复原状:SET ROLE joe; / SET ROLE NONE; / RESET ROLE; 三者等价。

「SET ROLE 一步直达」的两面性

直达规则让运维很方便,但也意味着:只要登录角色到高危角色的链路全是 SET TRUE,任何获得该会话控制权的人都能一步切到高危角色。对高危组角色,考虑在链路的某一跳显式 SET FALSE,强制经由受控入口(比如只开放给运维工具的专用登录角色)切换。

SET ROLE 与 SESSION AUTHORIZATION 的区别 ​

两者都会改变 current_user(权限检查依据),但层次完全不同:

维度SET ROLESET SESSION AUTHORIZATION
改变 current_user是是
改变 session_user否是(连同最初会话身份一起替换)
前置条件必须是目标角色的成员(且 SET 选项链为真)必须是超级用户,无需任何成员关系
可逆性RESET ROLE 回到 session_userRESET SESSION AUTHORIZATION 回到最初身份
典型用途使用组成员身份、临时提权运维场景下冒充任意用户验证权限、恢复流程

用一个对照实验说明:

sql
-- 以 alice 登录
SELECT session_user, current_user;   -- alice | alice
SET ROLE app_readwrite;
SELECT session_user, current_user;   -- alice | app_readwrite
RESET ROLE;

-- 超级用户会话里:
SET SESSION AUTHORIZATION alice;
SELECT session_user, current_user;   -- alice | alice   (两者都变了)

另一个会改变 current_user 的场景是 SECURITY DEFINER 函数的执行期——它以函数属主身份运行,属于函数安全主题,这里只需知道它也走「替换 current_user」的机制。

一次完整的身份旅程:会话演示 ​

把本页的关键机制串成一段可直接执行的会话记录,跑一遍胜过读十遍:

sql
-- 以 alice(NOINHERIT,app_readwrite 的成员且 SET TRUE)登录
SELECT session_user, current_user, current_role;
--  session_user | current_user | current_role
-- --------------+--------------+--------------
--  alice        | alice        | alice
SHOW is_superuser;
--  is_superuser
-- --------------
--  off
SELECT pg_has_role('app_readwrite', 'MEMBER');   -- t:成员身份
SELECT pg_has_role('app_readwrite', 'USAGE');    -- f:NOINHERIT,权限未并入

SET LOCAL ROLE app_readwrite;        -- 事务级切换(本事务结束后自动还原)
SELECT session_user, current_user;
--  session_user | current_user
-- --------------+--------------
--  alice        | app_readwrite
INSERT INTO app.orders(id) VALUES (1);   -- 组权限生效
COMMIT;                                  -- 事务结束,身份自动还原为 alice

SET ROLE app_readwrite;              -- 会话级切换
CREATE TABLE app.tmp_notes(t text);  -- 新对象属主是 app_readwrite,不是 alice
RESET ROLE;                          -- 显式还原

三个观察点:session_user 在整个会话里始终是 alice(这是 SET ROLE 与 SET SESSION AUTHORIZATION 的分水岭);SET LOCAL ROLE 让「切换 → 干活 → 还原」原子化地绑定在事务上;切换期间建的对象归组角色——想「对象归组」就别急着 RESET,想避免就要在切换前规划好属主。

排错手册:「应该有的权限却用不了」 ​

按命中率从高到低逐层排查,每层给出对应的确认手段:

sql
-- 第 1 步:确认现在到底是谁(几乎一半的问题出在身份误判)
SELECT session_user, current_user;

-- 第 2 步:成员关系存在吗?三态分别是什么?
SELECT pg_has_role(current_user, 'app_readwrite', 'MEMBER');  -- f → 根本不是成员
SELECT pg_has_role(current_user, 'app_readwrite', 'USAGE');   -- f → 是成员但没继承
SELECT pg_has_role(current_user, 'app_readwrite', 'SET');     -- f → 是成员但切换不了

-- 第 3 步:沿成员关系链逐跳找断点(多级组必查)
WITH RECURSIVE chain AS (
  SELECT roleid, member, grantor, inherit_option, set_option, 1 AS depth
  FROM pg_auth_members
  WHERE member = current_user::regrole::oid
  UNION ALL
  SELECT am.roleid, am.member, am.grantor, am.inherit_option, am.set_option, c.depth + 1
  FROM pg_auth_members am
  JOIN chain c ON am.member = c.roleid
)
SELECT * FROM chain ORDER BY depth;

-- 第 4 步:权限本身真的授给组了吗?(别忘了并集的其他路径)
SELECT has_table_privilege('app_readwrite', 'app.orders', 'INSERT');
SELECT has_schema_privilege('app_readwrite', 'app', 'USAGE');
SELECT has_database_privilege(current_user, current_database(), 'CONNECT');

-- 第 5 步:是不是列级权限在作怪?
SELECT has_column_privilege(current_user, 'app.orders', 'amount', 'SELECT');
症状最可能的原因一击定位
成员但 USAGE = f成员关系 INHERIT FALSE,或登录角色在 PG15- 是 NOINHERIT 且升级后未重设查 pg_auth_members.inherit_option
SET ROLE 报权限拒绝链上某跳 SET FALSE用第 3 步的递归查询看每跳 set_option
直接写表失败、SET ROLE 后成功继承关闭但切换可用第 2 步的三态组合即可判定
组里明明有权限,新会话还是不行连接池残留了 SET ROLE 到别的身份重连后先查 current_user
permission denied for schema只授了表没授模式 USAGE第 4 步的 has_schema_privilege

角色属性不通过继承传播 ​

这是必须单独强调的规则:LOGIN、SUPERUSER、CREATEDB、CREATEROLE、REPLICATION、BYPASSRLS 永远不随成员关系继承,无论成员关系的 INHERIT 选项如何。即使 alice 是 admin 组的成员,而 admin 拥有 CREATEDB,alice 直接执行 CREATE DATABASE 也会被拒;她必须 SET ROLE admin,以 admin 的身份执行(前提是成员关系 SET TRUE)。

sql
CREATE ROLE admin NOLOGIN CREATEDB CREATEROLE;
GRANT admin TO alice;

-- alice 的会话:
CREATE DATABASE tmp_db;      -- ERROR: permission denied to create database
SET ROLE admin;
CREATE DATABASE tmp_db;      -- 成功(current_user 已是 admin)
RESET ROLE;

从设计角度这是件好事:把高特权属性挂在 NOLOGIN 组角色上,人只能通过显式切换使用它,切换动作本身可被日志与 pg_stat_activity 观测到,比直接给登录角色发属性可审计得多。

WITH ADMIN OPTION 与 WITH GRANT OPTION 不被继承 ​

与角色属性同理,两个「转授权」也不随成员关系链传播:

  • ADMIN 选项:alice 拿到 app_admin 的 ADMIN 选项,能转授 app_admin 的成员关系;但这不妨碍另一个事实——她的 ADMIN 资格只针对这一层,不会因为她也间接是某个更上层组的成员而自动获得那个组的 ADMIN。
  • GRANT OPTION:对象权限的转授权资格属于「某角色对某对象」的授权记录,不是可继承的身份属性。组角色持有 WITH GRANT OPTION 时,成员要通过继承使用它需要权限本身被继承,但官方建议这类「谁能转授」的场景干脆用 SET ROLE 到持有者身份操作,语义最清晰。

pg_auth_members:成员关系的完整视图 ​

所有成员关系与三选项都记录在集群级目录 pg_auth_members 中(公开可读):

列类型说明
oidoid行标识
roleidoid(引用 pg_authid.oid)「组」一方的角色 OID
memberoid(引用 pg_authid.oid)「成员」一方的角色 OID
grantoroid(引用 pg_authid.oid)授予这条成员关系的角色
admin_optionbool成员能否转授 roleid 的成员关系
inherit_optionbool成员是否自动继承 roleid 的权限(PG16 新增)
set_optionbool成员能否 SET ROLE 到 roleid(PG16 新增)
sql
-- 以名字呈现的成员关系全表
SELECT g.rolname AS group_role,
       m.rolname AS member,
       gr.rolname AS grantor,
       am.admin_option,
       am.inherit_option,
       am.set_option
FROM pg_auth_members am
JOIN pg_roles g  ON g.oid  = am.roleid
JOIN pg_roles m  ON m.oid  = am.member
JOIN pg_roles gr ON gr.oid = am.grantor
ORDER BY 1, 2;

版本兼容

inherit_option 与 set_option 是 PG16 新增列。在 PG15 及更早版本的实例上运行包含这两列的查询会直接报错;需要兼容多版本的工具请用 SELECT * 或先探测列存在性。

继承与 pg_roles.rolinherit ​

角色级的 INHERIT/NOINHERIT 属性可以从 pg_roles 视图读到(列名 rolinherit,布尔)。要区分清楚它的角色:

sql
SELECT rolname, rolinherit, rolcanlogin, rolsuper, rolcreaterole, rolcreatedb
FROM pg_roles
WHERE rolname NOT LIKE 'pg\_%'
ORDER BY rolcanlogin, rolname;
  • PG15 及更早:rolinherit 直接决定该角色所有成员关系的运行时行为,是真正的「运行时开关」。
  • PG16 及以后:rolinherit 只决定新建立的成员关系的默认 inherit_option,运行时真正起作用的是 pg_auth_members.inherit_option。审计时若只看 rolinherit 就下结论,在 16+ 的实例上会得出错误判断。

一个实用的迁移巡检口径:把「rolinherit = false 的登录角色」与「其成员关系的 inherit_option = true」两个集合交叉比对,就能发现「角色声明了不继承、但某条关系显式开了继承」的漂移点——升级到 PG16 后这类漂移是允许的,但往往不是本意。

与 SECURITY DEFINER 的交互 ​

SECURITY DEFINER 函数以函数属主的身份执行,这为继承体系提供了第三条「临时获得权限」的通道,也带来独特的风险组合:

  • 属主设为组角色:把敏感函数的属主设为 NOLOGIN 组角色,成员即便对表毫无权限,也能通过调用函数完成受控操作。相比 SET ROLE,这条通道连「切换」动作都不需要,但也因此更难在会话层面审计。
  • 不受成员关系选项约束:任何能执行该函数的角色(注意 EXECUTE 默认授予 PUBLIC)都会以属主身份运行函数体,无论它在 pg_auth_members 里是什么选项组合。函数是绕过成员关系设计的后门,必须纳入权限审计范围。
  • 特洛伊木马警告:官方文档明确提醒——若某角色的权限被继承但不应被 SET ROLE 使用,就不要让它拥有任何 SQL 对象。原因是:能继承其权限的成员虽不能切换成它,却可以操纵它拥有的对象(例如重定义它拥有的函数)来获得对该角色的完全访问。
  • 配合 SET search_path 加固:SECURITY DEFINER 函数定义时应显式 SET search_path,防止通过 search_path 劫持来调用非预期对象——这与本页主题的交集在于:函数内部的权限检查同样以属主身份进行,任何劫持都会被放大成属主级权限。
sql
-- 典型的受控写入口:调用者对表零权限,函数属主持有写权
CREATE ROLE app_writer NOLOGIN;
GRANT INSERT, UPDATE ON app.ledger TO app_writer;

CREATE FUNCTION app.record_entry(p_amount numeric)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER                 -- 以 app_writer(属主)身份执行
SET search_path = app, pg_temp   -- 固定搜索路径
AS $$
BEGIN
  INSERT INTO app.ledger(amount) VALUES (p_amount);
END;
$$;
ALTER FUNCTION app.record_entry(numeric) OWNER TO app_writer;

REVOKE EXECUTE ON FUNCTION app.record_entry(numeric) FROM PUBLIC;
GRANT EXECUTE  ON FUNCTION app.record_entry(numeric) TO app_clerk;  -- 仅业务组可调用

PG16 的 CREATEROLE 收紧与 createrole_self_grant ​

PG16 对 CREATEROLE 的整顿与继承语义直接相关,方向有三点(概述,细节以官方文档为准):

  • 管理权变为显式授予:CREATEROLE 角色不再能修改任意角色,只能管理被授予了 ADMIN 选项的角色。老脚本里「CREATEROLE 用户改任意角色」的用法会开始报权限错误。
  • 建角色即建关系:CREATEROLE 角色创建新角色时,会自动建立「新角色 → 创建者」的成员关系,让创建者可管理、可切换、可继承——创建者不再拥有对无主角色的裸特权。
  • createrole_self_grant 参数(默认空):设置如 'set, inherit' 后,CREATEROLE 角色创建的每个新角色都会自动把这两个选项授回创建者,方便自动化流水线里保持旧行为。

修改继承设置的影响面 ​

把所有变化点整理成一张「什么改了、影响谁」的表:

变更影响范围立即生效?
ALTER ROLE u INHERIT / NOINHERIT(PG16+)仅影响今后把 u 加入组时的默认 inherit_option是(对既有关系无影响)
ALTER ROLE u INHERIT / NOINHERIT(PG15-)u 的全部成员关系的运行时行为是
GRANT g TO u WITH INHERIT FALSE(重设既有关系)仅这一条成员关系是
GRANT g TO u WITH SET FALSEu 能否 SET ROLE g是
REVOKE g FROM uu 失去 g 的继承与切换能力是(存量会话中已切换的 SET ROLE 会话在下次权限检查时表现为无权)

版本迁移清单(PG15 → PG16+) ​

围绕继承语义的升级,按顺序核对以下事项:

  1. 盘点存量成员关系:升级前用 pg_auth_members 导出全部成员关系;PG16 起 ALTER ROLE ... NOINHERIT 不再改变既有关系的运行时行为,升级后先核对 inherit_option 的自动换算结果是否符合预期。
  2. 改写授权脚本:新脚本一律显式写 WITH INHERIT TRUE/FALSE, SET TRUE/FALSE,不依赖「角色属性推断默认值」的旧习惯。
  3. 演练 CREATEROLE 收紧:依赖 CREATEROLE 的自动化(建号、改属性、授成员)在 16+ 会报权限错误,需改为「管理员显式 GRANT ... WITH ADMIN OPTION」或配置 createrole_self_grant。
  4. 兼容性探测:监控与审计脚本引用 inherit_option / set_option 前先探测列存在(PG16 之前无此列)。
  5. 重审「NOINHERIT 角色」语义:PG15 时代用 ALTER ROLE u NOINHERIT 一键关掉全部继承的团队,升级后要逐条关系重设,否则成员会突然开始继承。
  6. 检查工具链:备份/恢复链路(如导出成员关系再导入)在两版本间的目录结构差异需要适配。

常见反模式 ​

反模式后果纠正
给登录角色直接挂满权限,用完再逐条回收权限残留、审计困难权限进组,人挂组,走继承或切换
「SUPERUSER 做成组再拉人」无效——属性不继承,成员什么也得不到高特权操作走受控的专用登录角色
只看 rolinherit 判断 16+ 实例的继承行为结论错误——运行时看的是成员关系选项审计查询以 pg_auth_members.inherit_option 为准
多级组不写选项、依赖默认值PG16 前后默认语义不同,环境间漂移每跳显式写全三个选项
SET ROLE 后不 RESET ROLE后续语句以组身份执行,新建对象归组成对书写 / 事务内用 SET LOCAL ROLE
用 NOINHERIT 当「防提权锁」有 SET TRUE 依然能提权要阻断就 SET FALSE,并设受控入口

实战示例 ​

示例一:NOINHERIT 登录角色 + SET ROLE 的职责分离 ​

场景:应用连接角色平时只拥有「连接 + 查询」的能力,写操作必须显式切换到写组——把「能不能写」的决策点显式化。

bash
psql -U postgres -d appdb <<'SQL'
-- 组角色
CREATE ROLE app_readonly  NOLOGIN;
CREATE ROLE app_readwrite NOLOGIN;
GRANT USAGE ON SCHEMA app TO app_readonly, app_readwrite;
GRANT SELECT ON ALL TABLES IN SCHEMA app TO app_readonly, app_readwrite;
GRANT INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app TO app_readwrite;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA app TO app_readwrite;

-- 登录角色:NOINHERIT + 成员只挂只读组可切换写组
CREATE ROLE worker LOGIN PASSWORD 'W0rker_P@ss' NOINHERIT;
GRANT app_readonly  TO worker WITH INHERIT TRUE,  SET TRUE;
GRANT app_readwrite TO worker WITH INHERIT FALSE, SET TRUE;
SQL
bash
psql "dbname=appdb user=worker password=W0rker_P@ss"
sql
-- 平时:只有只读权限
SELECT has_table_privilege(current_user, 'app.orders', 'INSERT');
--  has_table_privilege
-- ---------------------
--  f
SELECT * FROM app.orders LIMIT 1;      -- 成功(只读继承)
INSERT INTO app.orders VALUES (1);     -- 失败:permission denied for table orders

-- 需要写时显式切换
SET ROLE app_readwrite;
INSERT INTO app.orders VALUES (1);     -- 成功
RESET ROLE;

示例二:多级组的继承链验证 ​

bash
psql -U postgres -d appdb <<'SQL'
CREATE ROLE grp_base    NOLOGIN;   -- 基础组
CREATE ROLE grp_power   NOLOGIN;   -- 高权组
GRANT grp_base TO grp_power WITH INHERIT TRUE;   -- 高权组继承基础组

CREATE ROLE u_inherit LOGIN NOINHERIT;
CREATE ROLE u_setonly LOGIN NOINHERIT;
GRANT grp_base  TO u_inherit WITH INHERIT TRUE;
GRANT grp_power TO u_inherit WITH INHERIT TRUE;   -- 两跳都可继承

GRANT grp_base  TO u_setonly WITH INHERIT FALSE, SET TRUE;
GRANT grp_power TO u_setonly WITH INHERIT FALSE, SET TRUE;
SQL
sql
-- u_inherit 登录:两级组权限全部自动可用
SET ROLE u_inherit;
SELECT pg_has_role('grp_base',  'MEMBER'),   -- t
       pg_has_role('grp_base',  'USAGE'),    -- t(继承可用)
       pg_has_role('grp_power', 'USAGE');    -- t
RESET ROLE;

-- u_setonly 登录:都只是 MEMBER,无 USAGE,必须 SET ROLE
SET ROLE u_setonly;
SELECT pg_has_role('grp_base', 'MEMBER'),      -- t
       pg_has_role('grp_base', 'USAGE'),       -- f(未继承)
       pg_has_role('grp_base', 'SET');         -- t(可切换)
RESET ROLE;

示例三:审计成员关系的三个开关 ​

sql
-- 找出「能切换到高危组」的成员
SELECT g.rolname AS group_role, m.rolname AS member
FROM pg_auth_members am
JOIN pg_roles g ON g.oid = am.roleid
JOIN pg_roles m ON m.oid = am.member
WHERE am.set_option
  AND g.rolname IN ('app_admin', 'grp_power')
ORDER BY 1, 2;

-- 找出 ADMIN 选项的持有者(转授权风险点)
SELECT g.rolname, m.rolname, am.grantor::regrole
FROM pg_auth_members am
JOIN pg_roles g ON g.oid = am.roleid
JOIN pg_roles m ON m.oid = am.member
WHERE am.admin_option;

示例四:受控提权入口——用 SET FALSE 设关卡 ​

场景:app_admin 是高危管理组,要求只有运维专用登录角色 dba_oncall 能切换进去;业务人员的登录角色即使通过别的路径成为 app_admin 的间接成员,也不能切换。

bash
psql -U postgres -d appdb <<'SQL'
CREATE ROLE app_admin   NOLOGIN;   -- 高危组:持有对象属主与管理权限
CREATE ROLE dba_oncall  LOGIN PASSWORD 'Dba0nc@ll';  -- 运维值班专用入口

-- 业务成员:只是间接成员,INHERIT FALSE 且 SET FALSE——拿不到也切不进
CREATE ROLE clerk LOGIN PASSWORD 'Cl3rk_P@ss';
GRANT app_admin TO clerk WITH INHERIT FALSE, SET FALSE;

-- 运维入口:显式放行 SET TRUE
GRANT app_admin TO dba_oncall WITH SET TRUE;
SQL
sql
-- clerk 的会话:
SELECT pg_has_role('app_admin', 'MEMBER');  -- t(成员身份成立)
SELECT pg_has_role('app_admin', 'USAGE');   -- f(不继承)
SET ROLE app_admin;
text
ERROR:  permission denied to set role to "app_admin"
sql
-- dba_oncall 的会话:
SET ROLE app_admin;      -- 成功:受控入口
RESET ROLE;

设计要点:关卡设在「成员关系」上而不是靠流程约束——SET FALSE 是数据库层面的硬边界,不依赖任何人遵守操作手册。间接成员关系(例如未来把 clerk 挂进某个中间组)也继承不了切换能力,因为 SET 与 INHERIT 一样是逐跳判定的。

示例五:CI 中的三态权限断言脚本 ​

权限漂移最好的防线是把它变成测试。下面的脚本对目标环境断言 pg_has_role() 的三个返回值,任何一条不符即失败(配合 CI 流水线在发布前拦截):

bash
#!/usr/bin/env bash
# assert-roles.sh —— 断言成员关系三选项,环境漂移即报错
# 用法:./assert-roles.sh <connstr>
set -euo pipefail

CONN="${1:?usage: assert-roles.sh <connstr>}"

run_assert() {
  psql "$CONN" -XAt -v ON_ERROR_STOP=1 -c "$1"
}

fail=0

# 1. clerk 必须是 app_admin 的成员,但不继承、不可切换
out=$(run_assert "SELECT pg_has_role('clerk','app_admin','MEMBER')::int
                       || pg_has_role('clerk','app_admin','USAGE')::int * 10
                       || pg_has_role('clerk','app_admin','SET')::int * 100")
[ "$out" = "1" ] || { echo "FAIL: clerk/app_admin 三态=${out},期望 1(仅 MEMBER)"; fail=1; }

# 2. dba_oncall 必须能切换 app_admin
out=$(run_assert "SELECT pg_has_role('dba_oncall','app_admin','SET')::int")
[ "$out" = "1" ] || { echo "FAIL: dba_oncall 无法切换 app_admin"; fail=1; }

# 3. 任何成员关系的 inherit_option 必须与设计矩阵一致(抽查写组)
out=$(run_assert "SELECT count(*) FROM pg_auth_members am
                  JOIN pg_roles g ON g.oid = am.roleid
                  WHERE g.rolname = 'app_readwrite' AND NOT am.inherit_option")
[ "$out" = "0" ] || { echo "FAIL: app_readwrite 存在 ${out} 条意外关闭继承的成员关系"; fail=1; }

[ "$fail" = "0" ] && echo "OK: 角色成员关系与设计矩阵一致" || exit 1

这个模式的威力在于:任何人手工 GRANT 了一个「计划外」的成员关系或改动了选项,下一次 CI 就会变红。断言矩阵建议与权限设计文档放在同一仓库、同一评审流程里。

注意事项 ​

SET ROLE 是替换不是叠加。 切换到组角色后,登录角色自身的权限不再参与权限检查;很多人以为「SET ROLE 后 = 自己的权限 + 组的权限」,实际是「只有组的权限」。需要叠加的场景应把权限都放进组里,让组嵌套表达层次。

继承链逐跳判定,一跳断则全断。 多级组里某一级 INHERIT FALSE,其上层的权限就不会自动可用;排错时沿 pg_auth_members 逐跳检查 inherit_option,不要只看首尾两层。

角色属性与转授权资格永不继承。 SUPERUSER、CREATEDB、CREATEROLE、REPLICATION、BYPASSRLS、LOGIN、ADMIN、GRANT OPTION 都必须通过 SET ROLE 使用(且需要 SET TRUE 链)。「把 SUPERUSER 做成组再拉人」不会让任何成员变成超级用户。

PG16 前后 INHERIT 语义不同。 老脚本迁移到 16+ 后,角色级 ALTER ROLE ... NOINHERIT 不再影响既有成员关系;逐条关系要显式 WITH INHERIT ... 重设。跨版本部署的 IaC 模板必须做分支处理。

pg_auth_members 的新列是 PG16 起才有。 兼容多版本的工具不要直接引用 inherit_option / set_option,或先查 pg_attribute 确认列存在。

NOINHERIT 不是加密锁。 只要 SET TRUE,任何获得会话控制权的主体都能执行 SET ROLE 拿到组权限。它的价值是「让提权成为显式动作、便于日志识别」,不是阻止提权。真正要阻断时用 SET FALSE。

RESET ROLE 才能回到登录身份。 SET ROLE 不会自动还原;长事务或长脚本里切换后忘记 RESET ROLE,后续语句会以组角色身份执行,创建的对象属主也会变成组角色——这可能是期望(对象归组),也可能是事故。

SET ROLE LOCAL 是事务级的自愈方案。 SET LOCAL ROLE group_role 只在当前事务内生效,事务结束(无论提交或回滚)自动还原为登录身份;发布脚本与迁移工具里用它,比手工配对 SET ROLE / RESET ROLE 更不容易漏。

CREATE ROLE 的 IN ROLE / ROLE / ADMIN 子句也能建初始成员关系。 但它们无法逐项指定 INHERIT / SET 两个开关(ADMIN 子句等价于附带管理权),建议自动化脚本统一用 GRANT 建成员关系,选项一目了然。

SECURITY DEFINER 函数是成员关系设计之外的通道。 即使某角色对组权限「既不继承也切换不进」,只要它有权限执行一个以高权角色为属主的 SECURITY DEFINER 函数,就能以属主身份做事。权限审计要把这类函数一并纳入,并检查它们的 EXECUTE 授予范围。

最佳实践 ​

  1. 人用 NOINHERIT,组用权限,切换显式化:登录角色默认无权(除连接),需要什么能力就 SET ROLE 什么组,让每次提权在日志里可见、可审计。
  2. 组角色一律 NOLOGIN 且不挂角色属性:高特权属性(CREATEDB/CREATEROLE)挂在 NOLOGIN 组上,通过 SET ROLE 使用,比直接发给人安全且可审计。
  3. 多级组的每一跳都显式写选项:GRANT ... WITH INHERIT TRUE, SET TRUE 写全,不依赖默认值,避免 PG16 前后默认语义差异造成的漂移。
  4. 高危组在链路里放一个 SET FALSE 关卡:确保高危能力只能从指定入口(如运维专用登录角色、受控工具)切换进入。
  5. 用 pg_has_role() 三态断言做巡检:MEMBER / USAGE / SET 三个返回值分别回答「是成员吗」「权限自动可用吗」「能切换吗」,比肉眼读表可靠。
  6. 审计脚本兼容 PG16:引用 inherit_option / set_option 前先探测列存在,或按版本维护两套查询。
  7. 升级 PG16 前演练 CREATEROLE 收紧:所有依赖 CREATEROLE 的自动化(建号、改属性)在测试环境过一遍,补齐显式的 GRANT ... WITH ADMIN OPTION。
  8. 在脚本里成对使用 SET ROLE / RESET ROLE:用事务包裹或 try/finally 语义保证还原;发布流程中把「切换 → 执行 → 还原」写成模板。
  9. 需要「临时变成某身份建对象」时优先组角色:SET ROLE 后创建的对象属主是组角色而非个人,天然避免「个人离职导致对象无主」的清理难题。
  10. 不要用 SET SESSION AUTHORIZATION 做业务功能:它是超级用户级的身份替换工具,只用于运维验证与恢复场景;业务里的身份切换应由应用层的多角色设计(成员关系 + SET ROLE)承担。

下一节

继续学习:组角色与会话角色

参考链接 ​