Przeglądaj źródła

fix(role): scope uniqueness and menu grants by tenant | server 1.0.425

角色编辑在超级管理员场景下的两个缺陷:

1) AddRole / UpdateRole 的重名校验只比 Name+Code、不带 TenantId。超管在
   SqlSugarSetup 中被显式排除租户过滤器(`if (isSuperAdmin) return;`),
   因此能看到其它租户的同名同码角色,编辑时被误判为 D1006「数据已存在」。
   由于 UpdateRole 的顺序是 查重 -> 更新 SysRole -> UpdateRoleMenu,该异常
   直接把菜单授权整段掐断:页面取消勾选后点确定,实际一行都没落库,弹窗也
   因为 submit 抛异常而不关闭。库中现有 12 组角色跨租户同 Name+Code,
   角色列表里几乎每一行都受影响。

   改为租户内唯一(跨租户允许复用,与现有数据模型一致;SysRole 上只有
   非唯一索引,无 DDL 阻挡)。租户Id取值:
   - AddRole 取最终落库值(入参为空/0 时回落当前登录租户,与 SqlSugar
     插入过滤器的回填口径一致),避免校验范围与写入范围不一致;
   - UpdateRole 取库中该角色的现有值。TenantId 标了 IsOnlyIgnoreUpdate,
     更新时本就不落库、角色无法迁租户,采信入参会出现「按客户端声称的
     租户查重、按库里真实租户写入」的错配。

2) UpdateRoleMenu 与 GrantMenu 各有一处 `MenuIdList.Count < 1 return`,
   把「空集合」当成「不修改菜单」,导致取消角色全部菜单权限无法落库。
   空集合的正确语义是「该角色最终不拥有任何菜单」,两处 early return
   一并去掉,交由 GrantRoleMenu 处理(其 DELETE 无条件、INSERT 按需,
   本身已支持清空,未改动)。null 入参归一为空集合。

验证(aidopdev 实测,超管走页面完整链路):
- 跨租户同名角色编辑不再 D1006,弹窗正常关闭;SysRoleMenu 全表重写为
  单批连续雪花号,证明 GrantRoleMenu 首次真正执行
- 同租户重名在新增与更新两条路径均仍返回 D1006,弹窗保持打开
- 不同租户可创建同 Name+Code
- 菜单 0 -> 70 -> 0 全链路正确,清空后该 RoleId 的 SysRoleMenu 为 0 行
- 对照租户的同名角色 311 行与 id 段完全未变
- dotnet build --framework net10.0:0 Error,无本批次新增告警
- 测试角色已按流程删除,SysRole 总数回到 124

已知遗留(本轮未处理,非本次引入):
- 角色编辑弹窗保存会连带清掉「树上渲染不出来」的既有授权(禁用菜单、
  ado_tenant_menu_suppression 抑制项),属 GrantRoleMenu 先删后插的既有
  语义,此前因 D1006 从未触发,修复后首次显形
- AddRole 直接采信入参 TenantId,普通租户管理员可借此在其它租户建角色,
  为改动前既有缺口,本次未扩大也未收窄
- UpdateRole 调用链上 DELETE+INSERT 无事务:[UnitOfWork] 是 MVC action
  filter,进程内直接调用 GrantMenu 时不生效
YY968XX 21 godzin temu
rodzic
commit
f1ec08fb2f

+ 13 - 5
server/Admin.NET.Core/Service/Role/SysRoleService.cs

@@ -86,7 +86,10 @@ public class SysRoleService : IDynamicApiController, ITransient
     [DisplayName("增加角色")]
     public async Task AddRole(AddRoleInput input)
     {
-        if (await _sysRoleRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code))
+        // 角色唯一性以租户为界:同租户内不允许 名称+编码 重复,跨租户允许复用
+        // 租户Id取最终落库值(未指定时 SqlSugar 插入过滤器会回填当前登录租户),保证校验范围与写入范围一致
+        long? tenantId = input.TenantId is > 0 ? input.TenantId : _userManager.TenantId;
+        if (await _sysRoleRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code && u.TenantId == tenantId))
             throw Oops.Oh(ErrorCodeEnum.D1006);
 
         var newRole = await _sysRoleRep.AsInsertable(input.Adapt<SysRole>()).ExecuteReturnEntityAsync();
@@ -101,11 +104,12 @@ public class SysRoleService : IDynamicApiController, ITransient
     /// <returns></returns>
     private async Task UpdateRoleMenu(AddRoleInput input)
     {
-        if (input.MenuIdList == null || input.MenuIdList.Count < 1) return;
+        // 空集合(或 null)语义为“该角色最终不拥有任何菜单”,而非“不修改菜单”,
+        // 因此必须继续下发以删除既有授权,不能在此提前返回
         await GrantMenu(new RoleMenuInput()
         {
             Id = input.Id,
-            MenuIdList = input.MenuIdList.ToList()
+            MenuIdList = input.MenuIdList?.ToList() ?? new List<long>()
         });
     }
 
@@ -118,7 +122,10 @@ public class SysRoleService : IDynamicApiController, ITransient
     [DisplayName("更新角色")]
     public async Task UpdateRole(UpdateRoleInput input)
     {
-        if (await _sysRoleRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code && u.Id != input.Id))
+        // 角色唯一性以租户为界:同租户内不允许 名称+编码 重复,跨租户允许复用
+        // 租户Id标记了 IsOnlyIgnoreUpdate(更新时不落库、不可迁移租户),故以库中现有值为准,不采信入参
+        var tenantId = await _sysRoleRep.AsQueryable().Where(u => u.Id == input.Id).Select(u => u.TenantId).FirstAsync();
+        if (await _sysRoleRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code && u.TenantId == tenantId && u.Id != input.Id))
             throw Oops.Oh(ErrorCodeEnum.D1006);
 
         await _sysRoleRep.AsUpdateable(input.Adapt<SysRole>()).IgnoreColumns(true)
@@ -167,7 +174,8 @@ public class SysRoleService : IDynamicApiController, ITransient
     [DisplayName("授权角色菜单")]
     public async Task GrantMenu(RoleMenuInput input)
     {
-        if (input.MenuIdList == null || input.MenuIdList.Count < 1) return;
+        // 空集合语义为“清空该角色全部菜单授权”,交由 GrantRoleMenu 先删后插(删除无条件、插入按需)
+        input.MenuIdList ??= new List<long>();
 
         await ClearUserApiCache(input.Id);
 

+ 3 - 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.424</AssemblyVersion>
-    <FileVersion>1.0.424</FileVersion>
-    <Version>1.0.424</Version>
+    <AssemblyVersion>1.0.425</AssemblyVersion>
+    <FileVersion>1.0.425</FileVersion>
+    <Version>1.0.425</Version>
   </PropertyGroup>
 
   <ItemGroup>