Files
git.stella-ops.org/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/EventWebhookEndpointExtensions.cs
master b1e78fe412
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
feat: Implement vulnerability token signing and verification utilities
- Added VulnTokenSigner for signing JWT tokens with specified algorithms and keys.
- Introduced VulnTokenUtilities for resolving tenant and subject claims, and sanitizing context dictionaries.
- Created VulnTokenVerificationUtilities for parsing tokens, verifying signatures, and deserializing payloads.
- Developed VulnWorkflowAntiForgeryTokenIssuer for issuing anti-forgery tokens with configurable options.
- Implemented VulnWorkflowAntiForgeryTokenVerifier for verifying anti-forgery tokens and validating payloads.
- Added AuthorityVulnerabilityExplorerOptions to manage configuration for vulnerability explorer features.
- Included tests for FilesystemPackRunDispatcher to ensure proper job handling under egress policy restrictions.
2025-11-03 10:04:10 +02:00

174 lines
6.2 KiB
C#

using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scheduler.WebService.Options;
namespace StellaOps.Scheduler.WebService.EventWebhooks;
public static class EventWebhookEndpointExtensions
{
public static void MapSchedulerEventWebhookEndpoints(this IEndpointRouteBuilder builder)
{
var group = builder.MapGroup("/events");
group.MapPost("/conselier-export", HandleConselierExportAsync);
group.MapPost("/excitor-export", HandleExcitorExportAsync);
}
private static async Task<IResult> HandleConselierExportAsync(
HttpContext httpContext,
[FromServices] IOptionsMonitor<SchedulerEventsOptions> options,
[FromServices] IWebhookRequestAuthenticator authenticator,
[FromServices] IWebhookRateLimiter rateLimiter,
[FromServices] IInboundExportEventSink sink,
CancellationToken cancellationToken)
{
var webhookOptions = options.CurrentValue.Webhooks.Conselier;
if (!webhookOptions.Enabled)
{
return Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
var readResult = await ReadPayloadAsync<ConselierExportEventRequest>(httpContext, cancellationToken).ConfigureAwait(false);
if (!readResult.Succeeded)
{
return readResult.ErrorResult!;
}
if (!rateLimiter.TryAcquire("conselier", webhookOptions.RateLimitRequests, webhookOptions.GetRateLimitWindow(), out var retryAfter))
{
var response = Results.StatusCode(StatusCodes.Status429TooManyRequests);
if (retryAfter > TimeSpan.Zero)
{
httpContext.Response.Headers.RetryAfter = ((int)Math.Ceiling(retryAfter.TotalSeconds)).ToString();
}
return response;
}
var authResult = await authenticator.AuthenticateAsync(httpContext, readResult.RawBody, webhookOptions, cancellationToken).ConfigureAwait(false);
if (!authResult.Succeeded)
{
return authResult.ToResult();
}
try
{
await sink.HandleConselierAsync(readResult.Payload!, cancellationToken).ConfigureAwait(false);
return Results.Accepted(value: new { status = "accepted" });
}
catch (ValidationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
private static async Task<IResult> HandleExcitorExportAsync(
HttpContext httpContext,
[FromServices] IOptionsMonitor<SchedulerEventsOptions> options,
[FromServices] IWebhookRequestAuthenticator authenticator,
[FromServices] IWebhookRateLimiter rateLimiter,
[FromServices] IInboundExportEventSink sink,
CancellationToken cancellationToken)
{
var webhookOptions = options.CurrentValue.Webhooks.Excitor;
if (!webhookOptions.Enabled)
{
return Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
var readResult = await ReadPayloadAsync<ExcitorExportEventRequest>(httpContext, cancellationToken).ConfigureAwait(false);
if (!readResult.Succeeded)
{
return readResult.ErrorResult!;
}
if (!rateLimiter.TryAcquire("excitor", webhookOptions.RateLimitRequests, webhookOptions.GetRateLimitWindow(), out var retryAfter))
{
var response = Results.StatusCode(StatusCodes.Status429TooManyRequests);
if (retryAfter > TimeSpan.Zero)
{
httpContext.Response.Headers.RetryAfter = ((int)Math.Ceiling(retryAfter.TotalSeconds)).ToString();
}
return response;
}
var authResult = await authenticator.AuthenticateAsync(httpContext, readResult.RawBody, webhookOptions, cancellationToken).ConfigureAwait(false);
if (!authResult.Succeeded)
{
return authResult.ToResult();
}
try
{
await sink.HandleExcitorAsync(readResult.Payload!, cancellationToken).ConfigureAwait(false);
return Results.Accepted(value: new { status = "accepted" });
}
catch (ValidationException ex)
{
return Results.BadRequest(new { error = ex.Message });
}
}
private static async Task<RequestPayload<T>> ReadPayloadAsync<T>(HttpContext context, CancellationToken cancellationToken)
{
context.Request.EnableBuffering();
await using var buffer = new MemoryStream();
await context.Request.Body.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
var bodyBytes = buffer.ToArray();
context.Request.Body.Position = 0;
try
{
var payload = JsonSerializer.Deserialize<T>(bodyBytes, new JsonSerializerOptions(JsonSerializerDefaults.Web));
if (payload is null)
{
return RequestPayload<T>.Failed(Results.BadRequest(new { error = "Request payload cannot be empty." }));
}
return RequestPayload<T>.Success(payload, bodyBytes);
}
catch (JsonException ex)
{
return RequestPayload<T>.Failed(Results.BadRequest(new { error = ex.Message }));
}
catch (ValidationException ex)
{
return RequestPayload<T>.Failed(Results.BadRequest(new { error = ex.Message }));
}
}
private readonly struct RequestPayload<T>
{
private RequestPayload(T? payload, byte[] rawBody, IResult? error, bool succeeded)
{
Payload = payload;
RawBody = rawBody;
ErrorResult = error;
Succeeded = succeeded;
}
public T? Payload { get; }
public byte[] RawBody { get; }
public IResult? ErrorResult { get; }
public bool Succeeded { get; }
public static RequestPayload<T> Success(T payload, byte[] rawBody)
=> new(payload, rawBody, null, true);
public static RequestPayload<T> Failed(IResult error)
=> new(default, Array.Empty<byte>(), error, false);
}
}