using Admin.NET.Core;
using Admin.NET.Core.Service;
using Admin.NET.Plugin.AiDOP.Const.S8;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
namespace Admin.NET.Plugin.AiDOP.Infrastructure.S8;
///
/// S8-P0-4-API-AUTHORIZATION-1:S8 接口能力级授权门。
///
/// 为什么不能直接复用平台的路由名匹配(这是本方案的唯一设计前提,先读这段):
/// 的 JwtHandler.CheckAuthorizeAsync 把
/// HttpContext.Request.Path 逐字转成权限名(/ → :)再与
/// SysMenu.Permission 比对。平台自身接口全是 /api/sysPos/delete 这类
/// 无路径参数的动词式路由,于是 sysPos:delete 天然对得上。
///
/// S8 是 RESTful 路由,两点结构性不兼容:
///
/// - 路径参数:/api/aidop/s8/exceptions/1329909430019/claim 会被转成
/// aidop:s8:exceptions:1329909430019:claim —— 含实例 id,
/// 任何静态权限码永远不可能相等;
/// - 命名体系:S8 权限码是能力语义(s8:exception:claim),
/// 而非路由字面量(aidop:s8:exceptions:…)。
///
///
/// 再加上 JwtHandler 末行 allBtnPermList.TrueForAll(u => !routeName.Equals(u))
/// —— 未登记路由默认放行,于是全部 S8 接口实测为「登录即可调用」:
/// 2026-09-02 以业务账号 UATExceptionA 实测 PUT /config/watch-rules/{id}/params
/// 与 POST /config/operator-bindings 均直达业务校验返回 400(而非 403)。
///
/// 本类做什么:只替换「路由名 → 权限码」这一步为显式声明,
/// 其余全部复用平台既有设施 —— 权限仍存 (Type=Btn).Permission,
/// 仍经 SysRoleMenu 授予角色,仍由平台的
/// (含缓存)取用户权限集。
/// 不新造权限存储、不改 JwtHandler(那是所有模块共用的平台代码,改它会外溢到 S0–S7)。
///
/// 失败语义:未持有声明的权限码 → 直接 403,
/// 在进入 Controller Action 与任何 Service 之前返回,DB 零写入。
///
/// 超管豁免:与 JwtHandler.CheckAuthorizeAsync 完全同口径
/// (),避免出现「平台放行、S8 拦截」的不一致。
///
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class S8PermissionAttribute : Attribute, IAsyncAuthorizationFilter
{
/// 本 Action 要求的能力码,必须取自 常量。
public string Code { get; }
public S8PermissionAttribute(string code)
{
if (string.IsNullOrWhiteSpace(code))
throw new ArgumentException("S8 权限码不能为空", nameof(code));
Code = code;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
// 认证本身由平台管道负责;这里只判「已认证身份是否具备该能力」。
// 未认证时交回平台的 401 语义,不在此重复判定。
var user = context.HttpContext.User;
if (user?.Identity?.IsAuthenticated != true) return;
var services = context.HttpContext.RequestServices;
// 超管豁免:与 JwtHandler 同口径。
var userManager = services.GetService();
if (userManager is { SuperAdmin: true }) return;
var menuService = services.GetService();
if (menuService == null)
{
// 拿不到平台权限服务时按拒绝处理(fail-closed)。
// 绝不 fail-open —— 本模块的事故类型全部是「本不该放行却放行了」。
context.Result = Forbid();
return;
}
List owned;
try
{
owned = await menuService.GetOwnBtnPermList();
}
catch
{
context.Result = Forbid();
return;
}
if (owned != null && owned.Contains(Code, StringComparer.OrdinalIgnoreCase)) return;
context.Result = Forbid();
}
///
/// 统一 403 出参。文案不回显所需权限码与用户已有权限,避免把权限拓扑透给调用方。
///
private static ObjectResult Forbid() => new(new { message = "没有权限执行该操作" })
{
StatusCode = StatusCodes.Status403Forbidden
};
}