S8ExceptionActionAttribute.cs 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. using Admin.NET.Core;
  2. using Admin.NET.Plugin.AiDOP.Const.S8;
  3. using Microsoft.AspNetCore.Http;
  4. using Microsoft.AspNetCore.Mvc;
  5. using Microsoft.AspNetCore.Mvc.Filters;
  6. using Microsoft.Extensions.DependencyInjection;
  7. namespace Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  8. /// <summary>
  9. /// S8-ACTION-PERMISSION-1:异常单动作的授权门。取代这些 Action 上原有的
  10. /// <see cref="S8PermissionAttribute"/>。
  11. ///
  12. /// <para><b>为什么要换掉</b>:<see cref="S8PermissionAttribute"/> 依赖平台的
  13. /// <c>GetOwnBtnPermList()</c>,而那条链<b>不校验角色的租户</b>,实测可被别家租户的角色
  14. /// 带进 <c>s8:exception:assign</c> 等能力(取证见 <see cref="S8TenantRoleResolver"/>)。
  15. /// 同一个问题不能有两个 authority,所以异常动作统一改由
  16. /// <see cref="IS8ExceptionActionAuthorizer"/> 回答;<see cref="S8PermissionAttribute"/>
  17. /// 继续负责配置面(<c>s8:config:*</c>),并同样补上租户交集。</para>
  18. ///
  19. /// <para><b>仍在 Action 之前返回 403</b>:与旧实现同一位置、同一文案,
  20. /// DB 零写入,不把权限拓扑回显给调用方。</para>
  21. /// </summary>
  22. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
  23. public sealed class S8ExceptionActionAttribute : Attribute, IAsyncAuthorizationFilter
  24. {
  25. /// <summary>本 Action 要求的动作码,必须取自 <see cref="S8ExceptionActionCode"/>。</summary>
  26. public string Code { get; }
  27. public S8ExceptionActionAttribute(string code)
  28. {
  29. if (string.IsNullOrWhiteSpace(code))
  30. throw new ArgumentException("S8 动作码不能为空", nameof(code));
  31. if (!S8ExceptionActionCatalog.IsKnown(code))
  32. throw new ArgumentException($"S8 动作码不在目录中:{code}", nameof(code));
  33. Code = code;
  34. }
  35. public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
  36. {
  37. var user = context.HttpContext.User;
  38. if (user?.Identity?.IsAuthenticated != true) return; // 401 交回平台管道
  39. var services = context.HttpContext.RequestServices;
  40. // 超管豁免:与 JwtHandler.CheckAuthorizeAsync / S8PermissionAttribute 同口径。
  41. var userManager = services.GetService<UserManager>();
  42. if (userManager is { SuperAdmin: true }) return;
  43. var authorizer = services.GetService<IS8ExceptionActionAuthorizer>();
  44. var scopeResolver = services.GetService<S8TrustedScopeResolver>();
  45. if (authorizer == null || scopeResolver == null || userManager == null)
  46. {
  47. // 拿不到判定所需服务时按拒绝处理。绝不 fail-open ——
  48. // 本模块的事故类型全部是「本不该放行却放行了」。
  49. context.Result = Forbid();
  50. return;
  51. }
  52. try
  53. {
  54. var scope = await scopeResolver.ResolveAsync();
  55. var result = await authorizer.AuthorizeAsync(scope.TenantId, userManager.UserId, Code);
  56. if (result.Allowed) return;
  57. }
  58. catch
  59. {
  60. // 作用域解析失败(如超管未选租户)同样按拒绝处理,语义与既有 400/403 边界一致。
  61. }
  62. context.Result = Forbid();
  63. }
  64. private static ObjectResult Forbid() => new(new { message = "没有权限执行该操作" })
  65. {
  66. StatusCode = StatusCodes.Status403Forbidden
  67. };
  68. }