feat: complete task board workflow gates
CI - Build & Test / Backend (.NET) (push) Failing after 31s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 20s
CI - Build & Test / Security Check (push) Successful in 3s

This commit is contained in:
2026-06-24 01:22:13 +02:00
parent 68b428e411
commit 95495a8332
19 changed files with 1064 additions and 144 deletions
+17 -22
View File
@@ -10,7 +10,7 @@ namespace Nexus.Api.Controllers;
[Authorize]
[ApiController]
[Route("api/v1/tasks")]
public class TasksController(ITaskService taskService, IAgentService agentService) : ControllerBase
public class TasksController(ITaskService taskService, IAgentService agentService, IConfiguration configuration) : ControllerBase
{
[HttpGet]
public async Task<IResult> GetAll(CancellationToken ct)
@@ -117,12 +117,12 @@ public class TasksController(ITaskService taskService, IAgentService agentServic
/// X-Nexus-Api-Key / JWT. Kein [AllowAnonymous] mehr.
/// Für Agent-zu-Agent-Kommunikation den /api/bridge/board Endpunkt nutzen.
/// </summary>
[AllowAnonymous]
[HttpGet("board")]
public async Task<IResult> GetBoard(CancellationToken ct)
{
// Erfordert mindestens einen identifizierbaren Agent-Aufrufer
var agentHeader = await GetAllowedAgentHeaderAsync(ct);
var isApiKey = HttpContext.User.IsInRole("Service");
var agentHeader = await RequestAuthorizationHelper.ResolveAllowedAgentHeaderAsync(HttpContext, agentService, ct);
var isApiKey = RequestAuthorizationHelper.IsAuthenticatedService(HttpContext, configuration);
var isAuth = HttpContext.User.Identity?.IsAuthenticated == true;
if (string.IsNullOrWhiteSpace(agentHeader) && !isApiKey && !isAuth)
@@ -136,33 +136,28 @@ public class TasksController(ITaskService taskService, IAgentService agentServic
/// Wird vom Iris Autonomous Worker genutzt.
///
/// SICHERHEIT: Erfordert X-Agent-Id Header (nur iris) ODER
/// X-Nexus-Api-Key / JWT-authenticated user.
/// X-Nexus-Api-Key / Service-Principal ODER owner/admin JWT.
/// Für Agent-zu-Agent-Kommunikation den /api/bridge Endpunkt nutzen.
/// </summary>
[AllowAnonymous]
[HttpPost("reset-stale")]
public async Task<IResult> ResetStale([FromBody] ResetStaleRequest request, CancellationToken ct)
{
var agentHeader = await GetAllowedAgentHeaderAsync(ct);
var isApiKey = HttpContext.User.IsInRole("Service");
var isAuth = HttpContext.User.Identity?.IsAuthenticated == true;
var agentHeaderResolution = await RequestAuthorizationHelper.ResolveAgentHeaderAsync(HttpContext, agentService, ct);
var isService = RequestAuthorizationHelper.IsAuthenticatedService(HttpContext, configuration);
var isPrivilegedUser = RequestAuthorizationHelper.IsPrivilegedUser(HttpContext);
var isIris = string.Equals(agentHeaderResolution.AgentId, "iris", StringComparison.OrdinalIgnoreCase);
if (!isIris && !isService && !isPrivilegedUser)
{
// A presented but unrecognized agent header is an invalid credential, not a missing one.
if (HttpContext.User.Identity?.IsAuthenticated == true || agentHeaderResolution.HeaderProvided)
return Results.Forbid();
// Nur iris, nexus-system (ApiKey) oder JWT-authenticated user
var isIris = string.Equals(agentHeader, "iris", StringComparison.OrdinalIgnoreCase);
if (!isIris && !isApiKey && !isAuth)
return Results.Unauthorized();
}
var count = await taskService.ResetStaleAsync(request.StaleHours, ct);
return Results.Ok(new ResetStaleResponse(count));
}
private async Task<string?> GetAllowedAgentHeaderAsync(CancellationToken ct)
{
var headerValue = HttpContext.Request.Headers["X-Agent-Id"].FirstOrDefault();
if (string.IsNullOrWhiteSpace(headerValue))
return null;
var normalized = headerValue.Trim().ToLowerInvariant();
var allowed = await agentService.GetAllowedAgentIdsAsync(ct);
return allowed.Contains(normalized) ? normalized : null;
}
}