using System.Collections.Concurrent; using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace MiGu.Server.Controllers; /// /// 运维白名单网关(占位)。真实落地时按架构 §5.1 + §10.5: /// - 校验 scope=RCSMonitor 是否允许该 op; /// - YARP 转发到 SimpleLite /api/ops/*; /// - 写 OpsAuditLog(simple_main.db) + 写 OpsLogs(platform.db)。 /// /// AR-4: 全 class 加 [Authorize] —— 至少要求登录,再按 op 白名单 + JWT ops claim 双校验。 /// [ApiController] [Authorize] [Route("api/sl/ops")] public class OpsController : ControllerBase { public record ExecuteRequest(string OpCode, string TargetId, string? Reason, string? IdempotencyKey); public record ExecuteResponse(bool Ok, string AuditId, string? Message); public record AuditEntry(string Id, DateTimeOffset Ts, string User, string Scope, string OpCode, string Target, string Result, string? Message); private static readonly ConcurrentQueue Audits = new(); private static long _seq = 0; private static readonly HashSet Whitelist = new(StringComparer.Ordinal) { "ops.car.pause", "ops.car.resume", "ops.car.gohome", "ops.car.resetSession", "ops.car.manualCharge", "ops.task.pause", "ops.task.cancel", "ops.task.reassign", "ops.task.boostPriority", "monitor.note.write" }; [HttpPost("execute")] public ActionResult Execute([FromBody] ExecuteRequest req) { if (!Whitelist.Contains(req.OpCode)) return BadRequest(new { message = $"非白名单 op:{req.OpCode}" }); // AR-4: JWT ops claim 二次校验 —— JWT 颁发时已写入用户被授权的 ops 列表(空格分隔), // 这里要求 (op 在白名单) AND (op 在用户 ops claim);admin 的 "*" 会被特判通过。 var opsClaim = User.FindFirst("ops")?.Value ?? ""; var userOps = opsClaim.Split(' ', StringSplitOptions.RemoveEmptyEntries); if (!userOps.Contains("*") && !userOps.Contains(req.OpCode)) return StatusCode(403, new { message = $"当前账号无权执行 {req.OpCode}" }); var user = User.Identity?.Name ?? User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous"; var scope = User.FindFirst("scope")?.Value ?? "unknown"; var id = $"A{Interlocked.Increment(ref _seq):D6}"; var entry = new AuditEntry(id, DateTimeOffset.UtcNow, user, scope, req.OpCode, req.TargetId, "ok", req.Reason); Audits.Enqueue(entry); while (Audits.Count > 200 && Audits.TryDequeue(out _)) { } return Ok(new ExecuteResponse(true, id, null)); } [HttpGet("audits")] public IActionResult Audits200() { return Ok(Audits.Reverse()); } }