PostgreSQL 的事件触发器非常强大,但它们通常只能由超级用户创建。在云环境中,授予超级用户权限并不可行。
幸运的是,得益于 PostgreSQL 的扩展性,我们可以以安全的方式允许普通用户创建事件触发器。
本文将介绍我们在 supautils 扩展中是如何实现这一功能的,方法是结合使用 Utility Hook 和 Function Manager Hook。
特权角色
Utility Hook
supautils 的核心是“特权角色”,该角色作为超级用户的代理,提供了一部分安全的超级用户权限,普通用户可以访问这个角色。
当特权角色创建事件触发器时,我们会通过 Utility Hook (即 ProcessUtility_hook)拦截该语句。在这里,我们将角色提升为超级用户,继续正常流程,并允许事件触发器在 PostgreSQL 核心中创建。最后,我们将角色降级回特权角色,并将事件触发器的所有权归于它。
然而,这种方式并不完全安全,因为它可能导致特权提升。
特权提升问题
问题出现在事件触发器创建后:
- 它会针对所有角色进行触发。
- 它会使用目标角色的权限执行。
这意味着恶意用户可以创建类似以下的事件触发器:
create or replace function become_super()
returns event_trigger
language plpgsql as
$$
begin
alter role malicious SUPERUSER;
end;
$$;
create event trigger bad_event_trigger on ddl_command_end
execute procedure become_super();
一旦超级用户触发了这个事件触发器,它会以超级用户的权限执行,从而将恶意用户提升为超级用户。
跳过事件触发器
FMGR Hook
为了解决这个问题,我们可以通过跳过超级用户的事件触发器来避免这种特权提升。
Function Manager Hook(即 fmgr_hook)允许我们拦截和修改函数的执行。
我们可以拦截事件触发器函数,并将其替换为一个“无操作”(noop)函数。虽然 PostgreSQL 没有提供一个内建的 noop 函数,但我们可以使用现有的 version() 函数来实现相同的效果。
除了超级用户,我们还希望跳过“保留角色”的事件触发器。这些角色通常用于管理服务(如 pgbouncer)。
安全的用户事件触发器
通过上述方法,用户可以在没有超级用户权限的情况下安全地创建事件触发器:
-- 使用特权角色,这里特权角色配置为“postgres”
set role postgres;
select current_setting('is_superuser'); -- 确认当前不是超级用户
current_setting
-----------------
off
(1 row)
-- 现在创建事件触发器
create function show_current_user()
returns event_trigger as $$
begin
raise notice '事件触发器执行于 %', current_user;
end;
$$ language plpgsql;
create event trigger myevtrig on ddl_command_end
execute procedure show_current_user();
-- 检查是否成功执行
create table foo();
NOTICE: 事件触发器执行于 postgres
set role myrole;
create table bar();
NOTICE: 事件触发器执行于 myrole
PostgreSQL 核心中的未来
我们也希望在 PostgreSQL 核心中允许普通用户创建事件触发器。为此,我们已经提交了一些补丁,目前正在进行有益的讨论。
请注意,PostgreSQL 核心中的用户事件触发器可能会比 supautils 版本更受限制。
尝试使用
用户事件触发器现在已经可以在 Supabase 平台的最新项目中使用。
你也可以通过 git clone 获取 supautils 仓库,并在自己的部署中进行安装。
最后,我们要特别感谢 Zero Sync 团队,他们推动了我们发布这一功能。
备注
- 这样做是为了让事件触发器可以被最终用户修改或删除。
- 如果你将事件触发器函数标记为
security definer,它将以函数所有者的权限执行。但这通常不适用于事件触发器,因为事件触发器通常希望保持当前用户的上下文。 - 这些角色是可配置的。你可以在这里阅读更多关于保留角色的信息。