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