fix(security): enforce audited account, network and resource boundaries
Implement B01-B06 with regression coverage and retained validation evidence. Preserve the production ingress preflight; F04 runtime verification remains open.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace JobTrackerApi.Services;
|
||||
|
||||
public sealed class PdfRenderOptions
|
||||
{
|
||||
// One browser at a time until the deployment's memory/CPU budget is measured (R04).
|
||||
[Range(1, 8)] public int MaxConcurrentRenders { get; set; } = 1;
|
||||
[Range(1, 60)] public int RenderTimeoutSeconds { get; set; } = 60;
|
||||
}
|
||||
|
||||
public sealed class PdfCapacityException : Exception
|
||||
{
|
||||
public PdfCapacityException() : base("PDF generation is busy. Please try again shortly.") { }
|
||||
}
|
||||
|
||||
public sealed class PdfRenderTimeoutException : TimeoutException
|
||||
{
|
||||
public PdfRenderTimeoutException() : base("PDF generation took too long. Please try again.") { }
|
||||
}
|
||||
|
||||
// Registered once for the API process, shared even by separately constructed exporters.
|
||||
// ponytail: zero waiting, one active render per document owner; measure before adding a scheduler.
|
||||
public sealed class PdfRenderAdmission
|
||||
{
|
||||
private readonly HashSet<string> _owners = new(StringComparer.Ordinal);
|
||||
private readonly int _limit;
|
||||
private readonly ILogger<PdfRenderAdmission> _logger;
|
||||
private bool _disabled;
|
||||
public TimeSpan RenderTimeout { get; }
|
||||
|
||||
public PdfRenderAdmission(IOptions<PdfRenderOptions> options, ILogger<PdfRenderAdmission> logger)
|
||||
{
|
||||
var value = options.Value;
|
||||
Validator.ValidateObject(value, new ValidationContext(value), validateAllProperties: true);
|
||||
_limit = value.MaxConcurrentRenders;
|
||||
RenderTimeout = TimeSpan.FromSeconds(value.RenderTimeoutSeconds);
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IDisposable Enter(string ownerUserId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(ownerUserId);
|
||||
lock (_owners)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (_disabled || _owners.Count >= _limit || _owners.Contains(ownerUserId))
|
||||
{
|
||||
_logger.LogDebug("PDF admission rejected; active={Active}, limit={Limit}, disabled={Disabled}", _owners.Count, _limit, _disabled);
|
||||
throw new PdfCapacityException();
|
||||
}
|
||||
// Log before reserving: even a failing logging provider must not strand a slot.
|
||||
_logger.LogDebug("PDF admission granted; active={Active}, limit={Limit}", _owners.Count + 1, _limit);
|
||||
_owners.Add(ownerUserId);
|
||||
return new Lease(this, ownerUserId);
|
||||
}
|
||||
}
|
||||
|
||||
// If the OS cannot confirm termination, never replace possibly still-running work.
|
||||
// This is a visible fail-closed fault, requiring operator inspection/restart.
|
||||
public void Disable()
|
||||
{
|
||||
lock (_owners) _disabled = true;
|
||||
}
|
||||
|
||||
private sealed class Lease(PdfRenderAdmission admission, string owner) : IDisposable
|
||||
{
|
||||
private int _disposed;
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) != 0) return;
|
||||
lock (admission._owners) admission._owners.Remove(owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user