Superadmin 权限自动分配触发器

PostgreSQL AFTER INSERT Trigger · 迁移 1779400000001
2026-02-11 · RBAC 统一架构

一、问题:人工分配必然遗漏

旧流程(依赖人类记忆)

每次开发者在新迁移中添加权限时,需要手动记住给 superadmin 角色也分配一份。

// 开发者写了一个新迁移,添加 time_clock 权限
// 给 employee、manager、admin 都分配了...
// 但忘了 platform_superadmin!

const adminPermissions = ['time_clock:punch', 'time_clock:pin_manage', ...];

// ✅ admin — 有
for (const perm of adminPermissions) {
  pgm.sql(`INSERT INTO role_permissions ... WHERE r.name = 'admin' ...`);
}

// ❌ platform_superadmin — 完全没提到!

之前这不是问题,因为后端中间件有 if (isSuperadminRole) return next() 直接绕过数据库查询。 但项目已切换到统一 RBAC,移除代码绕过后 superadmin 会立即丢失这些权限。

二、方案:数据库触发器自动分配

核心思路

permissions 表上创建 AFTER INSERT 触发器。每当新权限被插入,PostgreSQL 引擎自动将其分配给 superadmin 角色。开发者不需要做任何额外操作

权限层级规则

角色 获得范围 排除类别
platform_superadmin 全部权限
super_admin 全部权限 platformdeveloper

排除的具体权限:

三、触发器工作原理

场景:开发者新增 voice_bot:manage 权限

开发者的迁移 SQL │ ▼ ┌─────────────────────────────────────┐ │ INSERT INTO permissions │ ← 第 1 步:行被写入 permissions 表 │ ('voice_bot:manage', 'voice', ...) │ │ │ │ PostgreSQL 赋值: │ │ NEW.id = 87 │ │ NEW.name = 'voice_bot:manage' │ │ NEW.category = 'voice' │ └─────────────────────────────────────┘ │ ▼ AFTER INSERT 事件自动触发 ┌─────────────────────────────────────┐ │ fn_auto_grant_superadmin_permissions │ ← 第 2 步:触发器函数被调用 │ │ │ 函数拿到 NEW(刚插入的那一行) │ │ 可以直接读取 NEW 的任何列 │ └─────────────────────────────────────┘ │ ▼ 函数内部逻辑判断 ┌─────────────────────────────────────┐ │ ① platform_superadmin: │ │ category 无所谓 → 直接 INSERT │ │ role_permissions( │ │ platform_superadmin, │ │ permission_id=87 │ │ ) │ │ │ │ ② super_admin: │ │ category='voice' │ │ 不在排除列表 → INSERT │ │ role_permissions( │ │ super_admin, │ │ permission_id=87 │ │ ) │ └─────────────────────────────────────┘ │ ▼ 迁移结束,两个角色都自动获得了新权限

如果新增的是 tenants:audit(category=platform

① platform_superadmin: category 无所谓 → INSERT ✅ ② super_admin: category='platform' 在排除列表 → SKIP ⏭️

四、关键机制详解

NEW 隐式变量

PostgreSQL 触发器的核心概念。在 AFTER INSERT 触发器中,NEW 代表刚刚被插入的那一行的完整数据。通过 NEW.idNEW.category 可以直接访问该行的所有列值。

(对应地,在 UPDATE 触发器中还有 OLD 变量代表更新前的旧值。DELETE 触发器只有 OLD 没有 NEW。)

ON CONFLICT DO NOTHING 防御性设计

如果开发者在迁移中已经手动分配了权限(像以前那样),触发器再次尝试插入时会命中 UNIQUE(role_id, permission_id) 约束。DO NOTHING 让它静默跳过而不报错,避免迁移失败。

FOR EACH ROW 触发粒度

意味着每插入一行就执行一次函数。如果一个迁移一次性插入 10 个权限,触发器会执行 10 次,每次 NEW 指向不同的行。

性能影响可以忽略:权限表极少变动(仅在部署迁移时),每次只多两个 INSERT ... ON CONFLICT 小查询。

同一事务内执行

触发器在触发它的 INSERT 语句所在的同一个事务中执行。如果迁移回滚,触发器插入的 role_permissions 行也会一起回滚,保证数据一致性。

五、与其他方案的对比

维度 DB 触发器 当前方案 启动同步 代码绕过 已废弃
时机 INSERT 瞬间,同一事务内 服务重启时 每次权限检查时
可靠性 无法绕过 依赖服务重启 100%(但不可审计)
对开发者要求 零——只需设对 category 零——但需重启才生效
可审计性 数据库有完整记录 数据库有完整记录 无——权限在内存中
可撤销性 可以从 DB 删除特定权限 可以从 DB 删除 不可能——代码硬编码

六、完整触发器 SQL

-- 触发器函数
CREATE OR REPLACE FUNCTION fn_auto_grant_superadmin_permissions()
RETURNS trigger AS $$
DECLARE
  v_role_id INTEGER;
  v_excluded_categories TEXT[] := ARRAY['platform', 'developer'];
BEGIN
  -- platform_superadmin: 获得所有权限
  SELECT id INTO v_role_id
    FROM roles WHERE name = 'platform_superadmin' LIMIT 1;
  IF v_role_id IS NOT NULL THEN
    INSERT INTO role_permissions
      (role_id, permission_id, permission_level, is_active, granted_at)
    VALUES (v_role_id, NEW.id, 'manage', true, NOW())
    ON CONFLICT (role_id, permission_id) DO NOTHING;
  END IF;

  -- super_admin: 获得非 platform/developer 权限
  IF NEW.category IS NULL OR NOT
     (NEW.category = ANY(v_excluded_categories)) THEN
    SELECT id INTO v_role_id
      FROM roles WHERE name = 'super_admin' LIMIT 1;
    IF v_role_id IS NOT NULL THEN
      INSERT INTO role_permissions
        (role_id, permission_id, permission_level, is_active, granted_at)
      VALUES (v_role_id, NEW.id, 'manage', true, NOW())
      ON CONFLICT (role_id, permission_id) DO NOTHING;
    END IF;
  END IF;

  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 绑定触发器到 permissions 表
CREATE TRIGGER trg_auto_grant_superadmin
AFTER INSERT ON permissions
FOR EACH ROW
EXECUTE FUNCTION fn_auto_grant_superadmin_permissions();

七、开发者须知

新增权限迁移时你需要做什么?

什么都不需要做。

只需确保 INSERT INTO permissionscategory 值设置正确:

你仍然需要手动给 adminmanageremployee 等角色分配权限,触发器只负责 superadmin 层。

迁移文件: backend/database/migrations/1779400000001_auto-grant-superadmin-permissions.js