| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- using Admin.NET.Core;
- using Admin.NET.Plugin.AiDOP.Const.S8;
- using Admin.NET.Plugin.AiDOP.Entity.S8;
- using Microsoft.Extensions.Logging;
- namespace Admin.NET.Plugin.AiDOP.Service.S8;
- /// <summary>供给结果。分项计数,运维要能看出「这次到底做了什么」。</summary>
- public sealed class S8ActionRoleProvisioningResult
- {
- public int TenantCount { get; set; }
- public int ActionCount { get; set; }
- /// <summary>新建的授权行数。</summary>
- public int CreatedCount { get; set; }
- /// <summary>已有授权、本次跳过的租户数(只对<b>完全没有任何授权行</b>的租户做首次供给)。</summary>
- public int SkippedTenantCount { get; set; }
- /// <summary>推导后仍然一个角色都没有的 (租户, 动作) 数 —— 这些动作保持关闭,需人工配置。</summary>
- public int UnconfiguredCount { get; set; }
- }
- /// <summary>
- /// S8-ACTION-PERMISSION-1:把「本租户原有的 S8 权限语义」一次性投射成动作授权矩阵的初值。
- ///
- /// <para><b>只做首次供给</b>:一个租户只要已经存在<b>任意一行</b>授权,本服务就整体跳过它。
- /// 理由是这张表随后由管理员在页面上维护 —— 每次启动都去"补齐默认"会把管理员
- /// 刻意取消的授权悄悄加回来,那是比缺权限更难发现的故障(页面显示没配,实际有权)。</para>
- ///
- /// <para><b>跨租户角色不带入</b>:推导链是
- /// <c>SysRole(TenantId == 目标租户) → SysRoleMenu → SysMenu(Btn).Permission</c>。
- /// 真库里 <c>UATAdminA</c> 经别家租户的 <c>ROLE_S6_IPQC_SUPERVISOR</c> 持有
- /// <c>s8:exception:assign</c>,该角色因租户不符<b>不会</b>进入本租户的转派授权 ——
- /// 迁移后它失去转派能力属预期修复结果。</para>
- ///
- /// <para><b>推不出角色的动作留空</b>(如上述租户的「转派」),保持 fail closed。
- /// 不给兜底角色:那等于把刚堵上的越权换个形式放回去。</para>
- /// </summary>
- public class S8ExceptionActionRoleProvisioningService : ITransient
- {
- private readonly SqlSugarRepository<AdoS8ExceptionActionRole> _rep;
- private readonly ILogger<S8ExceptionActionRoleProvisioningService> _logger;
- public S8ExceptionActionRoleProvisioningService(
- SqlSugarRepository<AdoS8ExceptionActionRole> rep,
- ILogger<S8ExceptionActionRoleProvisioningService> logger)
- {
- _rep = rep;
- _logger = logger;
- }
- public async Task<S8ActionRoleProvisioningResult> SyncAsync(CancellationToken ct = default)
- {
- // 「租户有效」沿用本仓既有口径(Status = Enable),与 S8RuleProvisioningService 一致。
- var tenantIds = await _rep.Context.Queryable<SysTenant>()
- .Where(t => t.Status == StatusEnum.Enable)
- .Select(t => t.Id)
- .ToListAsync(ct);
- var result = new S8ActionRoleProvisioningResult
- {
- TenantCount = tenantIds.Count,
- ActionCount = S8ExceptionActionCatalog.All.Count
- };
- if (tenantIds.Count == 0) return result;
- var tenantsWithRows = (await _rep.AsQueryable().ClearFilter()
- .Where(x => tenantIds.Contains(x.TenantId))
- .Select(x => x.TenantId)
- .Distinct()
- .ToListAsync(ct)).ToHashSet();
- var pending = tenantIds.Where(t => !tenantsWithRows.Contains(t)).ToList();
- result.SkippedTenantCount = tenantIds.Count - pending.Count;
- if (pending.Count == 0)
- {
- _logger.LogInformation(
- "s8_action_role_provisioning_skipped tenants={Tenants}(全部已有授权配置)", tenantIds.Count);
- return result;
- }
- // 一次取回待供给租户的 (租户, 角色, 能力码) 三元组。角色租户在 join 条件里就已经限死。
- var grants = await _rep.Context.Queryable<SysRole>().ClearFilter()
- .InnerJoin<SysRoleMenu>((r, rm) => r.Id == rm.RoleId)
- .InnerJoin<SysMenu>((r, rm, m) => rm.MenuId == m.Id)
- .Where((r, rm, m) => r.TenantId != null && pending.Contains(r.TenantId.Value)
- && m.Type == MenuTypeEnum.Btn
- && m.Permission != null
- && m.Permission.StartsWith("s8:"))
- .Select((r, rm, m) => new { TenantId = r.TenantId!.Value, RoleId = r.Id, m.Permission })
- .ToListAsync(ct);
- var now = DateTime.Now;
- var inserts = new List<AdoS8ExceptionActionRole>();
- foreach (var tenantId in pending)
- {
- var tenantGrants = grants.Where(g => g.TenantId == tenantId).ToList();
- foreach (var action in S8ExceptionActionCatalog.All)
- {
- var codes = new List<string>();
- if (!string.IsNullOrWhiteSpace(action.LegacyPermissionCode)) codes.Add(action.LegacyPermissionCode!);
- codes.AddRange(action.LegacyPermissionAliases);
- // 旧体系里没有对应能力码的动作(ViewAll)无从推导 —— 留空,由管理员显式配置。
- if (codes.Count == 0) { result.UnconfiguredCount++; continue; }
- var roleIds = tenantGrants
- .Where(g => codes.Contains(g.Permission!, StringComparer.OrdinalIgnoreCase))
- .Select(g => g.RoleId)
- .Distinct()
- .ToList();
- if (roleIds.Count == 0) { result.UnconfiguredCount++; continue; }
- inserts.AddRange(roleIds.Select(rid => new AdoS8ExceptionActionRole
- {
- TenantId = tenantId,
- ActionCode = action.Code,
- RoleId = rid,
- CreatedAt = now,
- CreatedBy = "PROVISIONING"
- }));
- }
- }
- if (inserts.Count > 0)
- {
- await _rep.AsInsertable(inserts).ExecuteCommandAsync();
- result.CreatedCount = inserts.Count;
- }
- _logger.LogInformation(
- "s8_action_role_provisioning_done tenants={Tenants} provisioned={Pending} skipped={Skipped} "
- + "actions={Actions} created={Created} unconfigured={Unconfigured}",
- result.TenantCount, pending.Count, result.SkippedTenantCount,
- result.ActionCount, result.CreatedCount, result.UnconfiguredCount);
- if (result.UnconfiguredCount > 0)
- _logger.LogWarning(
- "s8_action_role_unconfigured count={Count};这些 (租户, 动作) 推导不出任何本租户角色,"
- + "按 fail-closed 保持关闭,需管理员在「异常操作权限」页面显式配置",
- result.UnconfiguredCount);
- return result;
- }
- }
|