以下为根据项目标题「PostgreSQL函数权限测试」生成的博文:
1. 函数权限的核心问题:调用权限与函数体内权限是两回事
PostgreSQL的权限体系本身就是一个容易踩坑的重灾区,尤其是函数权限。很多同学在排查问题时报错日志里看到一句permission denied for function,第一反应是给用户GRANT EXECUTE,结果授予之后依然报错,这时候基本就是掉进了“函数自身权限”和“函数体内对象权限”这两个不同层级的坑里。
先说最核心的一个结论:PostgreSQL里函数权限分为两层。
第一层是函数本身的执行权限,控制“谁能调用这个函数”。第二层是函数内部的SQL语句所涉及对象的权限,也就是函数执行时需要对表、视图、序列等对象做的SELECT、INSERT、UPDATE、DELETE操作,这些操作需要函数执行时的当前用户具备相应权限。
这两层权限是独立的,也是叠加的。换句话说,即使你给了用户函数的EXECUTE权限,函数内部访问的表用户没有权限,执行依然报错。反过来也一样,用户有了底层表权限,但函数EXECUTE没有授权,照样无法调用。
这篇文章就围绕这两层权限,结合我实际测试过的场景,把函数权限的授予、回收、验证以及容易踩的坑系统整理一遍。适合正在做数据库权限隔离、多用户应用开发,或者被函数权限折磨过的DBA和后台开发同学参考。
为了下面叙述方便,我统一用这么一套测试环境:
-- 创建两个普通用户 CREATE USER app_user WITH PASSWORD 'app_pass'; CREATE USER read_user WITH PASSWORD 'read_pass'; -- 数据表所属的角色 CREATE USER data_owner WITH PASSWORD 'owner_pass'; -- 建一张测试表,由 data_owner 拥有 CREATE TABLE IF NOT EXISTS data_owner.user_profile ( id serial PRIMARY KEY, username text NOT NULL, email text, created_at timestamptz DEFAULT now() ); INSERT INTO data_owner.user_profile (username, email) VALUES ('alice', 'alice@example.com'), ('bob', 'bob@example.com'), ('carol', 'carol@example.com');注意,我特意把表建在了data_owner这个schema下,这是为了后面测试跨schema的场景。默认的publicschema权限太开放,不具备典型性。
2. EXECUTE权限授予与回收:别忽略默认的PUBLIC授权
2.1 默认情况下所有人都有EXECUTE权限
PostgreSQL对函数的默认权限有一个非常容易忽视的点:函数创建之后,默认对PUBLIC(即所有角色)授予了EXECUTE权限。
也就是说,如果你创建了一个函数,没有主动做任何授权操作,那么数据库里所有能连接上来的用户都可以调用这个函数。从安全角度看,这个默认行为其实和很多人直觉中的“默认应该只有owner能调用”完全相反。
测试验证一下:
-- 以超级用户创建一个测试函数 CREATE OR REPLACE FUNCTION data_owner.get_user_count() RETURNS int LANGUAGE sql AS $$ SELECT count(*) FROM data_owner.user_profile; $$; -- 切换到 app_user SET ROLE app_user; SELECT data_owner.get_user_count();结果你会发现,app_user在没有被显式授权的情况下,直接可以执行这个函数。这就是PUBLIC默认EXECUTE权限起的作用。
2.2 回收PUBLIC权限,再定向授予
如果要做权限隔离,第一步就是把默认的PUBLIC权限收回来:
REVOKE EXECUTE ON FUNCTION data_owner.get_user_count() FROM PUBLIC;然后是定向授权给指定的角色:
GRANT EXECUTE ON FUNCTION data_owner.get_user_count() TO app_user;这里有个容易出错的地方:每当函数被CREATE OR REPLACE重新定义之后,它的权限并不会重置,所以如果你先做了REVOKE,之后又用CREATE OR REPLACE修改了函数体,权限依然保持REVOKE之后的状态,不需要重新授权。但如果用的是DROP FUNCTION再重建,权限就会回到默认状态(又变成PUBLIC可执行),需要重新执行REVOKE和GRANT。
实际操作中,我一般倾向于在函数创建之后立即执行REVOKE,形成一套固定的发布流程:
CREATE OR REPLACE FUNCTION data_owner.get_user_count() RETURNS int LANGUAGE sql AS $$ SELECT count(*) FROM data_owner.user_profile; $$; REVOKE ALL ON FUNCTION data_owner.get_user_count() FROM PUBLIC; GRANT EXECUTE ON FUNCTION data_owner.get_user_count() TO app_user;REVOKE ALL比单独REVOKE EXECUTE更稳健,它会同时回收将来可能出现的其他权限(比如后面要讲的ALTER权限、PROVIDE权限等等),确保函数权限从创建之初就是受控状态。
2.3 函数重载与权限的对应关系
PostgreSQL支持函数重载,也就是同名函数,参数列表不同,视为不同的函数对象。授权时必须带上完整的参数列表,否则容易授错对象。
CREATE OR REPLACE FUNCTION data_owner.get_user_count(start_date date) RETURNS int LANGUAGE sql AS $$ SELECT count(*) FROM data_owner.user_profile WHERE created_at >= start_date; $$; -- 注意:这两个是不同的函数对象 REVOKE ALL ON FUNCTION data_owner.get_user_count() FROM PUBLIC; REVOKE ALL ON FUNCTION data_owner.get_user_count(date) FROM PUBLIC; GRANT EXECUTE ON FUNCTION data_owner.get_user_count() TO app_user; GRANT EXECUTE ON FUNCTION data_owner.get_user_count(date) TO app_user;少写参数类型,PostgreSQL会报function ... does not exist,反过来如果你写了一个不存在的同名函数,它也会直接报错,不会静默处理。
2.4 基于实际需求的授权模型建议
从我处理过的一些项目经验来看,函数授权通常按三类角色划分:
第一类是只读报表用户,需要调用查询类函数,但不需要修改类函数。第二类是业务应用用户,需要完整的事务类函数,比如批量插入、状态更新。第三类是管理员用户,负责函数定义修改和版本维护。
对应的授权策略就是:
-- 报表用户:只给查询类函数 GRANT EXECUTE ON FUNCTION data_owner.get_user_count() TO read_user; -- 应用用户:查询+写入类函数都授 GRANT EXECUTE ON FUNCTION data_owner.get_user_count() TO app_user; GRANT EXECUTE ON FUNCTION data_owner.insert_user_profile(text, text) TO app_user; -- 管理员用户:拥有全部权限,不需要单独授这个模型的关键点在于:应用用户永远不应直接拥有底层表的DML权限,而是通过函数这个中间层去操作数据。这样后续的数据校验、权限审计、逻辑变更都集中在函数层,不至于权限扩散到表级别。
3. 函数体内的权限边界:SECURITY INVOKER与SECURITY DEFINER
3.1 两种执行模式的含义
函数体内的权限边界,由函数的SECURITY INVOKER或SECURITY DEFINER属性决定。这两个模式是PostgreSQL函数权限体系最重要的分水岭。
SECURITY INVOKER(默认):函数以调用者的身份执行。调用者在函数内部执行的每条SQL,都是以其自身权限来判定是否允许。
SECURITY DEFINER:函数以函数所有者的身份执行。调用者不管自身权限如何,只要被授予了该函数的EXECUTE权限,函数内部的SQL就是以所有者的权限去执行,哪怕调用者在底层表上一个权限都没有。
这种模式差异,决定了函数适合承载哪种业务逻辑。如果你只需要调用者自己有权访问的对象,用INVOKER。如果你需要让低权限用户通过函数间接访问高权限数据,用DEFINER。
3.2 测试记录:SECURITY INVOKER模式下的权限拒绝
用前面那张data_owner.user_profile表来测试。先创建INVOKER模式的函数:
CREATE OR REPLACE FUNCTION data_owner.get_user_email_by_id(target_id int) RETURNS text LANGUAGE sql SECURITY INVOKER AS $$ SELECT email FROM data_owner.user_profile WHERE id = target_id; $$; REVOKE ALL ON FUNCTION data_owner.get_user_email_by_id(int) FROM PUBLIC; GRANT EXECUTE ON FUNCTION data_owner.get_user_email_by_id(int) TO app_user;现在切换到app_user执行:
SET ROLE app_user; SELECT data_owner.get_user_email_by_id(1);结果报错:
ERROR: permission denied for table user_profile CONTEXT: SQL statement "SELECT email FROM data_owner.user_profile WHERE id = target_id"这个报错点就在于函数自身EXECUTE权限没问题,但函数体内SELECT了一张app_user没有权限的表。我在这里额外说明一下,省略号那一行是PostgreSQL返回的上下文信息,实际报错内容就是你看到的那段英文提示,如果用的是中文环境,会显示“对表 user_profile 权限被拒绝”。
这正是“函数自身权限”和“函数体内对象权限”互相独立的典型案例。很多新人在排查时反复检查EXECUTE授权,却没有意识到真正问题在表权限上。
3.3 测试记录:SECURITY DEFINER模式下的权限提升
把函数改成DEFINER模式:
CREATE OR REPLACE FUNCTION data_owner.get_user_email_by_id(target_id int) RETURNS text LANGUAGE sql SECURITY DEFINER AS $$ SELECT email FROM data_owner.user_profile WHERE id = target_id; $$;注意,我用的是CREATE OR REPLACE,它会保留之前的权限设置和SECURITY属性。由于之前已经授予了app_userEXECUTE权限,直接替换函数定义不影响权限。
再次以app_user身份执行:
SET ROLE app_user; SELECT data_owner.get_user_email_by_id(1);这次能正常返回alice@example.com。原因就是函数以data_owner这个表属主的身份执行了函数体内的SELECT,而data_owner对自己的表天然拥有所有权限。
3.4 SECURITY DEFINER的权限提升陷阱
DEFINER模式看起来很好用,但它是双刃剑。授权时稍有不慎,等于把一个高权限账号的钥匙交到了低权限用户手上。
举个例子:如果函数体内是动态SQL拼接,使用了用户传入的文本直接嵌入SQL,那么当调用者通过EXECUTE权限触发这个函数时,函数是以定义者权限去执行这段动态语句的,调用者等于借了一个高权限的身份去执行任意SQL。
再举一个更隐蔽的坑:如果DEFINER函数往一个表里插入数据,而这个表上有触发器,触发器内再去访问其他对象,那么整个链路都是以定义者权限执行的。一旦定义者是超级用户,这个调用链就能访问到超级用户能访问的一切东西。
所以我的经验是,能用INVOKER就不用DEFINER,非要用DEFINER时必须满足三个条件:
- 函数体内没有使用动态SQL,或者动态SQL不依赖用户输入。
- 函数的参数有严格的白名单校验。
- 函数所有者的权限被严格控制,不要把表权限授给一个日常业务账号却让它当DEFINER函数的owner。
此外,还需要主动把DEFINER函数的EXECUTE权限回收掉PUBLIC,只授给确需调用的角色,缩小暴露面。这个授权最小化原则,在函数权限测试中应当作为硬性要求。
4. 函数权限测试的完整实操流程
4.1 构造可重复的测试用户与测试数据
权限测试最怕环境不一致。可以在一个独立的测试库中做,或者用事务封装测试过程,这样不会污染业务数据。
我先说我的做法。用psql连接测试库,所有操作在一个事务里执行,最后回滚,这样即使测试产生脏数据也不会影响真实环境:
BEGIN; -- 创建角色(如果不存在) DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN CREATE USER app_user WITH PASSWORD 'app_pass'; END IF; END $$; -- 建测试表与数据 CREATE TABLE IF NOT EXISTS data_owner.user_profile ( id serial PRIMARY KEY, username text NOT NULL, email text, created_at timestamptz DEFAULT now() ); TRUNCATE data_owner.user_profile RESTART IDENTITY; INSERT INTO data_owner.user_profile (username, email) VALUES ('alice', 'alice@example.com'), ('bob', 'bob@example.com');事务里的DDL在PostgreSQL中是支持回滚的,所以整个测试过程是干净的。如果发现权限配置有误,直接ROLLBACK重来,不需要手动清理角色和表。
4.2 用SET ROLE模拟低权限用户
测试低权限用户是否真的能调用函数,不能用超级用户直接测,必须切换到目标角色。psql里切换用户身份用SET ROLE,但要注意SET ROLE和SET SESSION AUTHORIZATION的区别。
SET ROLE只是切换当前会话的“当前角色”,你仍然保有原角色的权限集合,并且可以RESET ROLE回到原有角色。
SET SESSION AUTHORIZATION则是真正切换了整个会话的身份,影响更彻底。
在权限测试里,用SET ROLE就够了。它执行时会把当前有效权限切换为目标角色的权限,能真实反映目标角色的权限边界。
SET ROLE app_user; SELECT current_user, session_user;结果:
current_user | session_user --------------+-------------- app_user | your_origin_role然后直接执行函数:
SELECT data_owner.get_user_email_by_id(1);如果报权限不足,就能立刻定位到函数体内部的权限问题。这时我用两个辅助SQL来区分到底是哪一层权限不够:
-- 查看当前角色是否具备函数的EXECUTE权限 SELECT has_function_privilege('app_user', 'data_owner.get_user_email_by_id(int)', 'EXECUTE'); -- 查看当前角色是否具备底层表的SELECT权限 SELECT has_table_privilege('app_user', 'data_owner.user_profile', 'SELECT');has_function_privilege和has_table_privilege这两个内置函数在权限测试中非常实用。前者返回布尔值,后者也返回布尔值,哪一层是false,问题就出在哪一层。
4.3 测试序列权限与NEXTVAL的坑
函数里使用serial自增列时,会有另一个隐蔽的权限问题,就是序列的权限。
PostgreSQL里serial列背后其实是一个序列对象,向表里插入数据时,如果没有显式指定id值,PostgreSQL会自动调用nextval从序列取下一个值。
很多DEFINER函数往表里插入数据后报错如下:
ERROR: permission denied for sequence user_profile_id_seq这个问题的根因在于:函数针对底层表有权限,但是针对表背后的序列对象没有权限,导致插入操作无法自动获取下一个自增值。
解决方法是把序列的USAGE权限也授予函数所有者,或者把函数改为显式传id值。我通常建议用前者,因为自增序列本意就是让数据库自动管理id,不应该在应用层手工指定。
在权限测试时,不要只测INSERT和SELECT,还需要检查序列权限:
GRANT USAGE, SELECT ON SEQUENCE data_owner.user_profile_id_seq TO data_owner;顺便说一句,serial这类旧式自增写法在大型系统里逐渐被IDENTITY列替代,但它的权限模型仍然是基于序列对象的,所以序列权限问题并不能因为改成IDENTITY就自动消失,只是PostgreSQL 15以后对IDENTITY列的序列做了更透明的权限绑定,不过旧库升级过程中依然可能遇到历史遗留的权限配置。
4.4 用EXPLAIN验证执行计划时权限的差异
还有一个容易被忽略的测试点:EXPLAIN和EXPLAIN ANALYZE在某些权限情况下表现不同。
EXPLAIN只生成执行计划,不实际执行SQL,因此即使没有底层表权限,也有可能正常返回执行计划。EXPLAIN ANALYZE会真实执行SQL,触发权限校验。
我用一个测试示例来演示:
SET ROLE app_user; EXPLAIN SELECT email FROM data_owner.user_profile WHERE id = 1;如果app_user没有表的SELECT权限,这条语句可能依然能成功,返回一条执行计划。但如果你在函数里排查时用EXPLAIN ANALYZE,就会立刻触发权限拒绝。
所以在测试函数权限时,如果发现EXPLAIN不报错而实际执行报错,不要以为权限没问题,这是正常的,只能证明优化器能生成计划,不代表执行层放行。
4.5 用事务回滚做权限配置的临时验证
有些权限改动不方便在生产环境长期保留,比如临时开放某个用户对一个表的SELECT权限,只为了验证一个DEFINER函数是否能正常工作。
可以用事务包裹权限GRANT,验证后回滚:
BEGIN; GRANT SELECT ON data_owner.user_profile TO app_user; SET ROLE app_user; SELECT data_owner.get_user_email_by_id(1); RESET ROLE; ROLLBACK;执行完成后权限恢复原状,不留残余。这是一个安全且高效的验证手段,权限测试过程中我经常用这个方式反复尝试不同配置,不需要等到权限变更审批流程走完才能测试。
5. 常见问题与排查技巧实录
5.1 问题速查表
整理几张我在测试过程中积累的问题速查表,方便你遇到报错时快速对照。
| 问题 | 可能原因 | 排查方向 |
|---|---|---|
| permission denied for function | PUBLIC的EXECUTE被回收且用户未获授权 | 检查函数自身的权限授予 |
| permission denied for table | 函数体内部访问了调用者或owner无权限的表 | 检查底层表权限、INVOKER/DEFINER模式 |
| permission denied for sequence | 自增列对应的序列未授权 | 检查序列的USAGE和SELECT权限 |
| function does not exist | 函数名或参数列表不匹配,或者函数在search_path之外 | 检查函数签名、schema路径 |
| cannot execute ... in a read-only transaction | 函数内部有写操作而事务是只读 | 检查函数是否真的需要写数据 |
5.2 默认权限与PUBLIC陷阱
再提一次最容易被忽略的陷阱:新建函数默认对PUBLIC有EXECUTE权限。这个问题在publicschema里创建函数时尤其危险,因为所有的用户都能连接进publicschema并调用函数。
我曾经接手过一个项目,某张业务表被设计成只能通过函数访问,函数是定义在publicschema里的,当时认为“只要不授权别人就调不了”。结果审计发现,任何能连接这个库的用户都能直接调用函数绕过表权限控制,因为默认PUBLIC权限没有回收,而函数还是DEFINER模式。
这种问题用一条SQL就能查出来:
SELECT p.proname, n.nspname AS schema_name, pg_get_userbyid(p.proowner) AS owner, p.prosecdef AS is_security_definer, CASE WHEN EXISTS ( SELECT 1 FROM pg_proc p2 JOIN pg_namespace n2 ON n2.oid = p2.pronamespace WHERE p2.oid = p.oid AND has_function_privilege('PUBLIC', p2.oid, 'EXECUTE') ) THEN 'PUBLIC_CAN_EXECUTE' ELSE 'RESTRICTED' END AS public_execute_status FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') AND p.prokind = 'f';执行这个查询后,你会看到所有用户自定义函数是否仍被PUBLIC可执行。public_execute_status显示为PUBLIC_CAN_EXECUTE的,就必须警惕,尤其是DEFINER模式下将对底层表造成严重权限绕过风险。
这条SQL中条件p.prokind = 'f'表示只列出普通函数,p.prokind = 'p'则是存储过程,如果需要排查存储过程,把条件改掉或者去掉即可。
5.3 search_path与函数权限的组合坑
函数权限排查中,search_path问题与权限问题经常同时出现,而且报错信息都很类似。函数内部如果用了不带schema限定的表名,PostgreSQL会根据函数创建时的search_path去解析表对象,而不是根据调用者的search_path。
如果函数创建时search_path指向了一个恶意或意外schema,可能会出现“函数看起来在访问表A,实际上却在访问另一个schema下的同名表B”的情况,从而引发权限不一致的问题。
举个例子:
CREATE OR REPLACE FUNCTION public.safe_query() RETURNS text LANGUAGE sql SECURITY DEFINER SET search_path = public AS $$ SELECT email FROM user_profile; $$;这里我显式加了SET search_path = public,让函数无论被谁调用,都固定从publicschema解析user_profile。如果不加这个SET子句,函数会沿用调用者的search_path,一旦调用者的search_path里插入了恶意schema,同名表替换就会造成数据泄露或权限绕过。
所以在函数权限测试中,检查函数的proconfig里是否设置了search_path也是必做项。
SELECT proname, proconfig FROM pg_proc WHERE proname = 'safe_query';如果proconfig为空,DBA应该主动为DEFINER函数加上SET search_path,打磨测试脚本时也应把这一项列入检查清单。
5.4 已有会话权限残留导致的误判
做权限测试时,我会先确认当前会话确实切换到了目标角色,并且没有因为之前测试中的误操作让权限残留。
常见误判场景是:之前用超级用户执行了SET ROLE app_user,随后某个测试步骤里执行了RESET ROLE或者新的SET ROLE other_user,但没有完全退出会话,导致后续测试身份混乱。
稳妥的做法是每轮测试都从全新的psql会话开始,或者在测试脚本头部强制重置身份:
RESET ROLE; SET ROLE app_user; SELECT current_user;确认输出是app_user再继续。如果输出还是原来的超级用户,说明SET ROLE语句因为某个外层事务异常未能生效,需要先ROLLBACK清理事务状态。
5.5 函数内部事务与外层事务的交互
很多人在写函数时没有意识到,PostgreSQL函数体内的操作是跟随外层事务的。也就是说,如果外部调用时包裹了一个事务,函数内即使写了COMMIT也不允许执行,会报错:
ERROR: cannot commit while a subtransaction is active反过来说,函数内的DDL语句,比如创建临时表,也遵循事务语义,外层回滚时函数内的DDL也会回滚。
这一点和权限测试有什么关系?如果业务代码在外层事务中先调用了某个DEFINER函数,该函数内部执行了SET类语句改变了当前会话状态(比如修改search_path),外层事务提交前这个状态变更可能一直残留到事务结束,影响后续所有SQL的权限解析。
所以在验证函数权限时,不仅要测试单条SQL调用,还要模拟真实业务中的事务包裹场景:
BEGIN; SELECT data_owner.insert_user_profile('dave', 'dave@example.com'); -- 验证函数内部的权限行为 ROLLBACK;如果函数在事务中表现正常,在事务外再测一遍,确保两种场景权限行为一致。若只在一种场景下出错,优先排查函数是否修改了会话级状态。
5.6 给测试环境的一点建议
权限测试最好不要直接在生产环境做,哪怕用事务回滚,一旦出现误操作(比如忘了回滚,把GRANT直接提交了),都会在生产库遗留不期望的权限配置。
建议专门准备一个与生产环境版本一致的测试库,采用相同的schema结构和角色体系,然后在测试库上反复调整权限配置,直到所有场景验证通过,再把GRANT语句带到生产环境执行。
拿我自己的实践来说,我测试库的脚本结构如下:
# 1. 创建角色和表结构的脚本 # 2. 创建函数的脚本 # 3. 权限授予的脚本 # 4. 测试用例的脚本(每个用例都是事务包围,最后回滚) # 5. 权限查询脚本(用于验证最终状态)分步骤执行的好处是,每一步都能独立验证,权限步骤出错时能明确知道是哪一个GRANT导致的,不用回滚重跑全部内容。
6. 从报错到排查:一个完整的问题定位实例
6.1 问题描述
我在一个模拟项目X中遇到过这样的问题:应用新版本上线后,用户登录接口全部报ERROR: permission denied for function。查看日志,接口调用的函数是auth.verify_user_login(text, text),但应用连接数据库的账号在数据库里根本没有这个函数的EXECUTE权限。
6.2 排查过程
第一步,以超级用户身份查看当前函数的权限分配:
SELECT p.proname, n.nspname AS schema_name, CASE WHEN has_function_privilege('app_user', p.oid, 'EXECUTE') THEN 'YES' ELSE 'NO' END AS app_user_execute, CASE WHEN has_function_privilege('read_user', p.oid, 'EXECUTE') THEN 'YES' ELSE 'NO' END AS read_user_execute FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE p.proname = 'verify_user_login';结果发现app_user的app_user_execute列显示为NO。也就是说,函数自身权限层面就没有放行,报错自然无法避免。
第二步,检查该函数的定义。结果发现函数签名是verify_user_login(user_name text, pwd text),但应用调用的SQL写的是SELECT auth.verify_user_login($1, $2),而且search_path没有包含authschema,导致函数解析失败。
这两个问题叠加,应用看到的是“找不到函数”,但PostgreSQL在某些配置下返回的却是permission denied。我把函数调用改成带schema限定的写法,问题就消失了。
6.3 解决方案与复盘
完整处方如下:
-- 1. 给函数授权 GRANT EXECUTE ON FUNCTION auth.verify_user_login(text, text) TO app_user; -- 2. 回收PUBLIC权限(确认不会误开放给其他用户) REVOKE ALL ON FUNCTION auth.verify_user_login(text, text) FROM PUBLIC; -- 3. 应用侧统一使用schema限定调用 -- SELECT auth.verify_user_login($1, $2);复盘时我意识到,真正的问题不只是权限本身,而是函数调用没有显式指定schema,使得PostgreSQL在不同search_path下解析到不同的对象,权限校验结果也随之改变。这种问题隐藏得很深,只在特定环境或特定用户登录时才复现,排查时需要同时关注权限和对象解析路径。
6.4 实例中的教训
这个实例反映出函数权限测试的三个关键检验点:
- 调用时是否使用schema限定的函数名。
- 函数是否处于用户的可见
search_path中。 - 权限授予是否存在且没有被后续REVOKE覆盖。
这三个检验点正好对应我在第3节和第4节中的“函数自身权限”、“函数体内权限”与“search_path解析”三方面内容。权限测试不只是跑通一个SQL就完事,需要把调用路径、角色上下文和对象解析都纳入验证范围。
我在实战中会把这三项做成一个简单的检查脚本,每次发布函数前都跑一遍:
-- 检查1:函数权限 SELECT has_function_privilege('app_user', 'auth.verify_user_login(text, text)', 'EXECUTE'); -- 检查2:底层表权限 SELECT has_table_privilege('app_user', 'auth.user_login_log', 'SELECT'); -- 检查3:函数依赖的序列权限 SELECT has_sequence_privilege('app_user', 'auth.user_login_log_id_seq', 'USAGE');三个检查项全部返回true,才说明权限配置完整。否则按对应项逐一补齐。
内容到这里,函数权限测试的关键链条已经全部打通:从函数自身的EXECUTE授权,到函数体内的对象权限,再到DEFINER模式的权限提升特性,最后是实际测试与排查技巧。如果严格按照这套方法走一遍,基本不会再被PostgreSQL函数权限问题困住。