| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- -- =====================================================================================
- -- 1.0.497 S8-ACTION-PERMISSION-1 · 异常动作授权矩阵
- --
- -- 背景(真库取证,只读):
- -- 平台的权限解析链 SysUserRole → SysRoleMenu → SysMenu(Btn) 只对**菜单**做了租户校验
- -- (InnerJoin SysTenantMenu ... t.TenantId == 当前租户),**对角色没有**;
- -- SysUserRole 本身也不要求用户与角色同租户。
- --
- -- 实测后果:账号 UATAdminA(TenantId = 838257186181189)被授予了角色
- -- ROLE_S6_IPQC_SUPERVISOR,而该角色 SysRole.TenantId = 1300000000001(默认租户)。
- -- 它由此获得 s8:exception:assign / claim / close / reject / upgrade ——
- -- 这些能力在它自己的租户里**没有任何角色授予**。同租户的 UATExceptionA 调转派得 403,
- -- UATAdminA 却成功,差别完全来自那个别家租户的角色。
- --
- -- 本次动作:把「谁能对异常单做什么」收回 S8 自己回答,tenant_id 成为行上的一等字段,
- -- 跨租户在结构上不成立。授权矩阵的初值由启动期 Provisioning 从**本租户既有**
- -- SysRoleMenu 语义推导(跨租户角色不带入),推不出角色的动作保持空 = fail closed。
- --
- -- 表结构本身由 CodeFirst(InitTables) 负责创建;本脚本只做:
- -- · 幂等地补齐唯一索引(CodeFirst 对既有表不一定补索引)
- -- · postcheck 断言
- -- 不在此写任何授权数据 —— 授权是 per-tenant 推导结果,写死在迁移里无法适配新租户。
- -- =====================================================================================
- -- ─────────────────────────────────────────────────────────────────────────────────────
- -- PHASE 1 · PREFLIGHT
- -- ─────────────────────────────────────────────────────────────────────────────────────
- CREATE TABLE IF NOT EXISTS `ado_s8_exception_action_role` (
- `id` BIGINT NOT NULL AUTO_INCREMENT,
- `tenant_id` BIGINT NOT NULL COMMENT '租户(唯一安全边界)',
- `action_code` VARCHAR(64) NOT NULL COMMENT '动作码,取值须在 S8ExceptionActionCatalog 内',
- `role_id` BIGINT NOT NULL COMMENT '被授权角色 SysRole.Id,必须属于 tenant_id',
- `created_at` DATETIME NOT NULL,
- `created_by` VARCHAR(64) NULL,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S8 异常动作角色授权';
- -- ─────────────────────────────────────────────────────────────────────────────────────
- -- PHASE 2 · DIAGNOSTICS
- -- 把「本次迁移之前,有多少账号是靠跨租户角色拿到 S8 能力的」原样记下来。
- -- 这些账号在迁移后会失去对应能力 —— 那是修复的预期结果,但必须留下可核对的清单,
- -- 否则明天有人报「我的转派按钮不见了」时无从判断是回归还是修复。
- -- ─────────────────────────────────────────────────────────────────────────────────────
- CREATE TABLE IF NOT EXISTS `ado_s8_cross_tenant_grant_1_0_497` (
- `id` BIGINT NOT NULL AUTO_INCREMENT,
- `user_id` BIGINT NOT NULL,
- `user_account` VARCHAR(64) NULL,
- `user_tenant_id` BIGINT NULL,
- `role_id` BIGINT NOT NULL,
- `role_code` VARCHAR(64) NULL,
- `role_tenant_id` BIGINT NULL,
- `permission` VARCHAR(128) NULL,
- `noted_at` DATETIME NOT NULL,
- PRIMARY KEY (`id`),
- KEY `idx_cross_grant_497_user` (`user_id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='1.0.497 迁移前的跨租户 S8 能力授予取证';
- DELETE FROM `ado_s8_cross_tenant_grant_1_0_497`;
- INSERT INTO `ado_s8_cross_tenant_grant_1_0_497`
- (`user_id`,`user_account`,`user_tenant_id`,`role_id`,`role_code`,`role_tenant_id`,`permission`,`noted_at`)
- SELECT u.`Id`, u.`Account`, u.`TenantId`, r.`Id`, r.`Code`, r.`TenantId`, m.`Permission`, NOW()
- FROM `SysUserRole` ur
- JOIN `SysUser` u ON u.`Id` = ur.`UserId`
- JOIN `SysRole` r ON r.`Id` = ur.`RoleId`
- JOIN `SysRoleMenu` rm ON rm.`RoleId` = r.`Id`
- JOIN `SysMenu` m ON m.`Id` = rm.`MenuId`
- WHERE m.`Type` = 3 -- MenuTypeEnum.Btn(Dir=1 / 菜单=2 / 按钮=3)
- AND m.`Permission` LIKE 's8:%'
- AND r.`TenantId` <> u.`TenantId`;
- -- 诊断自检:本脚本执行时若一条跨租户授予都没记到,要么真的没有,要么上面的判据写错了。
- -- 这两种情况后果完全不同(前者无事,后者是"看起来成功、实际什么都没做"),
- -- 所以把当时的可比对基数一并记下来,让人能当场分辨。
- INSERT INTO `ado_s8_cross_tenant_grant_1_0_497`
- (`user_id`,`user_account`,`user_tenant_id`,`role_id`,`role_code`,`role_tenant_id`,`permission`,`noted_at`)
- SELECT 0, '__DIAGNOSTIC__', NULL, 0, '__s8_btn_menu_total__', NULL,
- CAST(COUNT(*) AS CHAR), NOW()
- FROM `SysMenu` WHERE `Type` = 3 AND `Permission` LIKE 's8:%';
- -- ─────────────────────────────────────────────────────────────────────────────────────
- -- PHASE 3 · SCHEMA
- -- 唯一索引:同一 (租户, 动作, 角色) 只允许一行。没有它,重复授权会让
- -- 「取消授权」变成删一行、剩一行,界面显示已取消而实际仍然放行。
- -- ─────────────────────────────────────────────────────────────────────────────────────
- SET @has_idx := (SELECT COUNT(DISTINCT INDEX_NAME) FROM information_schema.STATISTICS
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception_action_role'
- AND INDEX_NAME='uk_s8_action_role');
- -- 建唯一索引前先去重(CodeFirst 先建表、后加索引的场景下可能已有重复行)。
- SET @sql := IF(@has_idx = 0, '
- DELETE a FROM `ado_s8_exception_action_role` a
- JOIN `ado_s8_exception_action_role` b
- ON a.`tenant_id` = b.`tenant_id`
- AND a.`action_code` = b.`action_code`
- AND a.`role_id` = b.`role_id`
- AND a.`id` > b.`id`', 'DO 0');
- PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
- SET @sql := IF(@has_idx = 0,
- 'ALTER TABLE `ado_s8_exception_action_role` ADD UNIQUE INDEX `uk_s8_action_role` (`tenant_id`,`action_code`,`role_id`)',
- 'DO 0');
- PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
- -- ─────────────────────────────────────────────────────────────────────────────────────
- -- PHASE 4 · POSTCHECK
- -- ─────────────────────────────────────────────────────────────────────────────────────
- -- ① 唯一索引必须存在且覆盖三列
- SET @cols := (SELECT COUNT(*) FROM information_schema.STATISTICS
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception_action_role'
- AND INDEX_NAME='uk_s8_action_role'
- AND COLUMN_NAME IN ('tenant_id','action_code','role_id'));
- DROP TABLE IF EXISTS `tmp_s8_497_guard`;
- CREATE TABLE `tmp_s8_497_guard` (
- probe BIGINT NOT NULL,
- CONSTRAINT s8_497_BLOCKED_unique_index_missing CHECK (probe = 0)
- ) COMMENT '1.0.497 断言门禁;通过后自动删除';
- INSERT INTO `tmp_s8_497_guard` (probe) VALUES ((3 - @cols));
- DROP TABLE IF EXISTS `tmp_s8_497_guard`;
- -- ② 不允许出现「授权角色不属于该行租户」的数据 —— 这正是本批要消灭的形态
- SET @bad := (SELECT COUNT(*) FROM `ado_s8_exception_action_role` a
- JOIN `SysRole` r ON r.`Id` = a.`role_id`
- WHERE r.`TenantId` <> a.`tenant_id`);
- DROP TABLE IF EXISTS `tmp_s8_497_guard`;
- CREATE TABLE `tmp_s8_497_guard` (
- probe BIGINT NOT NULL,
- CONSTRAINT s8_497_BLOCKED_cross_tenant_role_granted CHECK (probe = 0)
- ) COMMENT '1.0.497 断言门禁;通过后自动删除';
- INSERT INTO `tmp_s8_497_guard` (probe) VALUES (@bad);
- DROP TABLE IF EXISTS `tmp_s8_497_guard`;
- -- ─────────────────────────────────────────────────────────────────────────────────────
- -- PHASE 5 · IDEMPOTENT RERUN GUARD
- -- 第二次执行:CREATE TABLE IF NOT EXISTS 跳过、索引已存在跳过、断言依旧成立。
- -- 本脚本**不写任何授权数据**,因此重跑不会改变任何一条授权。
- -- ─────────────────────────────────────────────────────────────────────────────────────
- -- ─────────────────────────────────────────────────────────────────────────────────────
- -- PHASE 6 · 菜单重指
- -- /aidop/s8/config/roles 原指向「操作员账号绑定」。该页面在 S8-SYSUSER-ONLY-1 已退役
- -- (它唯一的用途是把员工绑成 S8 可用处理人,账号即身份之后不再有对应概念)。
- -- 本批把这个位置改为「异常操作权限」—— 复用既有菜单与既有 s8:config:* 权限位,
- -- 不新增菜单节点,避免用户在配置中心里看到一个空壳入口和一个新入口并存。
- -- ─────────────────────────────────────────────────────────────────────────────────────
- UPDATE `SysMenu`
- SET `Title` = '异常操作权限',
- `Component` = '/aidop/s8/config/S8ActionPermissionConfigPage'
- WHERE `Path` = '/aidop/s8/config/roles'
- AND `Type` = 2;
|