Просмотр исходного кода

fix(s0): restore sysOrg permission for UAT role

恢复租户 A 的 UAT_FUNCTION_USER 角色的 sysOrg:list 按钮权限。

背景:JwtHandler.CheckAuthorizeAsync 把 /api/sysOrg/list 映射为按钮权限
sysOrg:list(SysMenu Id=1300200030201, Type=3)。该权限 1.0.386 曾补过,
但 SysRoleMenuService.GrantRoleMenu 采用「先删该角色全部 SysRoleMenu,
再按 UI 勾选项全量 INSERT」,管理员重存角色时把它冲掉,导致 UATAdminA
打开 S0 页面时两次 /api/sysOrg/list 均返回 403。

1.0.431.sql
  精确到单租户单角色单菜单,不按 RoleCode 跨租户批量补:
    TenantId = 838257186181189
    RoleId   = 838258976469061
    MenuId   = 1300200030201
  仅 4 个 SET + 1 条 INSERT,带 SysTenantMenu 授权校验与 NOT EXISTS 守卫,
  重复执行 affected rows = 0;不改租户 B、不改其他角色、不改 SysMenu /
  SysTenantMenu、不放开 sysOrg 的 add/update/delete。

1.0.431.verify.sql
  取代 1.0.386.verify.sql 的跨租户口径。旧断言按 RoleCode JOIN 后 COUNT,
  同名角色在 A/B 两租户各有一个,只要任一租户还留着绑定即判 Success,
  故租户 A 二次丢失时全程无告警(跨租户假阳性)。
  新断言四条全部钉死 TenantId / RoleId / MenuId,并校验角色归属租户 A、
  菜单确为 sysOrg:list(Type=3)、SysTenantMenu 允许、SysRoleMenu 精确 1 条。

数据库侧已于 2026-09-01 11:26:24 应用成功(affected_rows=1,verify=Success),
本提交为脚本入库收尾,使 Git 与迁移记录一致。
YY968XX 10 часов назад
Родитель
Сommit
a9c25ead5d

+ 9 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.430</AssemblyVersion>
-    <FileVersion>1.0.430</FileVersion>
-    <Version>1.0.430</Version>
+    <AssemblyVersion>1.0.431</AssemblyVersion>
+    <FileVersion>1.0.431</FileVersion>
+    <Version>1.0.431</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -589,6 +589,12 @@
     <None Update="UpdateScripts\1.0.424.verify.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="UpdateScripts\1.0.431.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.431.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
     <None Update="UpdateScripts\UAT-PLACEHOLDER-MENU-HIDE.ops.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>

+ 96 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.431.sql

@@ -0,0 +1,96 @@
+-- ============================================================================
+-- 1.0.431:恢复 UAT 租户 A 的 UAT_FUNCTION_USER 角色的 sysOrg:list 按钮权限
+--
+-- 症状:
+--   UATAdminA(Id=838257187360837,AccountType=777 普通账号,非超管)打开任意 S0
+--   页面时弹两个「403 禁止访问,没有权限」。
+--
+-- 链路(CODE VERIFIED):
+--   Web/src/views/aidop/s0/sales/ProductDesignCycleList.vue → loadOptions()
+--     → loadOrgList('201') / loadOrgList('501')(s0SalesApi.ts:765)
+--     → 两次 GET /api/sysOrg/list
+--   JwtHandler.CheckAuthorizeAsync(Admin.NET.Web.Core/Handlers/JwtHandler.cs:85)
+--     把路径映射为按钮权限名 sysOrg:list;非超管必须命中自有按钮权限,
+--     未命中时仅当该权限「未登记为按钮权限」才兜底放行。
+--     sysOrg:list 已登记(SysMenu Id=1300200030201,Type=3 Btn),
+--     而该角色未授权 → 授权失败 → AdminResultProvider.cs:84 返回
+--     「403 禁止访问,没有权限」。两次请求 → 两个弹窗。
+--
+-- 为什么 1.0.386 补过又丢了(DB VERIFIED):
+--   1.0.386 于 2026-08-24 13:21 Success(affected_rows=4),同时给 A/B 两个租户的
+--   同名角色补了权限,Id 依次为 …807686(租户 A)与 …807687(租户 B)。
+--   当前 …807687 仍在,…807686 已不存在;且租户 A 该角色现有 290 条 SysRoleMenu
+--   的 Id 为一整段连续块(843881101070405~843881101086842),
+--   是 SysRoleMenuService.GrantRoleMenu(Role/SysRoleMenuService.cs:73)
+--   「先 DELETE 该角色全部 SysRoleMenu,再按 UI 勾选项全量 INSERT」的签名。
+--   → 管理员在「角色管理 → 授权菜单」重存该角色、未勾选机构管理,
+--     把 migration 单独补的这条冲掉了。
+--
+-- 本脚本口径:精确到单租户单角色单菜单,不按 RoleCode 跨租户批量补。
+--   TenantId = 838257186181189(租户 A)
+--   RoleId   = 838258976469061(租户 A 的 UAT_FUNCTION_USER)
+--   MenuId   = 1300200030201(sysOrg:list,Type=3 Btn)
+--
+-- 明确不做:
+--   - 不按 RoleCode 给所有租户补权限(1.0.386 的口径,已证明会掩盖单租户丢失)
+--   - 不改租户 B 的 RoleId=838259499909189(仅作对照)
+--   - 不改其他任何角色
+--   - 不改 SysMenu、不改 SysTenantMenu
+--   - 不放开 sysOrg:add / sysOrg:update / sysOrg:delete
+--   - 无 DDL、无 DELETE、无业务数据写入
+--
+-- 已知连带影响(不可避免,且与对照租户 B 现状一致):
+--   SysMenuService.GetLoginMenuTree 在过滤 Type != Btn 之后,会对角色菜单集合做
+--   ExpandMenuIdsWithAncestorsAsync(SysMenuService.cs:65/441)祖先补全。
+--   补入 Btn 1300200030201 后,其祖先「机构管理」(1300200030101, Type=2) 会被补进
+--   菜单树;租户 A 的 SysTenantMenu 允许该菜单,故 UATAdminA 侧边栏会新增
+--   「系统管理 / 机构管理」入口(只读可用,add/update/delete 仍会 403)。
+--   租户 B 的同名角色已是同一形态,本脚本使 A 与 B 对齐。
+--
+-- ID 策略:沿用 UpdateScripts 既有写法(1.0.386 / 1.0.406)
+--   SET @base := (SELECT IFNULL(MAX(Id),0) FROM SysRoleMenu); INSERT ... @base + 1
+--
+-- 幂等:INSERT ... SELECT ... FROM DUAL WHERE <全部前置条件> AND NOT EXISTS(...)。
+--   重复执行 affected rows = 0,不会产生第二条记录。
+--
+-- 环境容错:以上 Id 在不存在该 UAT 夹具的库上均查不到 → 整脚本为空操作,
+--   verify 同步短路为真(见 1.0.431.verify.sql)。
+--
+-- ⚠️ 缓存:GetOwnBtnPermList 结果按 sys_user_button:<userId> 缓存 7 天
+--   (SysMenuService.cs:355,CacheConst.KeyUserButton),当前 CacheType=Memory
+--   (Admin.NET.Application/Configuration/Cache.json:7)为进程内缓存,
+--   SQL 写库不会使其失效。AutoVersionUpdate 在应用启动时执行本脚本,
+--   该进程缓存本就为空,故随发布重启天然生效;若在已运行实例上手工执行,
+--   需另行清缓存(见报告「缓存处理」)。
+-- ============================================================================
+
+SET @orglist_tenant := 838257186181189;
+SET @orglist_role   := (SELECT r.Id FROM SysRole r
+                        WHERE r.Id = 838258976469061
+                          AND r.TenantId = 838257186181189
+                          AND r.Code = 'UAT_FUNCTION_USER'
+                          AND r.Status = 1
+                        LIMIT 1);
+SET @orglist_menu   := (SELECT m.Id FROM SysMenu m
+                        WHERE m.Id = 1300200030201
+                          AND m.Permission = 'sysOrg:list'
+                          AND m.Type = 3
+                          AND m.Status = 1
+                        LIMIT 1);
+SET @orglist_rm_base := (SELECT IFNULL(MAX(Id), 0) FROM SysRoleMenu);
+
+INSERT INTO SysRoleMenu (Id, RoleId, MenuId)
+SELECT @orglist_rm_base + 1, @orglist_role, @orglist_menu
+FROM DUAL
+WHERE @orglist_role IS NOT NULL
+  AND @orglist_menu IS NOT NULL
+  AND EXISTS (
+    SELECT 1 FROM SysTenantMenu t
+     WHERE t.TenantId = @orglist_tenant
+       AND t.MenuId = @orglist_menu
+  )
+  AND NOT EXISTS (
+    SELECT 1 FROM SysRoleMenu x
+     WHERE x.RoleId = @orglist_role
+       AND x.MenuId = @orglist_menu
+  );

+ 66 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.431.verify.sql

@@ -0,0 +1,66 @@
+-- ============================================================================
+-- 1.0.431 verify:租户 A 的 UAT_FUNCTION_USER 必须精确持有 sysOrg:list。
+--
+-- 为什么不能沿用 1.0.386.verify.sql 的口径:
+--   旧断言是 `JOIN SysRole r ON ... WHERE r.Code = 'UAT_FUNCTION_USER'
+--   AND m.Permission = 'sysOrg:list'` 然后 COUNT(*)——按 RoleCode 跨租户聚合。
+--   同名角色在租户 A / B 各有一个(RoleId 838258976469061 / 838259499909189);
+--   只要任一租户还留着这条绑定,COUNT 就 >0 判真。
+--   实测结果:租户 A 的绑定已被 GrantRoleMenu 全量覆盖删除,而旧 verify 因租户 B
+--   仍在而一直判 Success —— 跨租户假阳性,权限二次丢失全程无告警。
+--
+-- 新口径:全部断言绑定到精确的 TenantId / RoleId / MenuId,且校验绑定确实归属
+--   租户 A,避免「错误 RoleId 恰好有该菜单」被误判成功。
+--
+-- 约定(AutoVersionUpdate.RunVerifyScriptIfExists,Core/Update/AutoVersionUpdate.cs:224):
+--   每条语句取「首行首列」当布尔断言,必须为真;返回 0 行会得到 NULL 判假并中断启动。
+--   故每条断言一律返回标量真值,且不使用 SET(SET 不返回行 → 必然判假)。
+--
+-- 环境容错:目标 RoleId 不存在的库(非 UAT 夹具库)上主脚本为空操作,
+--   四条断言同步短路为真。注意短路条件是精确 RoleId 缺失,不是 RoleCode。
+-- ============================================================================
+
+-- 1) 角色存在,且确实属于租户 A(RoleId 存在但租户不符 → 判假,不放行)
+SELECT IF(
+  (SELECT COUNT(*) FROM SysRole WHERE Id = 838258976469061) = 0,
+  1,
+  (SELECT COUNT(*) FROM SysRole
+    WHERE Id = 838258976469061
+      AND TenantId = 838257186181189
+      AND Code = 'UAT_FUNCTION_USER'
+      AND Status = 1)
+) AS ok_role_in_tenant_a;
+
+-- 2) 菜单存在,且确实是 sysOrg:list 按钮(Type=3 Btn / 启用)
+SELECT IF(
+  (SELECT COUNT(*) FROM SysRole WHERE Id = 838258976469061) = 0,
+  1,
+  (SELECT COUNT(*) FROM SysMenu
+    WHERE Id = 1300200030201
+      AND Permission = 'sysOrg:list'
+      AND Type = 3
+      AND Status = 1)
+) AS ok_menu_is_sysorg_list;
+
+-- 3) 租户 A 的 SysTenantMenu 允许该菜单(GetOwnBtnPermList 会 InnerJoin 它)
+SELECT IF(
+  (SELECT COUNT(*) FROM SysRole WHERE Id = 838258976469061) = 0,
+  1,
+  (SELECT COUNT(*) FROM SysTenantMenu
+    WHERE TenantId = 838257186181189
+      AND MenuId = 1300200030201)
+) AS ok_tenant_a_allows_menu;
+
+-- 4) SysRoleMenu 精确存在 1 条(既非缺失也非重复),且该绑定归属租户 A
+SELECT IF(
+  (SELECT COUNT(*) FROM SysRole WHERE Id = 838258976469061) = 0,
+  1,
+  (SELECT COUNT(*) = 1
+     FROM SysRoleMenu rm
+     JOIN SysRole r ON r.Id = rm.RoleId
+     JOIN SysMenu m ON m.Id = rm.MenuId
+    WHERE rm.RoleId = 838258976469061
+      AND rm.MenuId = 1300200030201
+      AND r.TenantId = 838257186181189
+      AND m.Permission = 'sysOrg:list')
+) AS ok_rolemenu_exactly_one_in_tenant_a;