1.0.497.sql 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. -- =====================================================================================
  2. -- 1.0.497 S8-ACTION-PERMISSION-1 · 异常动作授权矩阵
  3. --
  4. -- 背景(真库取证,只读):
  5. -- 平台的权限解析链 SysUserRole → SysRoleMenu → SysMenu(Btn) 只对**菜单**做了租户校验
  6. -- (InnerJoin SysTenantMenu ... t.TenantId == 当前租户),**对角色没有**;
  7. -- SysUserRole 本身也不要求用户与角色同租户。
  8. --
  9. -- 实测后果:账号 UATAdminA(TenantId = 838257186181189)被授予了角色
  10. -- ROLE_S6_IPQC_SUPERVISOR,而该角色 SysRole.TenantId = 1300000000001(默认租户)。
  11. -- 它由此获得 s8:exception:assign / claim / close / reject / upgrade ——
  12. -- 这些能力在它自己的租户里**没有任何角色授予**。同租户的 UATExceptionA 调转派得 403,
  13. -- UATAdminA 却成功,差别完全来自那个别家租户的角色。
  14. --
  15. -- 本次动作:把「谁能对异常单做什么」收回 S8 自己回答,tenant_id 成为行上的一等字段,
  16. -- 跨租户在结构上不成立。授权矩阵的初值由启动期 Provisioning 从**本租户既有**
  17. -- SysRoleMenu 语义推导(跨租户角色不带入),推不出角色的动作保持空 = fail closed。
  18. --
  19. -- 表结构本身由 CodeFirst(InitTables) 负责创建;本脚本只做:
  20. -- · 幂等地补齐唯一索引(CodeFirst 对既有表不一定补索引)
  21. -- · postcheck 断言
  22. -- 不在此写任何授权数据 —— 授权是 per-tenant 推导结果,写死在迁移里无法适配新租户。
  23. -- =====================================================================================
  24. -- ─────────────────────────────────────────────────────────────────────────────────────
  25. -- PHASE 1 · PREFLIGHT
  26. -- ─────────────────────────────────────────────────────────────────────────────────────
  27. CREATE TABLE IF NOT EXISTS `ado_s8_exception_action_role` (
  28. `id` BIGINT NOT NULL AUTO_INCREMENT,
  29. `tenant_id` BIGINT NOT NULL COMMENT '租户(唯一安全边界)',
  30. `action_code` VARCHAR(64) NOT NULL COMMENT '动作码,取值须在 S8ExceptionActionCatalog 内',
  31. `role_id` BIGINT NOT NULL COMMENT '被授权角色 SysRole.Id,必须属于 tenant_id',
  32. `created_at` DATETIME NOT NULL,
  33. `created_by` VARCHAR(64) NULL,
  34. PRIMARY KEY (`id`)
  35. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S8 异常动作角色授权';
  36. -- ─────────────────────────────────────────────────────────────────────────────────────
  37. -- PHASE 2 · DIAGNOSTICS
  38. -- 把「本次迁移之前,有多少账号是靠跨租户角色拿到 S8 能力的」原样记下来。
  39. -- 这些账号在迁移后会失去对应能力 —— 那是修复的预期结果,但必须留下可核对的清单,
  40. -- 否则明天有人报「我的转派按钮不见了」时无从判断是回归还是修复。
  41. -- ─────────────────────────────────────────────────────────────────────────────────────
  42. CREATE TABLE IF NOT EXISTS `ado_s8_cross_tenant_grant_1_0_497` (
  43. `id` BIGINT NOT NULL AUTO_INCREMENT,
  44. `user_id` BIGINT NOT NULL,
  45. `user_account` VARCHAR(64) NULL,
  46. `user_tenant_id` BIGINT NULL,
  47. `role_id` BIGINT NOT NULL,
  48. `role_code` VARCHAR(64) NULL,
  49. `role_tenant_id` BIGINT NULL,
  50. `permission` VARCHAR(128) NULL,
  51. `noted_at` DATETIME NOT NULL,
  52. PRIMARY KEY (`id`),
  53. KEY `idx_cross_grant_497_user` (`user_id`)
  54. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='1.0.497 迁移前的跨租户 S8 能力授予取证';
  55. DELETE FROM `ado_s8_cross_tenant_grant_1_0_497`;
  56. INSERT INTO `ado_s8_cross_tenant_grant_1_0_497`
  57. (`user_id`,`user_account`,`user_tenant_id`,`role_id`,`role_code`,`role_tenant_id`,`permission`,`noted_at`)
  58. SELECT u.`Id`, u.`Account`, u.`TenantId`, r.`Id`, r.`Code`, r.`TenantId`, m.`Permission`, NOW()
  59. FROM `SysUserRole` ur
  60. JOIN `SysUser` u ON u.`Id` = ur.`UserId`
  61. JOIN `SysRole` r ON r.`Id` = ur.`RoleId`
  62. JOIN `SysRoleMenu` rm ON rm.`RoleId` = r.`Id`
  63. JOIN `SysMenu` m ON m.`Id` = rm.`MenuId`
  64. WHERE m.`Type` = 3 -- MenuTypeEnum.Btn(Dir=1 / 菜单=2 / 按钮=3)
  65. AND m.`Permission` LIKE 's8:%'
  66. AND r.`TenantId` <> u.`TenantId`;
  67. -- 诊断自检:本脚本执行时若一条跨租户授予都没记到,要么真的没有,要么上面的判据写错了。
  68. -- 这两种情况后果完全不同(前者无事,后者是"看起来成功、实际什么都没做"),
  69. -- 所以把当时的可比对基数一并记下来,让人能当场分辨。
  70. INSERT INTO `ado_s8_cross_tenant_grant_1_0_497`
  71. (`user_id`,`user_account`,`user_tenant_id`,`role_id`,`role_code`,`role_tenant_id`,`permission`,`noted_at`)
  72. SELECT 0, '__DIAGNOSTIC__', NULL, 0, '__s8_btn_menu_total__', NULL,
  73. CAST(COUNT(*) AS CHAR), NOW()
  74. FROM `SysMenu` WHERE `Type` = 3 AND `Permission` LIKE 's8:%';
  75. -- ─────────────────────────────────────────────────────────────────────────────────────
  76. -- PHASE 3 · SCHEMA
  77. -- 唯一索引:同一 (租户, 动作, 角色) 只允许一行。没有它,重复授权会让
  78. -- 「取消授权」变成删一行、剩一行,界面显示已取消而实际仍然放行。
  79. -- ─────────────────────────────────────────────────────────────────────────────────────
  80. SET @has_idx := (SELECT COUNT(DISTINCT INDEX_NAME) FROM information_schema.STATISTICS
  81. WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception_action_role'
  82. AND INDEX_NAME='uk_s8_action_role');
  83. -- 建唯一索引前先去重(CodeFirst 先建表、后加索引的场景下可能已有重复行)。
  84. SET @sql := IF(@has_idx = 0, '
  85. DELETE a FROM `ado_s8_exception_action_role` a
  86. JOIN `ado_s8_exception_action_role` b
  87. ON a.`tenant_id` = b.`tenant_id`
  88. AND a.`action_code` = b.`action_code`
  89. AND a.`role_id` = b.`role_id`
  90. AND a.`id` > b.`id`', 'DO 0');
  91. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  92. SET @sql := IF(@has_idx = 0,
  93. 'ALTER TABLE `ado_s8_exception_action_role` ADD UNIQUE INDEX `uk_s8_action_role` (`tenant_id`,`action_code`,`role_id`)',
  94. 'DO 0');
  95. PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
  96. -- ─────────────────────────────────────────────────────────────────────────────────────
  97. -- PHASE 4 · POSTCHECK
  98. -- ─────────────────────────────────────────────────────────────────────────────────────
  99. -- ① 唯一索引必须存在且覆盖三列
  100. SET @cols := (SELECT COUNT(*) FROM information_schema.STATISTICS
  101. WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception_action_role'
  102. AND INDEX_NAME='uk_s8_action_role'
  103. AND COLUMN_NAME IN ('tenant_id','action_code','role_id'));
  104. DROP TABLE IF EXISTS `tmp_s8_497_guard`;
  105. CREATE TABLE `tmp_s8_497_guard` (
  106. probe BIGINT NOT NULL,
  107. CONSTRAINT s8_497_BLOCKED_unique_index_missing CHECK (probe = 0)
  108. ) COMMENT '1.0.497 断言门禁;通过后自动删除';
  109. INSERT INTO `tmp_s8_497_guard` (probe) VALUES ((3 - @cols));
  110. DROP TABLE IF EXISTS `tmp_s8_497_guard`;
  111. -- ② 不允许出现「授权角色不属于该行租户」的数据 —— 这正是本批要消灭的形态
  112. SET @bad := (SELECT COUNT(*) FROM `ado_s8_exception_action_role` a
  113. JOIN `SysRole` r ON r.`Id` = a.`role_id`
  114. WHERE r.`TenantId` <> a.`tenant_id`);
  115. DROP TABLE IF EXISTS `tmp_s8_497_guard`;
  116. CREATE TABLE `tmp_s8_497_guard` (
  117. probe BIGINT NOT NULL,
  118. CONSTRAINT s8_497_BLOCKED_cross_tenant_role_granted CHECK (probe = 0)
  119. ) COMMENT '1.0.497 断言门禁;通过后自动删除';
  120. INSERT INTO `tmp_s8_497_guard` (probe) VALUES (@bad);
  121. DROP TABLE IF EXISTS `tmp_s8_497_guard`;
  122. -- ─────────────────────────────────────────────────────────────────────────────────────
  123. -- PHASE 5 · IDEMPOTENT RERUN GUARD
  124. -- 第二次执行:CREATE TABLE IF NOT EXISTS 跳过、索引已存在跳过、断言依旧成立。
  125. -- 本脚本**不写任何授权数据**,因此重跑不会改变任何一条授权。
  126. -- ─────────────────────────────────────────────────────────────────────────────────────
  127. -- ─────────────────────────────────────────────────────────────────────────────────────
  128. -- PHASE 6 · 菜单重指
  129. -- /aidop/s8/config/roles 原指向「操作员账号绑定」。该页面在 S8-SYSUSER-ONLY-1 已退役
  130. -- (它唯一的用途是把员工绑成 S8 可用处理人,账号即身份之后不再有对应概念)。
  131. -- 本批把这个位置改为「异常操作权限」—— 复用既有菜单与既有 s8:config:* 权限位,
  132. -- 不新增菜单节点,避免用户在配置中心里看到一个空壳入口和一个新入口并存。
  133. -- ─────────────────────────────────────────────────────────────────────────────────────
  134. UPDATE `SysMenu`
  135. SET `Title` = '异常操作权限',
  136. `Component` = '/aidop/s8/config/S8ActionPermissionConfigPage'
  137. WHERE `Path` = '/aidop/s8/config/roles'
  138. AND `Type` = 2;