feat: Initialize Zastava Webhook service with TLS and Authority authentication
- Added Program.cs to set up the web application with Serilog for logging, health check endpoints, and a placeholder admission endpoint. - Configured Kestrel server to use TLS 1.3 and handle client certificates appropriately. - Created StellaOps.Zastava.Webhook.csproj with necessary dependencies including Serilog and Polly. - Documented tasks in TASKS.md for the Zastava Webhook project, outlining current work and exit criteria for each task.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Signer.Core;
|
||||
|
||||
namespace StellaOps.Signer.Infrastructure.Quotas;
|
||||
|
||||
public sealed class InMemoryQuotaService : ISignerQuotaService
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, QuotaWindow> _windows = new(StringComparer.Ordinal);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<InMemoryQuotaService> _logger;
|
||||
|
||||
public InMemoryQuotaService(TimeProvider timeProvider, ILogger<InMemoryQuotaService> logger)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public ValueTask EnsureWithinLimitsAsync(
|
||||
SigningRequest request,
|
||||
ProofOfEntitlementResult entitlement,
|
||||
CallerContext caller,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ArgumentNullException.ThrowIfNull(entitlement);
|
||||
ArgumentNullException.ThrowIfNull(caller);
|
||||
|
||||
var payloadSize = EstimatePayloadSize(request);
|
||||
if (payloadSize > entitlement.MaxArtifactBytes)
|
||||
{
|
||||
throw new SignerQuotaException("artifact_too_large", $"Artifact size {payloadSize} exceeds plan cap ({entitlement.MaxArtifactBytes}).");
|
||||
}
|
||||
|
||||
if (entitlement.QpsLimit <= 0)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
var window = _windows.GetOrAdd(caller.Tenant, static _ => new QuotaWindow());
|
||||
lock (window)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
if (window.ResetAt <= now)
|
||||
{
|
||||
window.Reset(now, entitlement.QpsLimit);
|
||||
}
|
||||
|
||||
if (window.Remaining <= 0)
|
||||
{
|
||||
_logger.LogWarning("Quota exceeded for tenant {Tenant}", caller.Tenant);
|
||||
throw new SignerQuotaException("plan_throttled", "Plan QPS limit exceeded.");
|
||||
}
|
||||
|
||||
window.Remaining--;
|
||||
window.LastUpdated = now;
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private static int EstimatePayloadSize(SigningRequest request)
|
||||
{
|
||||
var predicateBytes = request.Predicate is null
|
||||
? Array.Empty<byte>()
|
||||
: Encoding.UTF8.GetBytes(request.Predicate.RootElement.GetRawText());
|
||||
|
||||
var subjectBytes = 0;
|
||||
foreach (var subject in request.Subjects)
|
||||
{
|
||||
subjectBytes += subject.Name.Length;
|
||||
foreach (var digest in subject.Digest)
|
||||
{
|
||||
subjectBytes += digest.Key.Length + digest.Value.Length;
|
||||
}
|
||||
}
|
||||
|
||||
return predicateBytes.Length + subjectBytes;
|
||||
}
|
||||
|
||||
private sealed class QuotaWindow
|
||||
{
|
||||
public DateTimeOffset ResetAt { get; private set; } = DateTimeOffset.MinValue;
|
||||
|
||||
public int Remaining { get; set; }
|
||||
|
||||
public DateTimeOffset LastUpdated { get; set; }
|
||||
|
||||
public void Reset(DateTimeOffset now, int limit)
|
||||
{
|
||||
ResetAt = now.AddSeconds(1);
|
||||
Remaining = limit;
|
||||
LastUpdated = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user