diff --git a/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/RbacPermissionMigrationContractTest.java b/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/RbacPermissionMigrationContractTest.java index 88ad7aa43..a0bc36b5e 100644 --- a/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/RbacPermissionMigrationContractTest.java +++ b/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/RbacPermissionMigrationContractTest.java @@ -132,6 +132,61 @@ public class RbacPermissionMigrationContractTest "select 5, 2240"); } + @Test + public void teacherAndStudentSafeDeletePermissionsRegisteredInSqlAndContractProtected() throws Exception + { + // 1. 验证权威初始化 SQL (005) 包含教师与学员安全删除按钮权限注册 + String sql005 = new String(Files.readAllBytes(findMigration("005_training_roles_permissions.sql")), StandardCharsets.UTF_8).toLowerCase(); + assertContains(sql005, + "train:teacher:remove", + "train:student:remove", + "2245", + "2246", + "教师删除", + "学员删除"); + + // 2. 验证增量 V2 migration (011) 包含幂等安全删除注册 + Path incrementalMigrationPath = findMigration("011_teacher_student_remove_permissions.sql"); + String sql011 = new String(Files.readAllBytes(incrementalMigrationPath), StandardCharsets.UTF_8).toLowerCase(); + assertContains(sql011, + "train:teacher:remove", + "train:student:remove", + "2245", + "2246", + "where not exists", + "train_admin"); + + // 3. 严格断言并未向 train_admin 授予教师/学员删除权限 + assertFalse("train_admin 绝不能被授予教师删除权限", sql005.contains("select 3, 2245")); + assertFalse("train_admin 绝不能被授予学员删除权限", sql005.contains("select 3, 2246")); + assertFalse("011迁移绝不能向sys_role_menu赋予train_admin删除权限", sql011.contains("insert into public.sys_role_menu (role_id, menu_id) values (3")); + + // 4. 验证后端 Controller 注解与权限树完全对齐 + boolean teacherRemoveFound = false; + for (Method method : TrainTeacherController.class.getDeclaredMethods()) + { + if (method.isAnnotationPresent(PreAuthorize.class) + && method.getAnnotation(PreAuthorize.class).value().contains("train:teacher:remove")) + { + teacherRemoveFound = true; + break; + } + } + assertTrue("TrainTeacherController 必须配置 train:teacher:remove", teacherRemoveFound); + + boolean studentRemoveFound = false; + for (Method method : TrainStudentController.class.getDeclaredMethods()) + { + if (method.isAnnotationPresent(PreAuthorize.class) + && method.getAnnotation(PreAuthorize.class).value().contains("train:student:remove")) + { + studentRemoveFound = true; + break; + } + } + assertTrue("TrainStudentController 必须配置 train:student:remove", studentRemoveFound); + } + private Path findMigration(String filename) { for (Path path = Paths.get("").toAbsolutePath(); path != null; path = path.getParent()) diff --git a/RuoYi-Vue/sql/system/kingbase/005_training_roles_permissions.sql b/RuoYi-Vue/sql/system/kingbase/005_training_roles_permissions.sql index d08afcebe..79142cc5b 100644 --- a/RuoYi-Vue/sql/system/kingbase/005_training_roles_permissions.sql +++ b/RuoYi-Vue/sql/system/kingbase/005_training_roles_permissions.sql @@ -112,6 +112,10 @@ INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, com SELECT 2242, '教师账号解绑', 2155, 6, '#', '', '', 1, 0, 'F', '0', '0', 'train:teacher:unbindUser', '#', 'admin', CURRENT_TIMESTAMP, '教师系统账号解绑' WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2242 OR perms = 'train:teacher:unbindUser'); +INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2245, '教师删除', 2155, 7, '#', '', '', 1, 0, 'F', '0', '0', 'train:teacher:remove', '#', 'admin', CURRENT_TIMESTAMP, '教师档案安全删除' +WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2245 OR perms = 'train:teacher:remove'); + -- 3.2 学员管理 (parent: 2156) INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) SELECT 2205, '学员查询', 2156, 1, '#', '', '', 1, 0, 'F', '0', '0', 'train:student:list', '#', 'admin', CURRENT_TIMESTAMP, '学员列表查询' @@ -133,6 +137,10 @@ INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, com SELECT 2244, '学员账号解绑', 2156, 5, '#', '', '', 1, 0, 'F', '0', '0', 'train:student:unbindUser', '#', 'admin', CURRENT_TIMESTAMP, '学员系统账号解绑' WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2244 OR perms = 'train:student:unbindUser'); +INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2246, '学员删除', 2156, 6, '#', '', '', 1, 0, 'F', '0', '0', 'train:student:remove', '#', 'admin', CURRENT_TIMESTAMP, '学员档案安全删除' +WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2246 OR perms = 'train:student:remove'); + -- 3.3 教室管理 (parent: 2157) INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) SELECT 2208, '教室查询', 2157, 1, '#', '', '', 1, 0, 'F', '0', '0', 'train:classroom:list', '#', 'admin', CURRENT_TIMESTAMP, '教室列表查询' @@ -326,6 +334,14 @@ BEGIN RAISE EXCEPTION 'Student binding permission missing'; END IF; + IF NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE perms = 'train:teacher:remove') THEN + RAISE EXCEPTION 'Teacher safe delete permission missing'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE perms = 'train:student:remove') THEN + RAISE EXCEPTION 'Student safe delete permission missing'; + END IF; + IF EXISTS (SELECT 1 FROM public.sys_menu WHERE perms LIKE 'system:board:%') THEN RAISE EXCEPTION 'Legacy system:board:* permission namespace still exists'; END IF; diff --git a/RuoYi-Vue/sql/system/kingbase/011_teacher_student_remove_permissions.sql b/RuoYi-Vue/sql/system/kingbase/011_teacher_student_remove_permissions.sql new file mode 100644 index 000000000..08c5ad5fc --- /dev/null +++ b/RuoYi-Vue/sql/system/kingbase/011_teacher_student_remove_permissions.sql @@ -0,0 +1,71 @@ +-- ============================================================================= +-- 脚本名称: 011_teacher_student_remove_permissions.sql +-- 业务模块: 教师与学员零引用安全删除权限节点注册 (ZERO-REFERENCE SAFE DELETE RBAC) +-- 适用数据库: KingbaseES, schema public +-- 范围: 仅限 sys_menu 按钮级权限增量注册,不改动已有角色分配 (sys_role_menu Δ = 0) +-- 幂等性设计: 支持重复执行,WHERE NOT EXISTS 防重,无破坏性语句 +-- 安全红线: +-- 1. 严禁执行 007_dicts.sql +-- 2. 严禁清空/重建 sys_menu +-- 3. 严禁重置角色权限 +-- 4. 严禁给 train_admin (role_id = 3) 自动赋权,保持 TRAIN_ADMIN_*_REMOVE = NO +-- ============================================================================= + +BEGIN; + +-- 前置碰撞检查 +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM public.sys_menu + WHERE menu_id = 2245 AND perms <> 'train:teacher:remove' + ) THEN + RAISE EXCEPTION 'Menu ID 2245 collides with an existing menu item'; + END IF; + + IF EXISTS ( + SELECT 1 FROM public.sys_menu + WHERE menu_id = 2246 AND perms <> 'train:student:remove' + ) THEN + RAISE EXCEPTION 'Menu ID 2246 collides with an existing menu item'; + END IF; +END $$; + +-- 1. 教师删除按钮权限 (train:teacher:remove, parent: 2155 教师管理) +INSERT INTO public.sys_menu + (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, + menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2245, '教师删除', 2155, 7, '#', '', '', 1, 0, + 'F', '0', '0', 'train:teacher:remove', '#', 'admin', CURRENT_TIMESTAMP, '教师档案安全删除' +WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE perms = 'train:teacher:remove'); + +-- 2. 学员删除按钮权限 (train:student:remove, parent: 2156 学员管理) +INSERT INTO public.sys_menu + (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, + menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2246, '学员删除', 2156, 6, '#', '', '', 1, 0, + 'F', '0', '0', 'train:student:remove', '#', 'admin', CURRENT_TIMESTAMP, '学员档案安全删除' +WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE perms = 'train:student:remove'); + +-- 3. 后置自检验收 (断言权限存在且唯一,且未向 train_admin 越权授权) +DO $$ +BEGIN + IF (SELECT count(*) FROM public.sys_menu WHERE perms = 'train:teacher:remove' AND parent_id = 2155 AND menu_type = 'F') <> 1 THEN + RAISE EXCEPTION 'Teacher remove permission registration verification failed'; + END IF; + + IF (SELECT count(*) FROM public.sys_menu WHERE perms = 'train:student:remove' AND parent_id = 2156 AND menu_type = 'F') <> 1 THEN + RAISE EXCEPTION 'Student remove permission registration verification failed'; + END IF; + + -- 严格断言 train_admin (role_id = 3) 未被分配这两个权限 + IF EXISTS ( + SELECT 1 FROM public.sys_role_menu rm + JOIN public.sys_menu m ON rm.menu_id = m.menu_id + WHERE rm.role_id = 3 AND m.perms IN ('train:teacher:remove', 'train:student:remove') + ) THEN + RAISE EXCEPTION 'FATAL: train_admin must not be granted teacher/student remove permissions'; + END IF; +END $$; + +COMMIT;