129 lines
4.8 KiB
C#
129 lines
4.8 KiB
C#
// -----------------------------------------------------------------------------
|
|
// RateLimitingExtensions.cs
|
|
// Sprint: SPRINT_3500_0002_0003_proof_replay_api
|
|
// Task: T4 - Rate Limiting
|
|
// Description: Rate limiting configuration for proof replay endpoints
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.RateLimiting;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using StellaOps.Scanner.WebService.Security;
|
|
using System.Threading.RateLimiting;
|
|
|
|
namespace StellaOps.Scanner.WebService.Extensions;
|
|
|
|
/// <summary>
|
|
/// Extensions for configuring rate limiting on proof replay endpoints.
|
|
/// </summary>
|
|
public static class RateLimitingExtensions
|
|
{
|
|
/// <summary>
|
|
/// Policy name for proof replay rate limiting (100 req/hr per tenant).
|
|
/// </summary>
|
|
public const string ProofReplayPolicy = "ProofReplay";
|
|
|
|
/// <summary>
|
|
/// Policy name for scan manifest rate limiting (100 req/hr per tenant).
|
|
/// </summary>
|
|
public const string ManifestPolicy = "Manifest";
|
|
|
|
/// <summary>
|
|
/// Add rate limiting services for scanner endpoints (proof replay, manifest, etc.).
|
|
/// </summary>
|
|
public static IServiceCollection AddScannerRateLimiting(this IServiceCollection services)
|
|
{
|
|
services.AddRateLimiter(options =>
|
|
{
|
|
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
|
|
|
// Proof replay: 100 requests per hour per tenant
|
|
options.AddPolicy(ProofReplayPolicy, context =>
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
return RateLimitPartition.GetFixedWindowLimiter(
|
|
partitionKey: $"proof-replay:{tenantId}",
|
|
factory: _ => new FixedWindowRateLimiterOptions
|
|
{
|
|
PermitLimit = 100,
|
|
Window = TimeSpan.FromHours(1),
|
|
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
|
|
QueueLimit = 0 // No queuing; immediate rejection
|
|
});
|
|
});
|
|
|
|
// Manifest: 100 requests per hour per tenant
|
|
options.AddPolicy(ManifestPolicy, context =>
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
return RateLimitPartition.GetFixedWindowLimiter(
|
|
partitionKey: $"manifest:{tenantId}",
|
|
factory: _ => new FixedWindowRateLimiterOptions
|
|
{
|
|
PermitLimit = 100,
|
|
Window = TimeSpan.FromHours(1),
|
|
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
|
|
QueueLimit = 0
|
|
});
|
|
});
|
|
|
|
// Configure rejection response
|
|
options.OnRejected = async (context, cancellationToken) =>
|
|
{
|
|
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
|
|
context.HttpContext.Response.Headers.RetryAfter = "3600"; // 1 hour
|
|
|
|
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
|
|
{
|
|
context.HttpContext.Response.Headers.RetryAfter =
|
|
((int)retryAfter.TotalSeconds).ToString();
|
|
}
|
|
|
|
await context.HttpContext.Response.WriteAsJsonAsync(new
|
|
{
|
|
type = "https://stellaops.org/problems/rate-limit",
|
|
title = "Too Many Requests",
|
|
status = 429,
|
|
detail = "Rate limit exceeded. Please wait before making more requests.",
|
|
retryAfterSeconds = context.HttpContext.Response.Headers.RetryAfter.ToString()
|
|
}, cancellationToken);
|
|
};
|
|
});
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extract tenant ID from the HTTP context for rate limiting partitioning.
|
|
/// </summary>
|
|
private static string GetTenantId(HttpContext context)
|
|
{
|
|
// Try to get tenant from claims
|
|
var tenantClaim = context.User?.FindFirst(ScannerClaims.TenantId);
|
|
if (tenantClaim is not null && !string.IsNullOrWhiteSpace(tenantClaim.Value))
|
|
{
|
|
return tenantClaim.Value;
|
|
}
|
|
|
|
// Fallback to tenant header
|
|
if (context.Request.Headers.TryGetValue("X-Tenant-Id", out var headerValue) &&
|
|
!string.IsNullOrWhiteSpace(headerValue))
|
|
{
|
|
return headerValue.ToString();
|
|
}
|
|
|
|
// Fallback to IP address for unauthenticated requests
|
|
return context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scanner claims constants.
|
|
/// </summary>
|
|
public static class ScannerClaims
|
|
{
|
|
public const string TenantId = "tenant_id";
|
|
}
|