前段时间给一个新库配只读账号,执行了这么一句:
sql
GRANT SELECT ON ALL TABLES IN SCHEMA app TO readonly;
应用连上去还是报 permission denied for schema app。当时还纳闷,表权限都给了,怎么连 schema 都不让看。后来才反应过来,schema 的 USAGE 没给。这个错不复杂,但挺典型:PostgreSQL 的权限不是给一张表就完事,它是分层管的,库、schema、对象各管各的,上面一层给了不代表下面自动通。
下面这些是我后来重新捋下来的,按自己的理解顺序写,不一定按官方文档目录来。
角色就是一切,所有者有"固有权利"
PostgreSQL 里没有单独的"用户"概念,只有角色。能登录的叫 LOGIN 角色,不能登录的也可以建,专门当权限容器用。建对象的人默认是对象的所有者。
这里有个容易绕的点:所有者对对象的权利是固有的,比如改表定义、DROP、GRANT/REVOKE,这些不是靠某条 GRANT 得来的。普通对象权限可以授来授去,但"所有权"本身不是普通权限,要换所有者得用 ALTER TABLE ... OWNER TO ... 这类语句。所有者的成员角色可以通过继承拿到这些权利,但概念上别把它和 GRANT SELECT 混在一起。
所以一个新角色刚建出来,哪怕能连库,默认也几乎什么都干不了。权限得一层一层给。
权限是分层的,不能越级
一个角色要读一张表,通常得同时具备:
- 数据库上的
CONNECT,不然连不上; - schema 上的
USAGE,不然看不到里面的对象; - 表上的
SELECT,不然读不了。
少哪层报哪层的错。我那次就是库和表都给了,schema 的 USAGE 漏了。
给权限的顺序一般也是从外往里:
sql
GRANT CONNECT ON DATABASE mydb TO readonly;
GRANT USAGE ON SCHEMA app TO readonly;
GRANT SELECT ON TABLE app.orders TO readonly;
ALL PRIVILEGES 也不是万能钥匙。它只覆盖该对象类型上可授予的那些权限,不包含数据库的 CONNECT,不包含 schema 的 USAGE,更不包含所有权带来的 DROP。所以看到 GRANT ALL 别默认万事大吉,还是得看给在哪一层。
GRANT 和 REVOKE 的两个坑
WITH GRANT OPTION 是让被授权者可以继续转授:
sql
GRANT SELECT ON TABLE orders TO alice WITH GRANT OPTION;
alice 可以把 SELECT 再授给别人。后面如果撤销 alice 的权限,默认的 RESTRICT 会阻止操作,因为下游还有人依赖她的授权;用 CASCADE 会连锁撤销下去。这个在授权链长了以后要小心,别随手 CASCADE。
另一个是 GRANT OPTION FOR,只撤销"转授资格",保留权限本身:
sql
REVOKE GRANT OPTION FOR SELECT ON TABLE orders FROM alice;
还有特殊角色名 PUBLIC,代表系统里每一个角色。GRANT ... TO PUBLIC 等于向所有人开放,生产环境里看到这种语句我一般会停下来多看两眼。
继承和 SET ROLE:自动拿权限,还是临时切换
角色成员关系有两种用法,取决于成员角色有没有 INHERIT。
默认是 INHERIT,成员自动拥有所属角色的权限:
sql
CREATE ROLE app_readonly NOINHERIT;
CREATE ROLE alice LOGIN INHERIT;
GRANT app_readonly TO alice;
alice 登录后直接就有 app_readonly 的权限,不用切换。
如果是 NOINHERIT,成员不会自动获得组权限,得手动:
sql
SET ROLE app_readonly;
-- 干只读的活
RESET ROLE;
这里注意,SET ROLE 是替换当前会话的权限集,不是叠加。切到 app_readonly 之后,alice 自己直接持有的权限在会话里暂时用不了,切回去才恢复。
另外,LOGIN、SUPERUSER、CREATEDB、CREATEROLE 这类角色属性不会通过继承自动生效。哪怕成员是 INHERIT,想用这些属性也得 SET ROLE 到持有属性的角色。
新表不会自动继承授权,默认权限得提前设
这是上线后最容易冒出来的问题:今天给现有表 GRANT 了,明天开发新建一张表,只读账号又报 42501。因为直接 GRANT 只影响当前已有的对象,未来表不认。
ALTER DEFAULT PRIVILEGES 就是干这个的:
sql
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app
GRANT SELECT ON TABLES TO app_readonly;
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app
GRANT USAGE, SELECT ON SEQUENCES TO app_readwrite;
它只影响未来创建的对象,不改已有对象;而且只能为当前角色或当前角色所属的角色设置。常见做法是由对象所有者角色来设,比如上面的 app_owner,这样谁建表都按这个规则走。
public schema 该动手就动手
PostgreSQL 15 之前,public schema 默认对 PUBLIC 给了 CREATE 和 USAGE,意思是任何能连库的角色都能在 public 里建表。PG15 起默认撤销了 CREATE,但很多库是从老版本升上来的,权限可能还在。所以不管新库老库,检查一下不亏:
sql
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
需要更严的话,连 USAGE 也一起收,再给每个用户建同名私有 schema,调整 search_path,让未限定名称的对象落到自己 schema 里,而不是共享的 public。
RLS(行级安全):能读表,和能读哪些行,是两码事
表级 GRANT SELECT 管的是"能不能读这张表",行级安全管的是"读到哪些行"。RLS 默认关闭,得先开:
sql
ALTER TABLE customer ENABLE ROW LEVEL SECURITY;
然后建策略:
sql
CREATE POLICY customer_own_rows ON customer
FOR ALL
TO app_user
USING (owner_id = current_user_id())
WITH CHECK (owner_id = current_user_id());
USING 管已有行对查询是否可见,也管 UPDATE/DELETE 的目标行;WITH CHECK 管新写入或修改后的行能不能落库。
一个容易忽略的点:表开了 RLS 但一条策略都没建,默认是拒绝所有行访问的。表所有者通常例外,除非上 FORCE ROW LEVEL SECURITY。多租户场景里经常配合 current_setting('app.tenant_id') 这类会话变量用,在同一套表里做行级隔离。
几个我见过的误区
GRANT ALL 不等于所有权,前面说过,它只覆盖可授予权限。
新表不会自动继承旧表授权,必须靠 ALTER DEFAULT PRIVILEGES 或建表后显式 GRANT。
超级用户(postgres)绕过所有权限检查,应用如果长期拿超级用户连库,上面这些配置基本等于白做。
我现在配权限的顺手流程
一般先建不带 LOGIN 的组角色,比如 app_readonly、app_readwrite,把对象权限授给角色,再把角色授给具体登录用户。权限调整时改角色成员就行,不用逐用户改。
然后按库、schema、对象逐层给,别跳。对象所有者角色设好 ALTER DEFAULT PRIVILEGES,避免新表上线漏权限。public schema 的 CREATE 收掉。应用连接用最小权限角色,不用超级用户。
排查的时候我常用这几个:
sql
\dp app.orders
\ddp
\dp 看表、视图、序列的权限,\ddp 看默认权限。要精确查可以用:
sql
SELECT * FROM information_schema.role_table_grants
WHERE table_schema = 'app' AND table_name = 'orders';
函数里判断权限用 has_table_privilege()、has_schema_privilege() 也方便。
差不多就这些。PostgreSQL 这套权限设计其实挺清楚,坑大多在"以为给了"和"其实没给"的边界上。先把分层和默认权限这两件事记住,能少排不少 42501。