| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- using Admin.NET.Core;
- using Admin.NET.Plugin.AiDOP.Const.S8;
- using Admin.NET.Plugin.AiDOP.Entity.S8;
- namespace Admin.NET.Plugin.AiDOP.Infrastructure.S8;
- /// <summary>授权判定结果。拒绝时带原因码,便于前端与日志区分「没配」与「没权」。</summary>
- public sealed record S8ActionAuthorizationResult(bool Allowed, string? ReasonCode, string? Message)
- {
- public static S8ActionAuthorizationResult Pass() => new(true, null, null);
- public static S8ActionAuthorizationResult Deny(string reason, string message) => new(false, reason, message);
- }
- /// <summary>授权失败原因码。</summary>
- public static class S8ActionDenyReason
- {
- /// <summary>动作码不在目录里 —— 调用方拼错或用了已删除的动作。</summary>
- public const string UnknownAction = "action_unknown";
- /// <summary>该动作在本租户下一个角色都没配 —— 未配置即禁止,不是「未配置即放行」。</summary>
- public const string ActionNotConfigured = "action_not_configured";
- /// <summary>账号在本租户下的角色都不在该动作的授权名单里。</summary>
- public const string RoleNotGranted = "action_role_not_granted";
- }
- /// <summary>
- /// S8-ACTION-PERMISSION-1:异常单动作授权的<b>唯一</b>判定入口。
- ///
- /// <para><b>禁止各 Service 自己散落 HasRole 判断</b>。同一个问题一旦有两处实现,
- /// 迟早出现「Controller 放行、Service 又拦一次且判据不同」或反之 ——
- /// 本仓在 <c>S8OperatorBindingService</c> 上已经发生过一次同类事故
- /// (List 用 ClearFilter + 可选谓词、Bind 用另一套口径,导致跨租户返回 500 行)。</para>
- ///
- /// <para><b>判据三段,缺一不可</b>:
- /// ① 动作在目录内;② 该动作在本租户配了至少一个角色;
- /// ③ 账号在<b>本租户</b>持有其中任一角色。</para>
- ///
- /// <para><b>fail closed 是硬要求</b>:配置表为空<b>不等于</b>所有人拥有全部权限。
- /// 这类"空即放行"的默认值是权限系统最典型的事故来源 —— 上线时表还没灌数据,
- /// 于是全员拥有全部动作,而且没有任何报错。</para>
- ///
- /// <para><b>Factory 不参与</b>,且不得加回来。<b>Employee 不参与</b>:
- /// S8 的人就是 <c>SysUser</c>,不存在「先查员工再查账号」。</para>
- /// </summary>
- public interface IS8ExceptionActionAuthorizer
- {
- /// <summary>判定账号在租户内是否可执行该动作。不抛异常。</summary>
- Task<S8ActionAuthorizationResult> AuthorizeAsync(long tenantId, long userId, string actionCode);
- /// <summary>批量取该账号在本租户被允许的全部动作码(供前端按钮可见性使用)。</summary>
- Task<IReadOnlySet<string>> GetAllowedActionsAsync(long tenantId, long userId);
- }
- /// <summary>正式实现。生产代码里只有这一份;接口只为测试留 seam。</summary>
- public sealed class S8ExceptionActionAuthorizer : IS8ExceptionActionAuthorizer, ITransient
- {
- private readonly SqlSugarRepository<AdoS8ExceptionActionRole> _rep;
- private readonly IS8TenantRoleResolver _roleResolver;
- public S8ExceptionActionAuthorizer(
- SqlSugarRepository<AdoS8ExceptionActionRole> rep,
- IS8TenantRoleResolver roleResolver)
- {
- _rep = rep;
- _roleResolver = roleResolver;
- }
- public async Task<S8ActionAuthorizationResult> AuthorizeAsync(long tenantId, long userId, string actionCode)
- {
- // ① 未知动作:拒绝。绝不因为「没听说过这个动作」就放行。
- if (!S8ExceptionActionCatalog.IsKnown(actionCode))
- return S8ActionAuthorizationResult.Deny(S8ActionDenyReason.UnknownAction, "没有权限执行该操作");
- if (tenantId <= 0 || userId <= 0)
- return S8ActionAuthorizationResult.Deny(S8ActionDenyReason.RoleNotGranted, "没有权限执行该操作");
- var grantedRoleIds = await GrantedRoleIdsAsync(tenantId, actionCode);
- // ② 该动作在本租户一个角色都没配 —— fail closed。
- if (grantedRoleIds.Count == 0)
- return S8ActionAuthorizationResult.Deny(
- S8ActionDenyReason.ActionNotConfigured, "没有权限执行该操作");
- // ③ 账号在本租户持有的角色(跨租户角色在这里就已经被排除,见 S8TenantRoleResolver)。
- var userRoleIds = await _roleResolver.GetRoleIdsAsync(tenantId, userId);
- if (userRoleIds.Count == 0 || !userRoleIds.Any(grantedRoleIds.Contains))
- return S8ActionAuthorizationResult.Deny(
- S8ActionDenyReason.RoleNotGranted, "没有权限执行该操作");
- return S8ActionAuthorizationResult.Pass();
- }
- public async Task<IReadOnlySet<string>> GetAllowedActionsAsync(long tenantId, long userId)
- {
- var allowed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
- if (tenantId <= 0 || userId <= 0) return allowed;
- var userRoleIds = await _roleResolver.GetRoleIdsAsync(tenantId, userId);
- if (userRoleIds.Count == 0) return allowed;
- var rows = await _rep.AsQueryable().ClearFilter()
- .Where(x => x.TenantId == tenantId && userRoleIds.Contains(x.RoleId))
- .Select(x => x.ActionCode)
- .ToListAsync();
- foreach (var code in rows.Where(S8ExceptionActionCatalog.IsKnown))
- allowed.Add(code);
- return allowed;
- }
- private async Task<HashSet<long>> GrantedRoleIdsAsync(long tenantId, string actionCode)
- {
- // ClearFilter:边界由本查询显式声明(tenant_id == 传入租户)。
- var ids = await _rep.AsQueryable().ClearFilter()
- .Where(x => x.TenantId == tenantId && x.ActionCode == actionCode)
- .Select(x => x.RoleId)
- .ToListAsync();
- return ids.ToHashSet();
- }
- }
|