S8ExceptionActionRoleProvisioningService.cs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. using Admin.NET.Core;
  2. using Admin.NET.Plugin.AiDOP.Const.S8;
  3. using Admin.NET.Plugin.AiDOP.Entity.S8;
  4. using Microsoft.Extensions.Logging;
  5. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  6. /// <summary>供给结果。分项计数,运维要能看出「这次到底做了什么」。</summary>
  7. public sealed class S8ActionRoleProvisioningResult
  8. {
  9. public int TenantCount { get; set; }
  10. public int ActionCount { get; set; }
  11. /// <summary>新建的授权行数。</summary>
  12. public int CreatedCount { get; set; }
  13. /// <summary>已有授权、本次跳过的租户数(只对<b>完全没有任何授权行</b>的租户做首次供给)。</summary>
  14. public int SkippedTenantCount { get; set; }
  15. /// <summary>推导后仍然一个角色都没有的 (租户, 动作) 数 —— 这些动作保持关闭,需人工配置。</summary>
  16. public int UnconfiguredCount { get; set; }
  17. }
  18. /// <summary>
  19. /// S8-ACTION-PERMISSION-1:把「本租户原有的 S8 权限语义」一次性投射成动作授权矩阵的初值。
  20. ///
  21. /// <para><b>只做首次供给</b>:一个租户只要已经存在<b>任意一行</b>授权,本服务就整体跳过它。
  22. /// 理由是这张表随后由管理员在页面上维护 —— 每次启动都去"补齐默认"会把管理员
  23. /// 刻意取消的授权悄悄加回来,那是比缺权限更难发现的故障(页面显示没配,实际有权)。</para>
  24. ///
  25. /// <para><b>跨租户角色不带入</b>:推导链是
  26. /// <c>SysRole(TenantId == 目标租户) → SysRoleMenu → SysMenu(Btn).Permission</c>。
  27. /// 真库里 <c>UATAdminA</c> 经别家租户的 <c>ROLE_S6_IPQC_SUPERVISOR</c> 持有
  28. /// <c>s8:exception:assign</c>,该角色因租户不符<b>不会</b>进入本租户的转派授权 ——
  29. /// 迁移后它失去转派能力属预期修复结果。</para>
  30. ///
  31. /// <para><b>推不出角色的动作留空</b>(如上述租户的「转派」),保持 fail closed。
  32. /// 不给兜底角色:那等于把刚堵上的越权换个形式放回去。</para>
  33. /// </summary>
  34. public class S8ExceptionActionRoleProvisioningService : ITransient
  35. {
  36. private readonly SqlSugarRepository<AdoS8ExceptionActionRole> _rep;
  37. private readonly ILogger<S8ExceptionActionRoleProvisioningService> _logger;
  38. public S8ExceptionActionRoleProvisioningService(
  39. SqlSugarRepository<AdoS8ExceptionActionRole> rep,
  40. ILogger<S8ExceptionActionRoleProvisioningService> logger)
  41. {
  42. _rep = rep;
  43. _logger = logger;
  44. }
  45. public async Task<S8ActionRoleProvisioningResult> SyncAsync(CancellationToken ct = default)
  46. {
  47. // 「租户有效」沿用本仓既有口径(Status = Enable),与 S8RuleProvisioningService 一致。
  48. var tenantIds = await _rep.Context.Queryable<SysTenant>()
  49. .Where(t => t.Status == StatusEnum.Enable)
  50. .Select(t => t.Id)
  51. .ToListAsync(ct);
  52. var result = new S8ActionRoleProvisioningResult
  53. {
  54. TenantCount = tenantIds.Count,
  55. ActionCount = S8ExceptionActionCatalog.All.Count
  56. };
  57. if (tenantIds.Count == 0) return result;
  58. var tenantsWithRows = (await _rep.AsQueryable().ClearFilter()
  59. .Where(x => tenantIds.Contains(x.TenantId))
  60. .Select(x => x.TenantId)
  61. .Distinct()
  62. .ToListAsync(ct)).ToHashSet();
  63. var pending = tenantIds.Where(t => !tenantsWithRows.Contains(t)).ToList();
  64. result.SkippedTenantCount = tenantIds.Count - pending.Count;
  65. if (pending.Count == 0)
  66. {
  67. _logger.LogInformation(
  68. "s8_action_role_provisioning_skipped tenants={Tenants}(全部已有授权配置)", tenantIds.Count);
  69. return result;
  70. }
  71. // 一次取回待供给租户的 (租户, 角色, 能力码) 三元组。角色租户在 join 条件里就已经限死。
  72. var grants = await _rep.Context.Queryable<SysRole>().ClearFilter()
  73. .InnerJoin<SysRoleMenu>((r, rm) => r.Id == rm.RoleId)
  74. .InnerJoin<SysMenu>((r, rm, m) => rm.MenuId == m.Id)
  75. .Where((r, rm, m) => r.TenantId != null && pending.Contains(r.TenantId.Value)
  76. && m.Type == MenuTypeEnum.Btn
  77. && m.Permission != null
  78. && m.Permission.StartsWith("s8:"))
  79. .Select((r, rm, m) => new { TenantId = r.TenantId!.Value, RoleId = r.Id, m.Permission })
  80. .ToListAsync(ct);
  81. var now = DateTime.Now;
  82. var inserts = new List<AdoS8ExceptionActionRole>();
  83. foreach (var tenantId in pending)
  84. {
  85. var tenantGrants = grants.Where(g => g.TenantId == tenantId).ToList();
  86. foreach (var action in S8ExceptionActionCatalog.All)
  87. {
  88. var codes = new List<string>();
  89. if (!string.IsNullOrWhiteSpace(action.LegacyPermissionCode)) codes.Add(action.LegacyPermissionCode!);
  90. codes.AddRange(action.LegacyPermissionAliases);
  91. // 旧体系里没有对应能力码的动作(ViewAll)无从推导 —— 留空,由管理员显式配置。
  92. if (codes.Count == 0) { result.UnconfiguredCount++; continue; }
  93. var roleIds = tenantGrants
  94. .Where(g => codes.Contains(g.Permission!, StringComparer.OrdinalIgnoreCase))
  95. .Select(g => g.RoleId)
  96. .Distinct()
  97. .ToList();
  98. if (roleIds.Count == 0) { result.UnconfiguredCount++; continue; }
  99. inserts.AddRange(roleIds.Select(rid => new AdoS8ExceptionActionRole
  100. {
  101. TenantId = tenantId,
  102. ActionCode = action.Code,
  103. RoleId = rid,
  104. CreatedAt = now,
  105. CreatedBy = "PROVISIONING"
  106. }));
  107. }
  108. }
  109. if (inserts.Count > 0)
  110. {
  111. await _rep.AsInsertable(inserts).ExecuteCommandAsync();
  112. result.CreatedCount = inserts.Count;
  113. }
  114. _logger.LogInformation(
  115. "s8_action_role_provisioning_done tenants={Tenants} provisioned={Pending} skipped={Skipped} "
  116. + "actions={Actions} created={Created} unconfigured={Unconfigured}",
  117. result.TenantCount, pending.Count, result.SkippedTenantCount,
  118. result.ActionCount, result.CreatedCount, result.UnconfiguredCount);
  119. if (result.UnconfiguredCount > 0)
  120. _logger.LogWarning(
  121. "s8_action_role_unconfigured count={Count};这些 (租户, 动作) 推导不出任何本租户角色,"
  122. + "按 fail-closed 保持关闭,需管理员在「异常操作权限」页面显式配置",
  123. result.UnconfiguredCount);
  124. return result;
  125. }
  126. }