DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

PostgreSQL 已授权表的 INSERT,插入仍报 permission denied for sequence

PostgreSQL 表已有 INSERT 权限,新增记录仍报 permission denied for sequence,可核对应用当前角色与实际取号序列,补充单对象 USAGE,并区分现有授权和未来序列的默认权限。

应用能读取表,管理员也给了表的写入权限,但新增记录时仍出现下面的错误:

ERROR: permission denied for sequence posts_post_id_seq

如果插入语句没有提供自增主键,表的默认值需要调用序列取号,就应检查报错中的序列。表和序列是两个数据库对象;给表授予 INSERT,甚至 ALL PRIVILEGES,都不会自动授予其序列的权限。

下面针对 PostgreSQL 中由 serial、bigserial 或显式 nextval(...) 默认值取号的普通插入场景。先确认实际表定义;不要把所有自动生成主键的机制都当成同一种权限路径,也不要仅凭列名带有 id 就认定它使用序列。示例使用 public.posts 表、post_id 列、public.posts_post_id_seq 序列和 app_writer 应用角色,执行前替换为实际名称。

确认是哪个连接、哪个序列

在应用使用的数据库连接中读取身份,或用相同登录角色连接同一个数据库:

SELECT current_database(), session_user, current_user;

权限检查使用 current_user。管理页面登录用户与应用用户可能不同;连接后执行过 SET ROLE,当前角色也可能与登录角色不同。管理员连接能插入,不能证明应用连接具备同样权限。

如果使用 psql,可以查看表定义中的默认值:

\d public.posts

重点看主键列是否有类似 nextval('public.posts_post_id_seq'::regclass) 的默认值。也可以查询列关联的序列:

SELECT pg_get_serial_sequence('public.posts', 'post_id');

这个函数可以找出 serial 列关联的序列,也能返回 identity 列内部关联的序列。它返回序列名,不代表已经判断了本次插入的权限要求。若返回 NULL,不能据此认定插入绝不会调用序列:手工写的默认表达式或触发器仍可能调用其他序列,需要继续核对表定义和完整错误上下文。不要猜测序列一定叫“表名加列名加 seq”。

只有 phpPgAdmin 等 SQL 查询窗口、没有 SSH 时,上面的 SQL 查询仍可使用;反斜杠命令属于 psql,不能直接粘贴到通用 SQL 窗口。表定义可在管理界面查看。

表能写入,不等于序列能取号

确认对象名称后,用应用连接检查当前角色的有效权限:

SELECT
  has_table_privilege(current_user, 'public.posts', 'INSERT') AS table_insert,
  has_sequence_privilege(current_user, 'public.posts_post_id_seq', 'USAGE') AS seq_usage,
  has_sequence_privilege(current_user, 'public.posts_post_id_seq', 'UPDATE') AS seq_update;

nextval 要求序列上的 USAGE 或 UPDATE 权限,满足其中一个即可。如果表级 INSERT 为真,而后两个结果都为假,说明当前角色缺少取号权限,与报错一致。这里的查询只检查权限,不会推进序列。

表级检查为假时,还要留意是否采用了列级授权:应用可能只获准向指定列插入,不能把一个表级检查结果当作全部授权情况。序列权限已经满足却仍失败时,应重新确认实际应用会话、schema 和错误点;触发器或函数可能访问了另一条序列,或在不同角色上下文中执行。此时继续扩大当前序列的授权范围通常找不到原因。

在 psql 中可保存当前授权信息,供变更后核对:

\dp public.posts
\dp public.posts_post_id_seq

管理界面里的“所有权限”也必须看清对应对象。数据库、schema、表和序列分别有自己的权限,数据库层面的授权不会替代序列授权。

只补应用需要的取号权限

由序列所有者,或持有相应授权选项的管理角色,在目标数据库中执行:

GRANT USAGE ON SEQUENCE public.posts_post_id_seq TO app_writer;

对于通过 nextval 取得新编号的用途,USAGE 已满足要求。不要为了这一条插入错误,把应用角色改成超级用户、修改对象所有者,或对整个 schema 的序列授予所有权限。

序列上的 SELECT 单独不足以允许 nextval;它与 USAGE 都可以允许 currval。setval 则要求 UPDATE。因此,只有应用确实要调整序列状态时,才另行评估 UPDATE,不能把它当作修复普通插入的必选项。

如果授权语句执行失败,先处理管理账号的授权资格。普通应用账号无法给自己增加原本没有的权限。托管环境只有查询界面时,可以让数据库管理员针对这条序列授权,无须向应用提供管理员密码。

授权后,回到应用连接重跑权限查询,再在允许写入的测试环境中使用原来的插入流程,保持主键仍由原有机制生成。验收应看到序列权限缺口消失,原插入不再报序列权限错误,并得到符合表约束的记录。若随后出现表权限、行级安全策略或约束错误,应按新的错误处理,不能认为一个序列授权解决了所有写入限制。

不要用 SELECT nextval(...) 当作无副作用的检查。它会消耗一个序列值,即使事务随后回滚,这个号码也不会被收回。试插入也可能推进序列;允许编号出现空缺,别为填补测试留下的空号执行 setval 或重置序列。

迁移每次新建序列时都复发

给现有序列授权只修复这个对象。若迁移工具不断创建新表和新序列,应把相应授权纳入迁移脚本,或在明确角色边界后设置默认权限。

例如,只有 migration_owner 创建在 public schema 内的未来序列,都应允许 app_writer 取号时,可由有资格管理该角色默认权限的账号设置:

ALTER DEFAULT PRIVILEGES FOR ROLE migration_owner IN SCHEMA public
GRANT USAGE ON SEQUENCES TO app_writer;

这条语句不改变已经存在的序列,不能替代前面的单对象 GRANT。FOR ROLE 指的是创建对象的角色,不是获得权限的应用角色。新对象只采用创建时当前角色的默认权限,不会因为它是另一个角色的成员,就自动采用那个角色的默认权限。迁移实际用哪个角色创建对象,应与这里一致。

默认权限覆盖一个角色在指定 schema 中创建的未来序列,比修复单个对象的范围大。如果并非所有新序列都应向应用开放,逐个对象写入迁移授权更合适。

撤销变更时按原授权记录恢复

变更前保存对象授权和默认权限记录,并确认应用对该序列的依赖。若本次只是新增了一条原先不存在的直接授权,且确认不再需要,可以撤销它:

REVOKE USAGE ON SEQUENCE public.posts_post_id_seq FROM app_writer;

撤销后,依赖它取号的插入可能再次失败。不要对原本已有的授权机械执行回退,也不要加 CASCADE 扩大影响。角色仍可能从其他角色或 PUBLIC 获得权限,应再次检查有效权限,而非仅凭一条撤销命令判断结果。

如果还新增了上述 schema 范围的默认授权,对应撤销为:

ALTER DEFAULT PRIVILEGES FOR ROLE migration_owner IN SCHEMA public
REVOKE USAGE ON SEQUENCES FROM app_writer;

它只影响以后创建的对象;期间已经创建并获得授权的序列要逐一核对。schema 范围的默认撤销也不能抵消全局默认授权,恢复时需要对照原有配置,而不是批量清空权限。