Selaa lähdekoodia

chore(系统): module/system 方法级授权清点 SQL 交付件(只读,待用户执行)

起因:查「角色权限抽屉全不勾」时量到 module/system/** 12 个 controller、
102 个端点,@PreAuthorize 出现 0 次;SecurityConfig 的授权规则只有
.anyRequest().authenticated(),全 main 树也没有授权用的 HandlerInterceptor
或 @Aspect。Python 版每条路由都挂 AuthPermission([...])(role 8/8、dict 16/16、
params 10/10……只有登录那批是 0)⇒ 平移时掉的,不是设计选择。

后果是具体路径不是泛泛的「权限不足」:sys_role / sys_menu / sys_role_menus
等表在 TenantInnerInterceptor 的 IGNORE_TABLES 里(不加租户谓词),所以普通
租户账号登录后可直接 PATCH /system/role/permission/setting 改写任意角色的
授权、或 DELETE /system/role/delete 删掉平台自己的角色。

为什么先清点而不是直接补注解:权限串若在 sys_menu 里不存在、或只存在于
status='1' 的行(@TableLogic 判成已删、进不了菜单树也就没法被勾选),就没有
任何角色能持有它 ⇒ 补上注解只会把非超管管理员锁在角色/用户管理之外。
超管不受影响(PermissionCheck 对 ROLE_SUPERUSER 直通),所以用超管跑的用例
一条都测不出这个缺陷。

三个结果集:① 现存的 module_system:% 权限串(含 status)② 各自授给了哪些角色
③ 期望清单里库中没有活行的串。期望清单 73 条由 `366311e^` 的 controller 机械
提取(不手抄),与 Python 版逐路由的 AuthPermission 一一对应。

只读性做了两向量:本文件行首写操作关键字 0 命中;同一把尺量 har-menu.sql
命中 5 ⇒ 是有对照的负读数,不是检索式造的。
alphaH 2 viikkoa sitten
vanhempi
sitoutus
a5f2c9e7b8
1 muutettua tiedostoa jossa 147 lisäystä ja 0 poistoa
  1. 147 0
      java/src/main/resources/db/sys-authz-inventory-readonly.sql

+ 147 - 0
java/src/main/resources/db/sys-authz-inventory-readonly.sql

@@ -0,0 +1,147 @@
+-- module/system 方法级授权 —— 清点(**只读**,2026-09-28)
+--
+-- 【本脚本只读】没有 INSERT / UPDATE / DELETE / DDL,跑它不改任何数据。
+--   执行由用户做,把三个结果集贴回即可。
+--
+-- ── 为什么要清点 ─────────────────────────────────────────────────────
+-- java 侧 module/system/** 共 12 个 controller、**102 个端点,@PreAuthorize 出现 0 次**;
+-- SecurityConfig 的授权规则只有 .anyRequest().authenticated()(仅认证,无 hasAuthority),
+-- 全 main 树也没有授权用的 HandlerInterceptor(唯一的 OperationLogInterceptor 只管日志)、
+-- 没有 @Aspect。Python 版不是这样:`366311e^` 时每条路由都挂
+-- AuthPermission([...]) ⇒ 这是**平移时掉的**,不是设计选择。
+--
+-- 后果是具体的:sys_role / sys_menu / sys_role_menus / sys_dept / sys_position /
+-- sys_dict_* / sys_param / sys_notice 全在 TenantInnerInterceptor 的 IGNORE_TABLES 里
+-- (不加租户谓词)⇒ 一个普通租户账号登录后可以直接
+--   PATCH  /system/role/permission/setting   改写**任意**角色的授权
+--   DELETE /system/role/delete               删掉平台自己的角色
+-- (POST /system/user/create 那条走不通:UserService#createUser 显式拦了 is_superuser。)
+--
+-- ── 为什么不能直接补注解 ─────────────────────────────────────────────
+-- 权限串若在 sys_menu 里**不存在**(或所在行 status='1',被 @TableLogic 判成已删),
+-- 就没有任何角色能持有它 ⇒ 补上注解后,**除超管外全部被锁在角色/用户管理之外**
+-- (超管不受影响:PermissionCheck 对 ROLE_SUPERUSER 直通)。所以先量,再改代码。
+--
+-- ── 三个结果集 ───────────────────────────────────────────────────────
+-- ① sys_menu 里现存的 module_system:% 权限串 —— 连同 status('0' 才是活的)
+-- ② 这些串分别授给了哪些角色 —— 判断「改了会锁住谁」
+-- ③ 期望清单里**库里没有**(或只有 status='1' 的行)的串 —— 这些端点不能照搬注解
+
+
+-- ① 现存的 module_system:% 权限串
+SELECT m.id,
+       m.parent_id,
+       m.type,
+       m.status,
+       m.permission,
+       m.name,
+       m.title
+  FROM sys_menu m
+ WHERE m.permission IS NOT NULL
+   AND m.permission <> ''
+   AND m.permission LIKE 'module_system:%'
+ ORDER BY m.permission, m.id;
+
+
+-- ② 这些串分别授给了哪些角色(menu_cnt 是行数,perms 是串本身)
+SELECT rm.role_id,
+       r.name  AS role_name,
+       r.status AS role_status,
+       count(*) AS menu_cnt,
+       string_agg(DISTINCT m.permission, ', ' ORDER BY m.permission) AS perms
+  FROM sys_role_menus rm
+  JOIN sys_role r ON r.id = rm.role_id
+  JOIN sys_menu m ON m.id = rm.menu_id
+ WHERE m.permission LIKE 'module_system:%'
+ GROUP BY rm.role_id, r.name, r.status
+ ORDER BY rm.role_id;
+
+
+-- ③ 期望清单里库中没有活行的串(73 个,取自 Python 版每条路由的 AuthPermission)
+--    这些串**没有**任何角色能持有 ⇒ 对应端点直接照搬注解就会锁人,要单独裁定。
+--    status 过滤与 MenuMapper.selectList(@TableLogic)对齐:'1' 的行不出现在菜单树里,
+--    也就没法被勾选授权。
+WITH expected(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: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')
+)
+SELECT e.permission AS missing_or_dead_permission
+  FROM expected e
+ WHERE NOT EXISTS (
+        SELECT 1 FROM sys_menu m
+         WHERE m.permission = e.permission
+           AND m.status = '0'
+       )
+ ORDER BY e.permission;