312 lines
10 KiB
C#
312 lines
10 KiB
C#
// <copyright file="DeterminizationConfigEndpoints.cs" company="StellaOps">
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-004)
|
|
// </copyright>
|
|
|
|
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Policy.Determinization;
|
|
using System.Security.Claims;
|
|
|
|
namespace StellaOps.Policy.Engine.Endpoints;
|
|
|
|
/// <summary>
|
|
/// API endpoints for determinization configuration.
|
|
/// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-004)
|
|
/// </summary>
|
|
public static class DeterminizationConfigEndpoints
|
|
{
|
|
/// <summary>
|
|
/// Maps determinization config endpoints.
|
|
/// </summary>
|
|
public static IEndpointRouteBuilder MapDeterminizationConfigEndpoints(this IEndpointRouteBuilder endpoints)
|
|
{
|
|
var group = endpoints.MapGroup("/api/v1/policy/config/determinization")
|
|
.WithTags("Determinization Configuration");
|
|
|
|
// Read endpoints (policy viewer access)
|
|
group.MapGet("", GetEffectiveConfig)
|
|
.WithName("GetEffectiveDeterminizationConfig")
|
|
.WithSummary("Get effective determinization configuration for the current tenant")
|
|
.Produces<EffectiveConfigResponse>(StatusCodes.Status200OK)
|
|
.RequireAuthorization("PolicyViewer");
|
|
|
|
group.MapGet("/defaults", GetDefaultConfig)
|
|
.WithName("GetDefaultDeterminizationConfig")
|
|
.WithSummary("Get default determinization configuration")
|
|
.Produces<DeterminizationOptions>(StatusCodes.Status200OK)
|
|
.RequireAuthorization("PolicyViewer");
|
|
|
|
group.MapGet("/audit", GetAuditHistory)
|
|
.WithName("GetDeterminizationConfigAuditHistory")
|
|
.WithSummary("Get audit history for determinization configuration changes")
|
|
.Produces<AuditHistoryResponse>(StatusCodes.Status200OK)
|
|
.RequireAuthorization("PolicyViewer");
|
|
|
|
// Write endpoints (policy admin access)
|
|
group.MapPut("", UpdateConfig)
|
|
.WithName("UpdateDeterminizationConfig")
|
|
.WithSummary("Update determinization configuration for the current tenant")
|
|
.Produces<EffectiveConfigResponse>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status400BadRequest)
|
|
.RequireAuthorization("PolicyAdmin");
|
|
|
|
group.MapPost("/validate", ValidateConfig)
|
|
.WithName("ValidateDeterminizationConfig")
|
|
.WithSummary("Validate determinization configuration without saving")
|
|
.Produces<ValidationResponse>(StatusCodes.Status200OK)
|
|
.RequireAuthorization("PolicyViewer");
|
|
|
|
return endpoints;
|
|
}
|
|
|
|
private static async Task<IResult> GetEffectiveConfig(
|
|
HttpContext context,
|
|
IDeterminizationConfigStore configStore,
|
|
ILogger logger,
|
|
CancellationToken ct)
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
|
|
logger.LogDebug("Getting effective determinization config for tenant {TenantId}", tenantId);
|
|
|
|
var config = await configStore.GetEffectiveConfigAsync(tenantId, ct);
|
|
|
|
return Results.Ok(new EffectiveConfigResponse
|
|
{
|
|
Config = config.Config,
|
|
IsDefault = config.IsDefault,
|
|
TenantId = config.TenantId,
|
|
LastUpdatedAt = config.LastUpdatedAt,
|
|
LastUpdatedBy = config.LastUpdatedBy,
|
|
Version = config.Version
|
|
});
|
|
}
|
|
|
|
private static IResult GetDefaultConfig(
|
|
ILogger logger)
|
|
{
|
|
logger.LogDebug("Getting default determinization config");
|
|
return Results.Ok(new DeterminizationOptions());
|
|
}
|
|
|
|
private static async Task<IResult> GetAuditHistory(
|
|
HttpContext context,
|
|
IDeterminizationConfigStore configStore,
|
|
ILogger logger,
|
|
int limit = 50,
|
|
CancellationToken ct = default)
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
|
|
logger.LogDebug("Getting audit history for tenant {TenantId}", tenantId);
|
|
|
|
var entries = await configStore.GetAuditHistoryAsync(tenantId, limit, ct);
|
|
|
|
return Results.Ok(new AuditHistoryResponse
|
|
{
|
|
Entries = entries.Select(e => new AuditEntryDto
|
|
{
|
|
Id = e.Id,
|
|
ChangedAt = e.ChangedAt,
|
|
Actor = e.Actor,
|
|
Reason = e.Reason,
|
|
Source = e.Source,
|
|
Summary = e.Summary
|
|
}).ToList()
|
|
});
|
|
}
|
|
|
|
private static async Task<IResult> UpdateConfig(
|
|
HttpContext context,
|
|
IDeterminizationConfigStore configStore,
|
|
ILogger logger,
|
|
UpdateConfigRequest request,
|
|
CancellationToken ct)
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
var actor = GetActorId(context);
|
|
|
|
logger.LogInformation(
|
|
"Updating determinization config for tenant {TenantId} by {Actor}: {Reason}",
|
|
tenantId,
|
|
actor,
|
|
request.Reason);
|
|
|
|
// Validate config
|
|
var validation = ValidateConfigInternal(request.Config);
|
|
if (!validation.IsValid)
|
|
{
|
|
return Results.BadRequest(new { errors = validation.Errors });
|
|
}
|
|
|
|
// Save with audit
|
|
await configStore.SaveConfigAsync(
|
|
tenantId,
|
|
request.Config,
|
|
new ConfigAuditInfo
|
|
{
|
|
Actor = actor,
|
|
Reason = request.Reason,
|
|
Source = "API",
|
|
CorrelationId = context.TraceIdentifier
|
|
},
|
|
ct);
|
|
|
|
// Return updated config
|
|
var updated = await configStore.GetEffectiveConfigAsync(tenantId, ct);
|
|
|
|
return Results.Ok(new EffectiveConfigResponse
|
|
{
|
|
Config = updated.Config,
|
|
IsDefault = updated.IsDefault,
|
|
TenantId = updated.TenantId,
|
|
LastUpdatedAt = updated.LastUpdatedAt,
|
|
LastUpdatedBy = updated.LastUpdatedBy,
|
|
Version = updated.Version
|
|
});
|
|
}
|
|
|
|
private static IResult ValidateConfig(
|
|
ValidateConfigRequest request,
|
|
ILogger logger)
|
|
{
|
|
logger.LogDebug("Validating determinization config");
|
|
|
|
var validation = ValidateConfigInternal(request.Config);
|
|
|
|
return Results.Ok(new ValidationResponse
|
|
{
|
|
IsValid = validation.IsValid,
|
|
Errors = validation.Errors,
|
|
Warnings = validation.Warnings
|
|
});
|
|
}
|
|
|
|
private static (bool IsValid, List<string> Errors, List<string> Warnings) ValidateConfigInternal(
|
|
DeterminizationOptions config)
|
|
{
|
|
var errors = new List<string>();
|
|
var warnings = new List<string>();
|
|
|
|
// Validate trigger config
|
|
if (config.Triggers.EpssDeltaThreshold < 0 || config.Triggers.EpssDeltaThreshold > 1)
|
|
{
|
|
errors.Add("EpssDeltaThreshold must be between 0 and 1");
|
|
}
|
|
|
|
if (config.Triggers.EpssDeltaThreshold < 0.1)
|
|
{
|
|
warnings.Add("EpssDeltaThreshold below 0.1 may cause excessive reanalysis");
|
|
}
|
|
|
|
// Validate conflict policy
|
|
if (config.ConflictPolicy.EscalationSeverityThreshold < 0 || config.ConflictPolicy.EscalationSeverityThreshold > 1)
|
|
{
|
|
errors.Add("EscalationSeverityThreshold must be between 0 and 1");
|
|
}
|
|
|
|
if (config.ConflictPolicy.ConflictTtlHours < 1)
|
|
{
|
|
errors.Add("ConflictTtlHours must be at least 1");
|
|
}
|
|
|
|
// Validate environment thresholds
|
|
ValidateThresholds(config.EnvironmentThresholds.Development, "Development", errors, warnings);
|
|
ValidateThresholds(config.EnvironmentThresholds.Staging, "Staging", errors, warnings);
|
|
ValidateThresholds(config.EnvironmentThresholds.Production, "Production", errors, warnings);
|
|
|
|
return (errors.Count == 0, errors, warnings);
|
|
}
|
|
|
|
private static void ValidateThresholds(
|
|
EnvironmentThresholdValues threshold,
|
|
string envName,
|
|
List<string> errors,
|
|
List<string> warnings)
|
|
{
|
|
if (threshold.MaxPassEntropy < 0 || threshold.MaxPassEntropy > 1)
|
|
{
|
|
errors.Add($"{envName}.MaxPassEntropy must be between 0 and 1");
|
|
}
|
|
|
|
if (threshold.MinEvidenceCount < 0)
|
|
{
|
|
errors.Add($"{envName}.MinEvidenceCount must be >= 0");
|
|
}
|
|
|
|
if (threshold.MaxPassEntropy > 0.8)
|
|
{
|
|
warnings.Add($"{envName}.MaxPassEntropy above 0.8 may reduce confidence controls");
|
|
}
|
|
}
|
|
|
|
private static string GetTenantId(HttpContext context)
|
|
{
|
|
return context.User.FindFirstValue("tenant_id") ?? "default";
|
|
}
|
|
|
|
private static string GetActorId(HttpContext context)
|
|
{
|
|
return context.User.FindFirstValue(ClaimTypes.NameIdentifier)
|
|
?? context.User.FindFirstValue("sub")
|
|
?? "system";
|
|
}
|
|
}
|
|
|
|
// DTOs
|
|
|
|
/// <summary>Effective config response.</summary>
|
|
public sealed record EffectiveConfigResponse
|
|
{
|
|
public required DeterminizationOptions Config { get; init; }
|
|
public required bool IsDefault { get; init; }
|
|
public string? TenantId { get; init; }
|
|
public DateTimeOffset? LastUpdatedAt { get; init; }
|
|
public string? LastUpdatedBy { get; init; }
|
|
public int Version { get; init; }
|
|
}
|
|
|
|
/// <summary>Update config request.</summary>
|
|
public sealed record UpdateConfigRequest
|
|
{
|
|
public required DeterminizationOptions Config { get; init; }
|
|
public required string Reason { get; init; }
|
|
}
|
|
|
|
/// <summary>Validate config request.</summary>
|
|
public sealed record ValidateConfigRequest
|
|
{
|
|
public required DeterminizationOptions Config { get; init; }
|
|
}
|
|
|
|
/// <summary>Validation response.</summary>
|
|
public sealed record ValidationResponse
|
|
{
|
|
public required bool IsValid { get; init; }
|
|
public required List<string> Errors { get; init; }
|
|
public required List<string> Warnings { get; init; }
|
|
}
|
|
|
|
/// <summary>Audit history response.</summary>
|
|
public sealed record AuditHistoryResponse
|
|
{
|
|
public required List<AuditEntryDto> Entries { get; init; }
|
|
}
|
|
|
|
/// <summary>Audit entry DTO.</summary>
|
|
public sealed record AuditEntryDto
|
|
{
|
|
public required Guid Id { get; init; }
|
|
public required DateTimeOffset ChangedAt { get; init; }
|
|
public required string Actor { get; init; }
|
|
public required string Reason { get; init; }
|
|
public string? Source { get; init; }
|
|
public string? Summary { get; init; }
|
|
}
|
|
|
|
|