S8TenantRoleResolver.cs 4.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. using Admin.NET.Core;
  2. namespace Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  3. /// <summary>
  4. /// S8-ACTION-PERMISSION-1:「这个账号在**当前租户**下持有哪些角色 / 哪些能力码」的<b>唯一</b>实现。
  5. ///
  6. /// <para><b>它修的是一个实测存在的跨租户权限泄漏。</b>平台的
  7. /// <c>SysMenuService.GetOwnBtnPermList()</c> 解析链是
  8. /// <c>SysUserRole → SysRoleMenu → SysMenu(Btn)</c>,其中<b>只对「菜单」做了租户校验</b>
  9. /// (<c>InnerJoin SysTenantMenu ... t.TenantId == 当前租户</c>),
  10. /// <b>对「角色」没有任何租户校验</b>;而 <c>SysUserRole</c> 本身也不要求用户与角色同租户。</para>
  11. ///
  12. /// <para>真库取证(aidopdev_s8_local,只读):账号 <c>UATAdminA</c>
  13. /// (<c>TenantId = 838257186181189</c>)被授予了角色 <c>ROLE_S6_IPQC_SUPERVISOR</c>,
  14. /// 而该角色的 <c>SysRole.TenantId = 1300000000001</c>(默认租户)。它由此拿到了
  15. /// <c>s8:exception:assign / claim / close / reject / upgrade</c> —— 这些能力
  16. /// <b>在它自己的租户里没有任何角色授予</b>。同租户的 <c>UATExceptionA</c> 调转派得到 403,
  17. /// 而 <c>UATAdminA</c> 成功,差别完全来自那个别家租户的角色。</para>
  18. ///
  19. /// <para><b>修法:取交集,只收窄不放宽。</b>平台原结果保留(它负责菜单租户校验、
  20. /// 超管语义与缓存),再与「同租户角色可达的能力码」求交。交集在数学上不可能
  21. /// 引入任何新权限,因此这个改动只可能让原本能做的事变成不能做,
  22. /// 不可能反过来 —— 对权限修复而言这是唯一可接受的方向。</para>
  23. ///
  24. /// <para><b>超管</b>:与 <c>JwtHandler.CheckAuthorizeAsync</c> 同口径豁免,
  25. /// 由调用方在进入本解析器之前判断,本类不重复实现那条分支。</para>
  26. /// </summary>
  27. public interface IS8TenantRoleResolver
  28. {
  29. /// <summary>该账号在指定租户下持有的角色 Id(角色本身必须属于该租户)。</summary>
  30. Task<IReadOnlyList<long>> GetRoleIdsAsync(long tenantId, long userId);
  31. /// <summary>该账号经<b>同租户角色</b>可达的能力码集合(<c>SysMenu.Permission</c>)。</summary>
  32. Task<IReadOnlySet<string>> GetPermissionCodesAsync(long tenantId, long userId);
  33. }
  34. /// <summary>正式实现。生产代码里只有这一份;接口只为测试留 seam。</summary>
  35. public sealed class S8TenantRoleResolver : IS8TenantRoleResolver, ITransient
  36. {
  37. private readonly SqlSugarRepository<SysUserRole> _userRoleRep;
  38. public S8TenantRoleResolver(SqlSugarRepository<SysUserRole> userRoleRep) => _userRoleRep = userRoleRep;
  39. public async Task<IReadOnlyList<long>> GetRoleIdsAsync(long tenantId, long userId)
  40. {
  41. if (tenantId <= 0 || userId <= 0) return Array.Empty<long>();
  42. // ClearFilter:边界由本查询显式声明(SysRole.TenantId == 传入租户),
  43. // 不依赖全局 multi-tenant AOP 的当前行为。
  44. return await _userRoleRep.AsQueryable().ClearFilter()
  45. .InnerJoin<SysRole>((ur, r) => ur.RoleId == r.Id)
  46. .Where((ur, r) => ur.UserId == userId && r.TenantId == tenantId)
  47. .Select((ur, r) => r.Id)
  48. .Distinct()
  49. .ToListAsync();
  50. }
  51. public async Task<IReadOnlySet<string>> GetPermissionCodesAsync(long tenantId, long userId)
  52. {
  53. var roleIds = await GetRoleIdsAsync(tenantId, userId);
  54. if (roleIds.Count == 0) return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  55. var codes = await _userRoleRep.Context.Queryable<SysRoleMenu>()
  56. .ClearFilter()
  57. .InnerJoin<SysMenu>((rm, m) => rm.MenuId == m.Id)
  58. .Where((rm, m) => roleIds.Contains(rm.RoleId)
  59. && m.Type == MenuTypeEnum.Btn
  60. && m.Permission != null
  61. && m.Permission != "")
  62. .Select((rm, m) => m.Permission)
  63. .Distinct()
  64. .ToListAsync();
  65. return new HashSet<string>(codes.Where(c => !string.IsNullOrWhiteSpace(c))!, StringComparer.OrdinalIgnoreCase);
  66. }
  67. }