Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- 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.
174 lines
6.2 KiB
C#
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);
|
|
}
|
|
}
|