S8RoleResolver.cs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. using Admin.NET.Core.Service;
  2. using Microsoft.Extensions.Logging;
  3. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  4. /// <summary>
  5. /// S8 本地 Role → UserIds 解析器。
  6. /// 不改 Admin.NET.Core;不新增 SysUserRoleService.GetUserIdListByRoleCode。
  7. /// 在 S8 侧自行完成 RoleCode → SysRole.Id → UserIds 的解析。
  8. ///
  9. /// 兼容策略(与 ado_s8_notification_layer.target_role_ids 现状一致:表当前 0 行,
  10. /// SeedData 同时存在 RoleCode 命名(ROLE_ORDER_PLANNER)和 SysRole.Code 实存值(S8_01)):
  11. /// - 纯数字 token → 视为 SysRole.Id
  12. /// - 非数字 token → 视为 SysRole.Code
  13. /// - 找不到的 token:仅 LogWarning,不抛异常。
  14. /// </summary>
  15. public class S8RoleResolver : ITransient
  16. {
  17. private readonly SqlSugarRepository<SysRole> _sysRoleRep;
  18. private readonly SysUserRoleService _sysUserRoleService;
  19. private readonly ILogger<S8RoleResolver> _logger;
  20. public S8RoleResolver(
  21. SqlSugarRepository<SysRole> sysRoleRep,
  22. SysUserRoleService sysUserRoleService,
  23. ILogger<S8RoleResolver> logger)
  24. {
  25. _sysRoleRep = sysRoleRep;
  26. _sysUserRoleService = sysUserRoleService;
  27. _logger = logger;
  28. }
  29. /// <summary>
  30. /// 解析 RoleCode / RoleId 混合 token 列表为去重 UserId 列表。
  31. ///
  32. /// <b>S8-ROLE-TENANT-SCOPE-1(P0 SECURITY):租户必须由调用方显式传入。</b>
  33. /// 原实现只按 Code / Id 匹配 <c>SysRole</c>,依赖平台的全局租户过滤器兜底;
  34. /// 但该过滤器只在有登录上下文时挂载(<c>SqlSugarSetup</c>:<c>App.User</c> 为空则不挂、超管直接跳过),
  35. /// 而 <c>S8WatchSchedulerJob</c> 走 <c>IServiceScopeFactory.CreateScope()</c>,**没有 App.User**。
  36. /// 又因 <c>SysRole.Code</c> 跨租户重复(每个业务角色每租户一份,实测 9 份),
  37. /// 后台派发时 "ROLE_PURCHASER" 会解析到全部租户的角色 → 跨租户通知投递。
  38. ///
  39. /// 因此租户在此作为**显式安全边界**,不依赖任何隐式过滤:
  40. /// Code 路径与数字 RoleId 路径**都**必须满足 <c>TenantId == tenantId</c>,
  41. /// 后者可防止调用方(或被篡改的配置)传入他租户的 RoleId 绕过边界。
  42. /// </summary>
  43. /// <param name="tenantId">安全边界租户。必须 &gt; 0,否则直接返回空集合(绝不放行全量)。</param>
  44. /// <param name="roleTokens">RoleCode / RoleId 混合 token。</param>
  45. public async Task<List<long>> ResolveUserIdsAsync(long tenantId, IEnumerable<string>? roleTokens)
  46. {
  47. var result = new HashSet<long>();
  48. if (roleTokens == null) return result.ToList();
  49. // 作用域缺失时宁可不通知,也不返回跨租户用户。
  50. if (tenantId <= 0)
  51. {
  52. _logger.LogWarning("S8RoleResolver: tenantId<=0,拒绝解析角色(避免跨租户投递)");
  53. return result.ToList();
  54. }
  55. var requestedIds = new HashSet<long>();
  56. var codes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  57. foreach (var raw in roleTokens)
  58. {
  59. if (string.IsNullOrWhiteSpace(raw)) continue;
  60. var t = raw.Trim();
  61. if (long.TryParse(t, out var rid) && rid > 0) requestedIds.Add(rid);
  62. else codes.Add(t);
  63. }
  64. // 经租户校验后的角色 Id,只有进入这里的才允许展开成用户。
  65. var roleIds = new HashSet<long>();
  66. if (codes.Count > 0)
  67. {
  68. try
  69. {
  70. var codeList = codes.ToList();
  71. var found = await _sysRoleRep.AsQueryable()
  72. .Where(r => codeList.Contains(r.Code) && r.TenantId == tenantId)
  73. .Select(r => new { r.Id, r.Code })
  74. .ToListAsync();
  75. var foundCodes = new HashSet<string>(found.Select(f => f.Code ?? string.Empty), StringComparer.OrdinalIgnoreCase);
  76. foreach (var c in codes)
  77. {
  78. if (!foundCodes.Contains(c))
  79. _logger.LogWarning("S8RoleResolver: RoleCode '{Code}' not found in SysRole for tenant {TenantId}", c, tenantId);
  80. }
  81. foreach (var f in found) roleIds.Add(f.Id);
  82. }
  83. catch (Exception ex)
  84. {
  85. _logger.LogWarning(ex, "S8RoleResolver: resolve RoleCode batch failed");
  86. }
  87. }
  88. if (requestedIds.Count > 0)
  89. {
  90. try
  91. {
  92. // 数字 RoleId 同样过租户校验:他租户 Id 一律丢弃并告警,不静默放行。
  93. var idList = requestedIds.ToList();
  94. var ownedIds = await _sysRoleRep.AsQueryable()
  95. .Where(r => idList.Contains(r.Id) && r.TenantId == tenantId)
  96. .Select(r => r.Id)
  97. .ToListAsync();
  98. var owned = new HashSet<long>(ownedIds);
  99. foreach (var rid in requestedIds)
  100. {
  101. if (owned.Contains(rid)) roleIds.Add(rid);
  102. else _logger.LogWarning("S8RoleResolver: RoleId {RoleId} 不属于租户 {TenantId},已丢弃", rid, tenantId);
  103. }
  104. }
  105. catch (Exception ex)
  106. {
  107. _logger.LogWarning(ex, "S8RoleResolver: resolve RoleId batch failed");
  108. }
  109. }
  110. foreach (var rid in roleIds)
  111. {
  112. try
  113. {
  114. var ids = await _sysUserRoleService.GetUserIdList(rid);
  115. foreach (var uid in ids)
  116. if (uid > 0) result.Add(uid);
  117. }
  118. catch (Exception ex)
  119. {
  120. _logger.LogWarning(ex, "S8RoleResolver: GetUserIdList(roleId={RoleId}) failed", rid);
  121. }
  122. }
  123. return result.ToList();
  124. }
  125. /// <summary>
  126. /// 将 ado_s8_notification_layer.target_role_ids 这类 CSV 字段拆为 token 列表。
  127. /// 兼容 ',' 和 ';' 分隔,trim + 去空。
  128. /// </summary>
  129. public static List<string> SplitTokens(string? csv)
  130. {
  131. if (string.IsNullOrWhiteSpace(csv)) return new List<string>();
  132. return csv.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
  133. }
  134. }