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