mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-26 05:29:43 +00:00
3d920a8113
Every HTTP request body the web, the MCP or the agent sends is now a flat class in
AutopilotMonitor.Shared implementing IApiRequest — the request twin of IApiResponse.
WireTypeManifestBuilder exports the implementers as a third root, so the request DTOs
reach wire-types.generated.ts on both clients; for request types a nullable property
keeps `| null` (a PATCH may send an explicit null to clear an override).
Functions read a body only through RequestBody.ReadAsync<T>/ReadOptionalAsync<T>
(System.Text.Json, ApiJsonOptions.Read = wire options + case-insensitive; empty or
malformed body = 400 envelope, never a 500) whose generic constrains on IApiRequest.
ReadNewtonsoftAsync<T> stays for the Newtonsoft-bound cluster (tenant/admin config via
the patch service's PopulateObject, rule and pattern documents) as a frozen per-file
baseline. The tenant-plan PATCH keeps its presence-sensitive JsonDocument walk with the
keys derived from PatchTenantPlanRequest by nameof (PlanPatchKeyParityTests).
35 nested request classes, 7 JObject/JsonDocument hand-walks and the anonymous
DeserializeAnonymousType bodies moved to Shared; the two identical TicketRequests are
one DownloadTicketRequest; ServerAction is no longer the request (QueueSessionActionRequest,
the stored row is built server-side); create-from-template takes { "variables": … }
instead of a bare map. Fields the endpoints default server-side are nullable now
(CPE mapping, report attachments, feedback rating on dismissal, upn for applications).
TypedRequestGuardTests: no raw deserializer under Functions/ (named exemptions: agent
routes frozen by D-200, Azure Monitor webhook, upstream/stored-column parses), the
Newtonsoft branch only in its baseline, no request type in the Functions assembly, every
DTO flat and in Shared. RequestBodyTests cover both readers, the cap and the envelope.
QueryParamGuardTests baseline shrinks by 14 (the Content-Length TryParse lives in the reader).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
170 lines
7.1 KiB
C#
170 lines
7.1 KiB
C#
using System;
|
|
using System.Net;
|
|
using System.Threading.Tasks;
|
|
using AutopilotMonitor.Functions.Helpers;
|
|
using AutopilotMonitor.Functions.Services;
|
|
using AutopilotMonitor.Shared.DataAccess;
|
|
using AutopilotMonitor.Shared.Models;
|
|
using Microsoft.Azure.Functions.Worker;
|
|
using Microsoft.Azure.Functions.Worker.Http;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace AutopilotMonitor.Functions.Functions.Admin
|
|
{
|
|
public class VersionBlockFunction
|
|
{
|
|
private readonly ILogger<VersionBlockFunction> _logger;
|
|
private readonly BlockedVersionService _blockedVersionService;
|
|
private readonly IMaintenanceRepository _maintenanceRepo;
|
|
private readonly OpsEventService _opsEventService;
|
|
|
|
public VersionBlockFunction(
|
|
ILogger<VersionBlockFunction> logger,
|
|
BlockedVersionService blockedVersionService,
|
|
IMaintenanceRepository maintenanceRepo,
|
|
OpsEventService opsEventService)
|
|
{
|
|
_logger = logger;
|
|
_blockedVersionService = blockedVersionService;
|
|
_maintenanceRepo = maintenanceRepo;
|
|
_opsEventService = opsEventService;
|
|
}
|
|
|
|
/// <summary>GET /api/versions/blocked — list all active version block rules</summary>
|
|
[Function("GetBlockedVersions")]
|
|
public async Task<HttpResponseData> GetBlockedVersions(
|
|
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "versions/blocked")] HttpRequestData req)
|
|
{
|
|
try
|
|
{
|
|
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
|
|
|
|
var rules = await _blockedVersionService.GetBlockedVersionsAsync();
|
|
|
|
var response = req.CreateResponse(HttpStatusCode.OK);
|
|
await response.WriteAsJsonAsync(new BlockedVersionListResponse { Success = true, Rules = rules });
|
|
return response;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return await req.InternalServerErrorAsync(_logger, ex, "VersionBlock");
|
|
}
|
|
}
|
|
|
|
/// <summary>POST /api/versions/block — add a version block rule</summary>
|
|
[Function("BlockVersion")]
|
|
public async Task<HttpResponseData> BlockVersion(
|
|
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "versions/block")] HttpRequestData req)
|
|
{
|
|
try
|
|
{
|
|
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
|
|
var userIdentifier = TenantHelper.GetUserIdentifier(req);
|
|
|
|
var read = await req.ReadAsync<BlockVersionRequest>();
|
|
if (read.Error != null) return read.Error;
|
|
var body = read.Value!;
|
|
|
|
var versionPattern = body.VersionPattern;
|
|
var action = body.Action ?? "Block";
|
|
var reason = body.Reason;
|
|
|
|
if (string.IsNullOrEmpty(versionPattern))
|
|
return await req.BadRequestAsync("versionPattern is required");
|
|
|
|
if (!string.Equals(action, "Block", StringComparison.OrdinalIgnoreCase) &&
|
|
!string.Equals(action, "Kill", StringComparison.OrdinalIgnoreCase))
|
|
return await req.BadRequestAsync("action must be 'Block' or 'Kill'");
|
|
|
|
try
|
|
{
|
|
await _blockedVersionService.BlockVersionAsync(versionPattern, action, userIdentifier, reason);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
return await req.BadRequestAsync(ex.Message);
|
|
}
|
|
|
|
var normalizedAction = string.Equals(action, "Kill", StringComparison.OrdinalIgnoreCase) ? "Kill" : "Block";
|
|
|
|
await _maintenanceRepo.LogAuditEntryAsync(
|
|
AutopilotMonitor.Shared.Constants.AuditGlobalTenantId,
|
|
"CREATE",
|
|
"VersionBlock",
|
|
versionPattern,
|
|
userIdentifier,
|
|
new Dictionary<string, string>
|
|
{
|
|
{ "Action", normalizedAction },
|
|
{ "Reason", reason ?? string.Empty }
|
|
}
|
|
);
|
|
|
|
await _opsEventService.RecordVersionBlockedAsync(versionPattern, userIdentifier);
|
|
|
|
_logger.LogWarning(
|
|
"Global Admin {User} added version {Action} rule: Pattern={Pattern}, Reason={Reason}",
|
|
userIdentifier, normalizedAction, versionPattern, reason);
|
|
|
|
var response = req.CreateResponse(HttpStatusCode.OK);
|
|
await response.WriteAsJsonAsync(new BlockVersionResponse
|
|
{
|
|
Success = true,
|
|
Message = $"Version pattern '{versionPattern}' set to {normalizedAction}.",
|
|
VersionPattern = versionPattern,
|
|
Action = normalizedAction
|
|
});
|
|
return response;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return await req.InternalServerErrorAsync(_logger, ex, "VersionBlock");
|
|
}
|
|
}
|
|
|
|
/// <summary>DELETE /api/versions/block/{encodedPattern} — remove a version block rule</summary>
|
|
[Function("UnblockVersion")]
|
|
public async Task<HttpResponseData> UnblockVersion(
|
|
[HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "versions/block/{encodedPattern}")] HttpRequestData req,
|
|
string encodedPattern)
|
|
{
|
|
try
|
|
{
|
|
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
|
|
var userIdentifier = TenantHelper.GetUserIdentifier(req);
|
|
|
|
var versionPattern = Uri.UnescapeDataString(encodedPattern ?? string.Empty);
|
|
if (string.IsNullOrEmpty(versionPattern))
|
|
return await req.BadRequestAsync("versionPattern is required");
|
|
|
|
await _blockedVersionService.UnblockVersionAsync(versionPattern);
|
|
|
|
await _maintenanceRepo.LogAuditEntryAsync(
|
|
AutopilotMonitor.Shared.Constants.AuditGlobalTenantId,
|
|
"DELETE",
|
|
"VersionBlock",
|
|
versionPattern,
|
|
userIdentifier
|
|
);
|
|
|
|
_logger.LogInformation(
|
|
"Global Admin {User} removed version block rule: Pattern={Pattern}",
|
|
userIdentifier, versionPattern);
|
|
|
|
var response = req.CreateResponse(HttpStatusCode.OK);
|
|
await response.WriteAsJsonAsync(new SuccessMessageResponse { Success = true, Message = $"Version pattern '{versionPattern}' unblocked." });
|
|
return response;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return await req.InternalServerErrorAsync(_logger, ex, "VersionBlock");
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Helpers
|
|
// -----------------------------------------------------------------------
|
|
|
|
}
|
|
}
|