- WP-1-3: Central/site failover + dual-node recovery tests (17 tests) - WP-4: Performance testing framework for target scale (7 tests) - WP-5: Security hardening (LDAPS, JWT key length, no secrets in logs) (11 tests) - WP-6: Script sandboxing adversarial tests (28 tests, all forbidden APIs) - WP-7: Recovery drill test scaffolds (5 tests) - WP-8: Observability validation (structured logs, correlation IDs, metrics) (6 tests) - WP-9: Message contract compatibility (forward/backward compat) (18 tests) - WP-10: Deployment packaging (installation guide, production checklist, topology) - WP-11: Operational runbooks (failover, troubleshooting, maintenance) 92 new tests, all passing. Zero warnings.
108 lines
4.0 KiB
C#
108 lines
4.0 KiB
C#
using System.Text.Json;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace ScadaLink.InboundAPI;
|
|
|
|
/// <summary>
|
|
/// WP-1: POST /api/{methodName} endpoint registration.
|
|
/// WP-2: Method routing and parameter validation.
|
|
/// WP-3: Script execution on central.
|
|
/// WP-5: Error handling — 401, 403, 400, 500.
|
|
/// </summary>
|
|
public static class EndpointExtensions
|
|
{
|
|
public static IEndpointRouteBuilder MapInboundAPI(this IEndpointRouteBuilder endpoints)
|
|
{
|
|
endpoints.MapPost("/api/{methodName}", HandleInboundApiRequest);
|
|
return endpoints;
|
|
}
|
|
|
|
private static async Task<IResult> HandleInboundApiRequest(
|
|
HttpContext httpContext,
|
|
string methodName)
|
|
{
|
|
var logger = httpContext.RequestServices.GetRequiredService<ILogger<ApiKeyValidator>>();
|
|
var validator = httpContext.RequestServices.GetRequiredService<ApiKeyValidator>();
|
|
var executor = httpContext.RequestServices.GetRequiredService<InboundScriptExecutor>();
|
|
var routeHelper = httpContext.RequestServices.GetRequiredService<RouteHelper>();
|
|
var options = httpContext.RequestServices.GetRequiredService<IOptions<InboundApiOptions>>().Value;
|
|
|
|
// WP-1: Extract and validate API key
|
|
var apiKeyValue = httpContext.Request.Headers["X-API-Key"].FirstOrDefault();
|
|
var validationResult = await validator.ValidateAsync(apiKeyValue, methodName, httpContext.RequestAborted);
|
|
|
|
if (!validationResult.IsValid)
|
|
{
|
|
// WP-5: Failures-only logging
|
|
logger.LogWarning(
|
|
"Inbound API auth failure for method {Method}: {Error} (status {StatusCode})",
|
|
methodName, validationResult.ErrorMessage, validationResult.StatusCode);
|
|
|
|
return Results.Json(
|
|
new { error = validationResult.ErrorMessage },
|
|
statusCode: validationResult.StatusCode);
|
|
}
|
|
|
|
var method = validationResult.Method!;
|
|
|
|
// WP-2: Deserialize and validate parameters
|
|
JsonElement? body = null;
|
|
try
|
|
{
|
|
if (httpContext.Request.ContentLength > 0 || httpContext.Request.ContentType?.Contains("json") == true)
|
|
{
|
|
using var doc = await JsonDocument.ParseAsync(
|
|
httpContext.Request.Body, cancellationToken: httpContext.RequestAborted);
|
|
body = doc.RootElement.Clone();
|
|
}
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return Results.Json(
|
|
new { error = "Invalid JSON in request body" },
|
|
statusCode: 400);
|
|
}
|
|
|
|
var paramResult = ParameterValidator.Validate(body, method.ParameterDefinitions);
|
|
if (!paramResult.IsValid)
|
|
{
|
|
return Results.Json(
|
|
new { error = paramResult.ErrorMessage },
|
|
statusCode: 400);
|
|
}
|
|
|
|
// WP-3: Execute the method's script
|
|
var timeout = method.TimeoutSeconds > 0
|
|
? TimeSpan.FromSeconds(method.TimeoutSeconds)
|
|
: options.DefaultMethodTimeout;
|
|
|
|
var scriptResult = await executor.ExecuteAsync(
|
|
method, paramResult.Parameters, routeHelper, timeout, httpContext.RequestAborted);
|
|
|
|
if (!scriptResult.Success)
|
|
{
|
|
// WP-5: 500 for script failures, safe error message
|
|
logger.LogWarning(
|
|
"Inbound API script failure for method {Method}: {Error}",
|
|
methodName, scriptResult.ErrorMessage);
|
|
|
|
return Results.Json(
|
|
new { error = scriptResult.ErrorMessage ?? "Internal server error" },
|
|
statusCode: 500);
|
|
}
|
|
|
|
// Return the script result as JSON
|
|
if (scriptResult.ResultJson != null)
|
|
{
|
|
return Results.Text(scriptResult.ResultJson, "application/json", statusCode: 200);
|
|
}
|
|
|
|
return Results.Ok();
|
|
}
|
|
}
|