PostgreSQL 无超级用户权限创建事件触发器

72 阅读3分钟

PostgreSQL 的事件触发器非常强大,但它们通常只能由超级用户创建。在云环境中,授予超级用户权限并不可行。

幸运的是,得益于 PostgreSQL 的扩展性,我们可以以安全的方式允许普通用户创建事件触发器。

本文将介绍我们在 supautils 扩展中是如何实现这一功能的,方法是结合使用 Utility HookFunction 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 团队,他们推动了我们发布这一功能。


备注

  1. 这样做是为了让事件触发器可以被最终用户修改或删除。
  2. 如果你将事件触发器函数标记为 security definer,它将以函数所有者的权限执行。但这通常不适用于事件触发器,因为事件触发器通常希望保持当前用户的上下文。
  3. 这些角色是可配置的。你可以在这里阅读更多关于保留角色的信息。