|
@@ -32,36 +32,59 @@ public class S8RoleResolver : ITransient
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// 解析 RoleCode / RoleId 混合 token 列表为去重 UserId 列表。
|
|
/// 解析 RoleCode / RoleId 混合 token 列表为去重 UserId 列表。
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// <b>S8-ROLE-TENANT-SCOPE-1(P0 SECURITY):租户必须由调用方显式传入。</b>
|
|
|
|
|
+ /// 原实现只按 Code / Id 匹配 <c>SysRole</c>,依赖平台的全局租户过滤器兜底;
|
|
|
|
|
+ /// 但该过滤器只在有登录上下文时挂载(<c>SqlSugarSetup</c>:<c>App.User</c> 为空则不挂、超管直接跳过),
|
|
|
|
|
+ /// 而 <c>S8WatchSchedulerJob</c> 走 <c>IServiceScopeFactory.CreateScope()</c>,**没有 App.User**。
|
|
|
|
|
+ /// 又因 <c>SysRole.Code</c> 跨租户重复(每个业务角色每租户一份,实测 9 份),
|
|
|
|
|
+ /// 后台派发时 "ROLE_PURCHASER" 会解析到全部租户的角色 → 跨租户通知投递。
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// 因此租户在此作为**显式安全边界**,不依赖任何隐式过滤:
|
|
|
|
|
+ /// Code 路径与数字 RoleId 路径**都**必须满足 <c>TenantId == tenantId</c>,
|
|
|
|
|
+ /// 后者可防止调用方(或被篡改的配置)传入他租户的 RoleId 绕过边界。
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
- public async Task<List<long>> ResolveUserIdsAsync(IEnumerable<string>? roleTokens)
|
|
|
|
|
|
|
+ /// <param name="tenantId">安全边界租户。必须 > 0,否则直接返回空集合(绝不放行全量)。</param>
|
|
|
|
|
+ /// <param name="roleTokens">RoleCode / RoleId 混合 token。</param>
|
|
|
|
|
+ public async Task<List<long>> ResolveUserIdsAsync(long tenantId, IEnumerable<string>? roleTokens)
|
|
|
{
|
|
{
|
|
|
var result = new HashSet<long>();
|
|
var result = new HashSet<long>();
|
|
|
if (roleTokens == null) return result.ToList();
|
|
if (roleTokens == null) return result.ToList();
|
|
|
|
|
|
|
|
- var roleIds = new HashSet<long>();
|
|
|
|
|
|
|
+ // 作用域缺失时宁可不通知,也不返回跨租户用户。
|
|
|
|
|
+ if (tenantId <= 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ _logger.LogWarning("S8RoleResolver: tenantId<=0,拒绝解析角色(避免跨租户投递)");
|
|
|
|
|
+ return result.ToList();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var requestedIds = new HashSet<long>();
|
|
|
var codes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var codes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
foreach (var raw in roleTokens)
|
|
foreach (var raw in roleTokens)
|
|
|
{
|
|
{
|
|
|
if (string.IsNullOrWhiteSpace(raw)) continue;
|
|
if (string.IsNullOrWhiteSpace(raw)) continue;
|
|
|
var t = raw.Trim();
|
|
var t = raw.Trim();
|
|
|
- if (long.TryParse(t, out var rid) && rid > 0) roleIds.Add(rid);
|
|
|
|
|
|
|
+ if (long.TryParse(t, out var rid) && rid > 0) requestedIds.Add(rid);
|
|
|
else codes.Add(t);
|
|
else codes.Add(t);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 经租户校验后的角色 Id,只有进入这里的才允许展开成用户。
|
|
|
|
|
+ var roleIds = new HashSet<long>();
|
|
|
|
|
+
|
|
|
if (codes.Count > 0)
|
|
if (codes.Count > 0)
|
|
|
{
|
|
{
|
|
|
try
|
|
try
|
|
|
{
|
|
{
|
|
|
var codeList = codes.ToList();
|
|
var codeList = codes.ToList();
|
|
|
var found = await _sysRoleRep.AsQueryable()
|
|
var found = await _sysRoleRep.AsQueryable()
|
|
|
- .Where(r => codeList.Contains(r.Code))
|
|
|
|
|
|
|
+ .Where(r => codeList.Contains(r.Code) && r.TenantId == tenantId)
|
|
|
.Select(r => new { r.Id, r.Code })
|
|
.Select(r => new { r.Id, r.Code })
|
|
|
.ToListAsync();
|
|
.ToListAsync();
|
|
|
var foundCodes = new HashSet<string>(found.Select(f => f.Code ?? string.Empty), StringComparer.OrdinalIgnoreCase);
|
|
var foundCodes = new HashSet<string>(found.Select(f => f.Code ?? string.Empty), StringComparer.OrdinalIgnoreCase);
|
|
|
foreach (var c in codes)
|
|
foreach (var c in codes)
|
|
|
{
|
|
{
|
|
|
if (!foundCodes.Contains(c))
|
|
if (!foundCodes.Contains(c))
|
|
|
- _logger.LogWarning("S8RoleResolver: RoleCode '{Code}' not found in SysRole", c);
|
|
|
|
|
|
|
+ _logger.LogWarning("S8RoleResolver: RoleCode '{Code}' not found in SysRole for tenant {TenantId}", c, tenantId);
|
|
|
}
|
|
}
|
|
|
foreach (var f in found) roleIds.Add(f.Id);
|
|
foreach (var f in found) roleIds.Add(f.Id);
|
|
|
}
|
|
}
|
|
@@ -71,6 +94,29 @@ public class S8RoleResolver : ITransient
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (requestedIds.Count > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ // 数字 RoleId 同样过租户校验:他租户 Id 一律丢弃并告警,不静默放行。
|
|
|
|
|
+ var idList = requestedIds.ToList();
|
|
|
|
|
+ var ownedIds = await _sysRoleRep.AsQueryable()
|
|
|
|
|
+ .Where(r => idList.Contains(r.Id) && r.TenantId == tenantId)
|
|
|
|
|
+ .Select(r => r.Id)
|
|
|
|
|
+ .ToListAsync();
|
|
|
|
|
+ var owned = new HashSet<long>(ownedIds);
|
|
|
|
|
+ foreach (var rid in requestedIds)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (owned.Contains(rid)) roleIds.Add(rid);
|
|
|
|
|
+ else _logger.LogWarning("S8RoleResolver: RoleId {RoleId} 不属于租户 {TenantId},已丢弃", rid, tenantId);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ _logger.LogWarning(ex, "S8RoleResolver: resolve RoleId batch failed");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
foreach (var rid in roleIds)
|
|
foreach (var rid in roleIds)
|
|
|
{
|
|
{
|
|
|
try
|
|
try
|