|
|
@@ -0,0 +1,248 @@
|
|
|
+-- module/system 方法级授权 —— 配套授权(**写数据**,2026-09-28)
|
|
|
+--
|
|
|
+-- 【执行顺序 —— 这是本文件最重要的一句】
|
|
|
+-- 先在**旧后端**上跑完本文件,再部署带 @PreAuthorize 的新后端。
|
|
|
+-- 反过来做的话,从新后端上线到本文件跑完之间的这段时间里,
|
|
|
+-- **除超管外的所有账号都会被挡在「系统管理」之外** —— 包括分配权限那个
|
|
|
+-- PATCH /system/role/permission/setting,也就是自己把自己锁在门外的死锁:
|
|
|
+-- 被锁的账号没有 role:permission 就进不去授权页,进不去就授不了权。
|
|
|
+-- 超管不受影响(PermissionCheck 对 ROLE_SUPERUSER 直通),所以只要还有超管能登,
|
|
|
+-- 锁了也能救回来;但别指望这一点。
|
|
|
+--
|
|
|
+-- ── 背景 ─────────────────────────────────────────────────────────────
|
|
|
+-- java 侧 module/system/** 此前 102 个端点、@PreAuthorize 出现 0 次
|
|
|
+-- (SecurityConfig 只有 .anyRequest().authenticated()),现已补齐 84 个端点。
|
|
|
+-- 守卫按 sys_menu.permission 的权限串匹配,authorities 的来路是
|
|
|
+-- sys_user_roles → sys_role_menus → sys_menu.permission
|
|
|
+-- (UserDetailsServiceImpl#buildLoginUser,MenuMapper#selectPermissionsByUserId)。
|
|
|
+-- 所以**串存在但没授给任何角色 = 该端点对所有非超管永久 403**,且按钮也不会出现。
|
|
|
+-- 清点读数(sys-authz-inventory-readonly.sql ①②)显示:
|
|
|
+-- 角色 1「管理员角色」只持有 2 个 module_system 菜单,角色 2「租户角色」5 个,
|
|
|
+-- 73 条核心串**一条都没授给任何角色** —— 不补授权就上守卫 = 锁人。
|
|
|
+--
|
|
|
+-- ── 本文件做什么 ─────────────────────────────────────────────────────
|
|
|
+-- §0 只读核对(不改数据,先跑,把读数贴回)
|
|
|
+-- §1 补 2 个权限点:module_system:invitation:create / :delete
|
|
|
+-- §2 把 module_system 全量菜单 + 其祖先目录授给角色 1
|
|
|
+-- §3 回读核对(应返回 0 行)
|
|
|
+-- §4 回滚
|
|
|
+--
|
|
|
+-- 为什么 §2 要连**祖先目录**一起授:UserService#buildMenuTree 对父节点不在
|
|
|
+-- 授权集合里的行是**静默丢弃**(既不进 tree 也不挂到父上),只授子节点 = 菜单看不见。
|
|
|
+
|
|
|
+
|
|
|
+-- ═══════════════════ §0 只读核对(不改任何数据)═══════════════════
|
|
|
+
|
|
|
+-- ⓪-A 谁持有角色 1 / 谁是超管。**这一条会决定 §2 是否安全**:
|
|
|
+-- 若角色 1 被某个**租户**账号持有,那么把整个 module_system 授给角色 1
|
|
|
+-- 等于把平台管理权交给那个租户(本次守卫对它也就不构成防线)。
|
|
|
+-- 若读数里出现 tenant_id 非空的角色 1 持有者,先停下,改授给平台自己的角色。
|
|
|
+SELECT u.id,
|
|
|
+ u.username,
|
|
|
+ u.is_superuser,
|
|
|
+ u.status,
|
|
|
+ u.tenant_id,
|
|
|
+ (SELECT string_agg(r.id || ':' || r.name, ', ' ORDER BY r.id)
|
|
|
+ FROM sys_user_roles ur JOIN sys_role r ON r.id = ur.role_id
|
|
|
+ WHERE ur.user_id = u.id) AS roles
|
|
|
+ FROM sys_user u
|
|
|
+ WHERE u.is_superuser IS TRUE
|
|
|
+ OR EXISTS (SELECT 1 FROM sys_user_roles ur WHERE ur.user_id = u.id AND ur.role_id = 1)
|
|
|
+ ORDER BY u.is_superuser DESC NULLS LAST, u.id;
|
|
|
+
|
|
|
+-- ⓪-B 角色 1、2 当前**完整**的已授权菜单行(含目录)。② 那张表只看了
|
|
|
+-- permission LIKE 'module_system:%' 的行,看不到祖先目录有没有授,
|
|
|
+-- 而祖先目录恰好决定菜单树渲染,所以这里要全量看一遍。
|
|
|
+-- **请把这次的输出留一份**,§4 回滚要用它比对。
|
|
|
+SELECT rm.role_id,
|
|
|
+ m.id,
|
|
|
+ m.parent_id,
|
|
|
+ m.type,
|
|
|
+ m.status,
|
|
|
+ m.name,
|
|
|
+ m.title,
|
|
|
+ m.permission
|
|
|
+ FROM sys_role_menus rm
|
|
|
+ JOIN sys_menu m ON m.id = rm.menu_id
|
|
|
+ WHERE rm.role_id IN (1, 2)
|
|
|
+ ORDER BY rm.role_id, m.id;
|
|
|
+
|
|
|
+
|
|
|
+-- ═══════════════════ §1 补两个权限点(幂等)═══════════════════
|
|
|
+-- 前端 frontend/src/views/module_system/invitation/index.vue 用的是
|
|
|
+-- module_system:invitation:create(第 15 行,:perm-create)
|
|
|
+-- module_system:invitation:delete(第 16 行 :perm-delete,第 120 行 v-hasPerm)
|
|
|
+-- 而 sys_menu 里只有 module_system:invitation:list(清点 ① 第 23 行,id 243)。
|
|
|
+-- 不补这两行的话:挂上守卫后**没有任何角色能持有这两个串** ⇒ 生成/删除邀请码
|
|
|
+-- 对非超管永久 403,且页面上的按钮永远不出现(v-hasPerm 是前端按权限串显示)。
|
|
|
+-- 列形与 har-menu.sql 的权限点段一致;type=3 = 按钮,不进菜单树、只作权限串。
|
|
|
+INSERT INTO sys_menu (parent_id, name, title, permission, type, "order",
|
|
|
+ hidden, keep_alive, always_show, affix, status, created_time, updated_time)
|
|
|
+SELECT p.id, v.name, v.title, v.permission, 3, v.ord,
|
|
|
+ false, false, false, false, '0', now(), now()
|
|
|
+ FROM (VALUES
|
|
|
+ ('创建邀请码', '创建邀请码', 'module_system:invitation:create', 1),
|
|
|
+ ('删除邀请码', '删除邀请码', 'module_system:invitation:delete', 2)
|
|
|
+ ) AS v(name, title, permission, ord)
|
|
|
+ CROSS JOIN (SELECT id FROM sys_menu
|
|
|
+ WHERE permission = 'module_system:invitation:list'
|
|
|
+ ORDER BY id LIMIT 1) p
|
|
|
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu m WHERE m.permission = v.permission);
|
|
|
+
|
|
|
+
|
|
|
+-- ═══════════════════ §2a 快照(为了 §4 能精确回滚)═══════════════════
|
|
|
+-- 建一张一次性备份表。跑第二次不会覆盖第一次的快照(IF NOT EXISTS)。
|
|
|
+CREATE TABLE IF NOT EXISTS _bak_role_menus_role1 AS
|
|
|
+SELECT * FROM sys_role_menus WHERE role_id = 1;
|
|
|
+
|
|
|
+
|
|
|
+-- ═══════════════════ §2 把 module_system 全量授给角色 1 ═══════════════════
|
|
|
+-- seed = 所有 status='0' 且 permission LIKE 'module_system:%' 的行(含 type=3 按钮)
|
|
|
+-- closure = seed ∪ 它们的全部祖先(目录),保证菜单树能渲染
|
|
|
+-- 幂等:只插 sys_role_menus 里还没有的 (1, menu_id)。
|
|
|
+WITH RECURSIVE seed AS (
|
|
|
+ SELECT m.id, m.parent_id
|
|
|
+ FROM sys_menu m
|
|
|
+ WHERE m.status = '0'
|
|
|
+ AND m.permission LIKE 'module_system:%'
|
|
|
+),
|
|
|
+closure AS (
|
|
|
+ SELECT id, parent_id FROM seed
|
|
|
+ UNION
|
|
|
+ SELECT m.id, m.parent_id
|
|
|
+ FROM sys_menu m
|
|
|
+ JOIN closure c ON m.id = c.parent_id
|
|
|
+)
|
|
|
+INSERT INTO sys_role_menus (role_id, menu_id)
|
|
|
+SELECT 1, c.id
|
|
|
+ FROM closure c
|
|
|
+ WHERE NOT EXISTS (SELECT 1 FROM sys_role_menus rm
|
|
|
+ WHERE rm.role_id = 1 AND rm.menu_id = c.id);
|
|
|
+
|
|
|
+-- 角色 2「租户角色」本次**不动**:它现有的 5 个菜单
|
|
|
+-- (module_system:tenant:api-key:query/create/delete、tenant:conf:query/update)
|
|
|
+-- 对应的端点在 payment 模块的 /payment/api-key,不在本批 84 个守卫里,
|
|
|
+-- 所以本次改动对它没有任何影响,不需要补授权。
|
|
|
+
|
|
|
+
|
|
|
+-- ═══════════════════ §3 回读核对(应返回 0 行)═══════════════════
|
|
|
+-- 清单由 java/src/test/.../SystemEndpointAuthzTest.java 机械提取(84 个受守卫端点
|
|
|
+-- 去重后的 76 个权限串),不是手抄。返回 0 行 = 角色 1 已持有每一个受守卫端点所需的串。
|
|
|
+-- 判据按「串」而不是按「行」:同一个串可能挂在多行 sys_menu 上(如 dept:query 同时
|
|
|
+-- 在 id 12 和 id 47),只要持有其中任意一行就算满足。
|
|
|
+WITH required(permission) AS (
|
|
|
+ VALUES
|
|
|
+ ('module_system:dept:create'),
|
|
|
+ ('module_system:dept:delete'),
|
|
|
+ ('module_system:dept:detail'),
|
|
|
+ ('module_system:dept:patch'),
|
|
|
+ ('module_system:dept:query'),
|
|
|
+ ('module_system:dept:update'),
|
|
|
+ ('module_system:dict_data:create'),
|
|
|
+ ('module_system:dict_data:delete'),
|
|
|
+ ('module_system:dict_data:detail'),
|
|
|
+ ('module_system:dict_data:export'),
|
|
|
+ ('module_system:dict_data:patch'),
|
|
|
+ ('module_system:dict_data:query'),
|
|
|
+ ('module_system:dict_data:update'),
|
|
|
+ ('module_system:dict_type:create'),
|
|
|
+ ('module_system:dict_type:delete'),
|
|
|
+ ('module_system:dict_type:detail'),
|
|
|
+ ('module_system:dict_type:export'),
|
|
|
+ ('module_system:dict_type:patch'),
|
|
|
+ ('module_system:dict_type:query'),
|
|
|
+ ('module_system:dict_type:update'),
|
|
|
+ ('module_system:invitation:create'),
|
|
|
+ ('module_system:invitation:delete'),
|
|
|
+ ('module_system:invitation:list'),
|
|
|
+ ('module_system:log:delete'),
|
|
|
+ ('module_system:log:detail'),
|
|
|
+ ('module_system:log:export'),
|
|
|
+ ('module_system:log:query'),
|
|
|
+ ('module_system:menu:create'),
|
|
|
+ ('module_system:menu:delete'),
|
|
|
+ ('module_system:menu:detail'),
|
|
|
+ ('module_system:menu:patch'),
|
|
|
+ ('module_system:menu:query'),
|
|
|
+ ('module_system:menu:update'),
|
|
|
+ ('module_system:notice:create'),
|
|
|
+ ('module_system:notice:delete'),
|
|
|
+ ('module_system:notice:detail'),
|
|
|
+ ('module_system:notice:export'),
|
|
|
+ ('module_system:notice:patch'),
|
|
|
+ ('module_system:notice:query'),
|
|
|
+ ('module_system:notice:update'),
|
|
|
+ ('module_system:param:create'),
|
|
|
+ ('module_system:param:delete'),
|
|
|
+ ('module_system:param:detail'),
|
|
|
+ ('module_system:param:export'),
|
|
|
+ ('module_system:param:query'),
|
|
|
+ ('module_system:param:update'),
|
|
|
+ ('module_system:param:upload'),
|
|
|
+ ('module_system:position:create'),
|
|
|
+ ('module_system:position:delete'),
|
|
|
+ ('module_system:position:detail'),
|
|
|
+ ('module_system:position:export'),
|
|
|
+ ('module_system:position:patch'),
|
|
|
+ ('module_system:position:query'),
|
|
|
+ ('module_system:position:update'),
|
|
|
+ ('module_system:role:create'),
|
|
|
+ ('module_system:role:delete'),
|
|
|
+ ('module_system:role:detail'),
|
|
|
+ ('module_system:role:export'),
|
|
|
+ ('module_system:role:patch'),
|
|
|
+ ('module_system:role:permission'),
|
|
|
+ ('module_system:role:query'),
|
|
|
+ ('module_system:role:update'),
|
|
|
+ ('module_system:tenant:create'),
|
|
|
+ ('module_system:tenant:delete'),
|
|
|
+ ('module_system:tenant:patch'),
|
|
|
+ ('module_system:tenant:query'),
|
|
|
+ ('module_system:tenant:update'),
|
|
|
+ ('module_system:user:create'),
|
|
|
+ ('module_system:user:delete'),
|
|
|
+ ('module_system:user:detail'),
|
|
|
+ ('module_system:user:download'),
|
|
|
+ ('module_system:user:export'),
|
|
|
+ ('module_system:user:import'),
|
|
|
+ ('module_system:user:patch'),
|
|
|
+ ('module_system:user:query'),
|
|
|
+ ('module_system:user:update')
|
|
|
+),
|
|
|
+held AS (
|
|
|
+ SELECT DISTINCT m.permission
|
|
|
+ FROM sys_menu m
|
|
|
+ JOIN sys_role_menus rm ON rm.menu_id = m.id AND rm.role_id = 1
|
|
|
+ WHERE m.permission IS NOT NULL
|
|
|
+),
|
|
|
+live AS (
|
|
|
+ SELECT DISTINCT permission FROM sys_menu
|
|
|
+ WHERE status = '0' AND permission IS NOT NULL
|
|
|
+),
|
|
|
+any_row AS (
|
|
|
+ SELECT DISTINCT permission FROM sys_menu WHERE permission IS NOT NULL
|
|
|
+)
|
|
|
+SELECT r.permission,
|
|
|
+ CASE
|
|
|
+ WHEN NOT EXISTS (SELECT 1 FROM any_row a WHERE a.permission = r.permission)
|
|
|
+ THEN '库里根本没有这个串'
|
|
|
+ WHEN NOT EXISTS (SELECT 1 FROM live l WHERE l.permission = r.permission)
|
|
|
+ THEN '有行但都不是 status=0(权限树里勾不到)'
|
|
|
+ ELSE '有行、但没授给角色 1'
|
|
|
+ END AS verdict
|
|
|
+ FROM required r
|
|
|
+ WHERE NOT EXISTS (SELECT 1 FROM held h WHERE h.permission = r.permission)
|
|
|
+ ORDER BY r.permission;
|
|
|
+
|
|
|
+
|
|
|
+-- ═══════════════════ §4 回滚 ═══════════════════
|
|
|
+-- 只回滚 §2(§1 加的两行权限点是新增数据,不改旧行为,可以留着)。
|
|
|
+-- §2a 的快照存的是「§2 之前」的角色 1 授权集合,所以「§2 新增的」= 现在多出来的那些。
|
|
|
+-- 需要时执行:
|
|
|
+--
|
|
|
+-- DELETE FROM sys_role_menus
|
|
|
+-- WHERE role_id = 1
|
|
|
+-- AND menu_id NOT IN (SELECT menu_id FROM _bak_role_menus_role1);
|
|
|
+--
|
|
|
+-- 然后与 §0-B 留的那份输出比对,确认回到原样,再删快照表:
|
|
|
+-- DROP TABLE _bak_role_menus_role1;
|