warnings fixes, tests fixes, sprints completions

This commit is contained in:
Codex Assistant
2026-01-08 08:38:27 +02:00
parent 75611a505f
commit 0b5d786ddb
125 changed files with 14610 additions and 368 deletions

View File

@@ -2,10 +2,12 @@
// Sprint: SPRINT_20251226_003_BE_exception_approval
// Task: EXCEPT-05, EXCEPT-06, EXCEPT-07 - Exception approval API endpoints
using System.Globalization;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Determinism.Abstractions;
using StellaOps.Policy.Engine.Services;
using StellaOps.Policy.Persistence.Postgres.Models;
using StellaOps.Policy.Persistence.Postgres.Repositories;
@@ -89,6 +91,8 @@ public static class ExceptionApprovalEndpoints
CreateApprovalRequestDto request,
IExceptionApprovalRepository repository,
IExceptionApprovalRulesService rulesService,
TimeProvider timeProvider,
IGuidProvider guidProvider,
ILogger<ExceptionApprovalRequestEntity> logger,
CancellationToken cancellationToken)
{
@@ -110,7 +114,8 @@ public static class ExceptionApprovalEndpoints
}
// Generate request ID
var requestId = $"EAR-{DateTimeOffset.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
var now = timeProvider.GetUtcNow();
var requestId = $"EAR-{now.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{guidProvider.NewGuid().ToString("N", CultureInfo.InvariantCulture)[..8].ToUpperInvariant()}";
// Parse gate level
if (!Enum.TryParse<GateLevel>(request.GateLevel, ignoreCase: true, out var gateLevel))
@@ -139,10 +144,9 @@ public static class ExceptionApprovalEndpoints
});
}
var now = DateTimeOffset.UtcNow;
var entity = new ExceptionApprovalRequestEntity
{
Id = Guid.NewGuid(),
Id = guidProvider.NewGuid(),
RequestId = requestId,
TenantId = tenantId,
ExceptionId = request.ExceptionId,
@@ -204,7 +208,7 @@ public static class ExceptionApprovalEndpoints
// Record audit entry
await repository.RecordAuditAsync(new ExceptionApprovalAuditEntity
{
Id = Guid.NewGuid(),
Id = guidProvider.NewGuid(),
RequestId = requestId,
TenantId = tenantId,
SequenceNumber = 1,

View File

@@ -8,6 +8,7 @@ using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Determinism.Abstractions;
using StellaOps.Policy.Exceptions.Models;
using StellaOps.Policy.Exceptions.Repositories;
using StellaOps.Policy.Gateway.Contracts;
@@ -134,6 +135,8 @@ public static class ExceptionEndpoints
CreateExceptionRequest request,
HttpContext context,
IExceptionRepository repository,
TimeProvider timeProvider,
IGuidProvider guidProvider,
CancellationToken cancellationToken) =>
{
if (request is null)
@@ -145,8 +148,10 @@ public static class ExceptionEndpoints
});
}
var now = timeProvider.GetUtcNow();
// Validate expiry is in future
if (request.ExpiresAt <= DateTimeOffset.UtcNow)
if (request.ExpiresAt <= now)
{
return Results.BadRequest(new ProblemDetails
{
@@ -157,7 +162,7 @@ public static class ExceptionEndpoints
}
// Validate expiry is not more than 1 year
if (request.ExpiresAt > DateTimeOffset.UtcNow.AddYears(1))
if (request.ExpiresAt > now.AddYears(1))
{
return Results.BadRequest(new ProblemDetails
{
@@ -170,7 +175,7 @@ public static class ExceptionEndpoints
var actorId = GetActorId(context);
var clientInfo = GetClientInfo(context);
var exceptionId = $"EXC-{Guid.NewGuid():N}"[..20];
var exceptionId = $"EXC-{guidProvider.NewGuid():N}"[..20];
var exception = new ExceptionObject
{
@@ -188,8 +193,8 @@ public static class ExceptionEndpoints
},
OwnerId = request.OwnerId,
RequesterId = actorId,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow,
CreatedAt = now,
UpdatedAt = now,
ExpiresAt = request.ExpiresAt,
ReasonCode = ParseReasonRequired(request.ReasonCode),
Rationale = request.Rationale,
@@ -210,6 +215,7 @@ public static class ExceptionEndpoints
UpdateExceptionRequest request,
HttpContext context,
IExceptionRepository repository,
TimeProvider timeProvider,
CancellationToken cancellationToken) =>
{
var existing = await repository.GetByIdAsync(id, cancellationToken);
@@ -238,7 +244,7 @@ public static class ExceptionEndpoints
var updated = existing with
{
Version = existing.Version + 1,
UpdatedAt = DateTimeOffset.UtcNow,
UpdatedAt = timeProvider.GetUtcNow(),
Rationale = request.Rationale ?? existing.Rationale,
EvidenceRefs = request.EvidenceRefs?.ToImmutableArray() ?? existing.EvidenceRefs,
CompensatingControls = request.CompensatingControls?.ToImmutableArray() ?? existing.CompensatingControls,
@@ -258,6 +264,7 @@ public static class ExceptionEndpoints
ApproveExceptionRequest? request,
HttpContext context,
IExceptionRepository repository,
TimeProvider timeProvider,
CancellationToken cancellationToken) =>
{
var existing = await repository.GetByIdAsync(id, cancellationToken);
@@ -290,12 +297,13 @@ public static class ExceptionEndpoints
});
}
var now = timeProvider.GetUtcNow();
var updated = existing with
{
Version = existing.Version + 1,
Status = ExceptionStatus.Approved,
UpdatedAt = DateTimeOffset.UtcNow,
ApprovedAt = DateTimeOffset.UtcNow,
UpdatedAt = now,
ApprovedAt = now,
ApproverIds = existing.ApproverIds.Add(actorId)
};
@@ -310,6 +318,7 @@ public static class ExceptionEndpoints
string id,
HttpContext context,
IExceptionRepository repository,
TimeProvider timeProvider,
CancellationToken cancellationToken) =>
{
var existing = await repository.GetByIdAsync(id, cancellationToken);
@@ -335,7 +344,7 @@ public static class ExceptionEndpoints
{
Version = existing.Version + 1,
Status = ExceptionStatus.Active,
UpdatedAt = DateTimeOffset.UtcNow
UpdatedAt = timeProvider.GetUtcNow()
};
var result = await repository.UpdateAsync(
@@ -350,6 +359,7 @@ public static class ExceptionEndpoints
ExtendExceptionRequest request,
HttpContext context,
IExceptionRepository repository,
TimeProvider timeProvider,
CancellationToken cancellationToken) =>
{
var existing = await repository.GetByIdAsync(id, cancellationToken);
@@ -384,7 +394,7 @@ public static class ExceptionEndpoints
var updated = existing with
{
Version = existing.Version + 1,
UpdatedAt = DateTimeOffset.UtcNow,
UpdatedAt = timeProvider.GetUtcNow(),
ExpiresAt = request.NewExpiresAt
};
@@ -400,6 +410,7 @@ public static class ExceptionEndpoints
[FromBody] RevokeExceptionRequest? request,
HttpContext context,
IExceptionRepository repository,
TimeProvider timeProvider,
CancellationToken cancellationToken) =>
{
var existing = await repository.GetByIdAsync(id, cancellationToken);
@@ -425,7 +436,7 @@ public static class ExceptionEndpoints
{
Version = existing.Version + 1,
Status = ExceptionStatus.Revoked,
UpdatedAt = DateTimeOffset.UtcNow
UpdatedAt = timeProvider.GetUtcNow()
};
var result = await repository.UpdateAsync(

View File

@@ -2,10 +2,12 @@
// Sprint: SPRINT_20251226_001_BE_cicd_gate_integration
// Task: CICD-GATE-01 - Create POST /api/v1/policy/gate/evaluate endpoint
using System.Globalization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Determinism.Abstractions;
using StellaOps.Policy.Audit;
using StellaOps.Policy.Deltas;
using StellaOps.Policy.Engine.Gates;
@@ -39,6 +41,8 @@ public static class GateEndpoints
IBaselineSelector baselineSelector,
IGateBypassAuditor bypassAuditor,
IMemoryCache cache,
TimeProvider timeProvider,
IGuidProvider guidProvider,
ILogger<DriftGateEvaluator> logger,
CancellationToken cancellationToken) =>
{
@@ -79,12 +83,12 @@ public static class GateEndpoints
return Results.Ok(new GateEvaluateResponse
{
DecisionId = $"gate:{DateTimeOffset.UtcNow:yyyyMMddHHmmss}:{Guid.NewGuid():N}",
DecisionId = $"gate:{timeProvider.GetUtcNow().ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture)}:{guidProvider.NewGuid():N}",
Status = GateStatus.Pass,
ExitCode = GateExitCodes.Pass,
ImageDigest = request.ImageDigest,
BaselineRef = request.BaselineRef,
DecidedAt = DateTimeOffset.UtcNow,
DecidedAt = timeProvider.GetUtcNow(),
Summary = "First build - no baseline for comparison",
Advisory = "This appears to be a first build. Future builds will be compared against this baseline."
});
@@ -224,7 +228,7 @@ public static class GateEndpoints
.WithDescription("Retrieve a previous gate evaluation decision by ID");
// GET /api/v1/policy/gate/health - Health check for gate service
gates.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTimeOffset.UtcNow }))
gates.MapGet("/health", (TimeProvider timeProvider) => Results.Ok(new { status = "healthy", timestamp = timeProvider.GetUtcNow() }))
.WithName("GateHealth")
.WithDescription("Health check for the gate evaluation service");
}

View File

@@ -5,6 +5,8 @@
using System.Collections.Concurrent;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Determinism.Abstractions;
namespace StellaOps.Policy.Gateway.Endpoints;
@@ -104,6 +106,7 @@ public static class GovernanceEndpoints
{
var tenant = tenantId ?? GetTenantId(httpContext) ?? "default";
var state = SealedModeStates.GetOrAdd(tenant, _ => new SealedModeState());
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
var response = new SealedModeStatusResponse
{
@@ -118,7 +121,7 @@ public static class GovernanceEndpoints
.Select(MapOverrideToResponse)
.ToList(),
VerificationStatus = "verified",
LastVerifiedAt = DateTimeOffset.UtcNow.ToString("O")
LastVerifiedAt = timeProvider.GetUtcNow().ToString("O")
};
return Task.FromResult(Results.Ok(response));
@@ -144,9 +147,9 @@ public static class GovernanceEndpoints
{
var tenant = GetTenantId(httpContext) ?? "default";
var actor = GetActorId(httpContext) ?? "system";
var now = DateTimeOffset.UtcNow;
var state = SealedModeStates.GetOrAdd(tenant, _ => new SealedModeState());
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
var now = timeProvider.GetUtcNow();
if (request.Enable)
{
@@ -173,7 +176,7 @@ public static class GovernanceEndpoints
// Audit
RecordAudit(tenant, actor, "sealed_mode_toggled", "sealed-mode", "system_config",
$"{(request.Enable ? "Enabled" : "Disabled")} sealed mode: {request.Reason}");
$"{(request.Enable ? "Enabled" : "Disabled")} sealed mode: {request.Reason}", timeProvider, guidProvider);
var response = new SealedModeStatusResponse
{
@@ -197,9 +200,11 @@ public static class GovernanceEndpoints
{
var tenant = GetTenantId(httpContext) ?? "default";
var actor = GetActorId(httpContext) ?? "system";
var now = DateTimeOffset.UtcNow;
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
var now = timeProvider.GetUtcNow();
var overrideId = $"override-{Guid.NewGuid():N}";
var overrideId = $"override-{guidProvider.NewGuid():N}";
var entity = new SealedModeOverrideEntity
{
Id = overrideId,
@@ -207,7 +212,7 @@ public static class GovernanceEndpoints
Type = request.Type,
Target = request.Target,
Reason = request.Reason,
ApprovalId = $"approval-{Guid.NewGuid():N}",
ApprovalId = $"approval-{guidProvider.NewGuid():N}",
ApprovedBy = [actor],
ExpiresAt = now.AddHours(request.DurationHours).ToString("O"),
CreatedAt = now.ToString("O"),
@@ -217,7 +222,7 @@ public static class GovernanceEndpoints
Overrides[overrideId] = entity;
RecordAudit(tenant, actor, "sealed_mode_override_created", overrideId, "sealed_mode_override",
$"Created override for {request.Target}: {request.Reason}");
$"Created override for {request.Target}: {request.Reason}", timeProvider, guidProvider);
return Task.FromResult(Results.Ok(MapOverrideToResponse(entity)));
}
@@ -229,6 +234,8 @@ public static class GovernanceEndpoints
{
var tenant = GetTenantId(httpContext) ?? "default";
var actor = GetActorId(httpContext) ?? "system";
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
if (!Overrides.TryGetValue(overrideId, out var entity) || entity.TenantId != tenant)
{
@@ -243,7 +250,7 @@ public static class GovernanceEndpoints
Overrides[overrideId] = entity;
RecordAudit(tenant, actor, "sealed_mode_override_revoked", overrideId, "sealed_mode_override",
$"Revoked override: {request.Reason}");
$"Revoked override: {request.Reason}", timeProvider, guidProvider);
return Task.FromResult(Results.NoContent());
}
@@ -293,9 +300,11 @@ public static class GovernanceEndpoints
{
var tenant = GetTenantId(httpContext) ?? "default";
var actor = GetActorId(httpContext) ?? "system";
var now = DateTimeOffset.UtcNow;
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
var now = timeProvider.GetUtcNow();
var profileId = $"profile-{Guid.NewGuid():N}";
var profileId = $"profile-{guidProvider.NewGuid():N}";
var entity = new RiskProfileEntity
{
Id = profileId,
@@ -317,7 +326,7 @@ public static class GovernanceEndpoints
RiskProfiles[profileId] = entity;
RecordAudit(tenant, actor, "risk_profile_created", profileId, "risk_profile",
$"Created risk profile: {request.Name}");
$"Created risk profile: {request.Name}", timeProvider, guidProvider);
return Task.FromResult(Results.Created($"/api/v1/governance/risk-profiles/{profileId}", MapProfileToResponse(entity)));
}
@@ -329,7 +338,9 @@ public static class GovernanceEndpoints
{
var tenant = GetTenantId(httpContext) ?? "default";
var actor = GetActorId(httpContext) ?? "system";
var now = DateTimeOffset.UtcNow;
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
var now = timeProvider.GetUtcNow();
if (!RiskProfiles.TryGetValue(profileId, out var existing))
{
@@ -354,7 +365,7 @@ public static class GovernanceEndpoints
RiskProfiles[profileId] = entity;
RecordAudit(tenant, actor, "risk_profile_updated", profileId, "risk_profile",
$"Updated risk profile: {entity.Name}");
$"Updated risk profile: {entity.Name}", timeProvider, guidProvider);
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
}
@@ -365,6 +376,8 @@ public static class GovernanceEndpoints
{
var tenant = GetTenantId(httpContext) ?? "default";
var actor = GetActorId(httpContext) ?? "system";
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
if (!RiskProfiles.TryRemove(profileId, out var removed))
{
@@ -376,7 +389,7 @@ public static class GovernanceEndpoints
}
RecordAudit(tenant, actor, "risk_profile_deleted", profileId, "risk_profile",
$"Deleted risk profile: {removed.Name}");
$"Deleted risk profile: {removed.Name}", timeProvider, guidProvider);
return Task.FromResult(Results.NoContent());
}
@@ -387,7 +400,9 @@ public static class GovernanceEndpoints
{
var tenant = GetTenantId(httpContext) ?? "default";
var actor = GetActorId(httpContext) ?? "system";
var now = DateTimeOffset.UtcNow;
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
var now = timeProvider.GetUtcNow();
if (!RiskProfiles.TryGetValue(profileId, out var existing))
{
@@ -408,7 +423,7 @@ public static class GovernanceEndpoints
RiskProfiles[profileId] = entity;
RecordAudit(tenant, actor, "risk_profile_activated", profileId, "risk_profile",
$"Activated risk profile: {entity.Name}");
$"Activated risk profile: {entity.Name}", timeProvider, guidProvider);
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
}
@@ -420,7 +435,9 @@ public static class GovernanceEndpoints
{
var tenant = GetTenantId(httpContext) ?? "default";
var actor = GetActorId(httpContext) ?? "system";
var now = DateTimeOffset.UtcNow;
var timeProvider = httpContext.RequestServices.GetRequiredService<TimeProvider>();
var guidProvider = httpContext.RequestServices.GetRequiredService<IGuidProvider>();
var now = timeProvider.GetUtcNow();
if (!RiskProfiles.TryGetValue(profileId, out var existing))
{
@@ -442,7 +459,7 @@ public static class GovernanceEndpoints
RiskProfiles[profileId] = entity;
RecordAudit(tenant, actor, "risk_profile_deprecated", profileId, "risk_profile",
$"Deprecated risk profile: {entity.Name} - {request.Reason}");
$"Deprecated risk profile: {entity.Name} - {request.Reason}", timeProvider, guidProvider);
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
}
@@ -542,7 +559,7 @@ public static class GovernanceEndpoints
{
if (RiskProfiles.IsEmpty)
{
var now = DateTimeOffset.UtcNow.ToString("O");
var now = TimeProvider.System.GetUtcNow().ToString("O", System.Globalization.CultureInfo.InvariantCulture);
RiskProfiles["profile-default"] = new RiskProfileEntity
{
Id = "profile-default",
@@ -582,15 +599,15 @@ public static class GovernanceEndpoints
?? httpContext.Request.Headers["X-StellaOps-Actor"].FirstOrDefault();
}
private static void RecordAudit(string tenantId, string actor, string eventType, string targetId, string targetType, string summary)
private static void RecordAudit(string tenantId, string actor, string eventType, string targetId, string targetType, string summary, TimeProvider timeProvider, IGuidProvider guidProvider)
{
var id = $"audit-{Guid.NewGuid():N}";
var id = $"audit-{guidProvider.NewGuid():N}";
AuditEntries[id] = new GovernanceAuditEntry
{
Id = id,
TenantId = tenantId,
Type = eventType,
Timestamp = DateTimeOffset.UtcNow.ToString("O"),
Timestamp = timeProvider.GetUtcNow().ToString("O", System.Globalization.CultureInfo.InvariantCulture),
Actor = actor,
ActorType = "user",
TargetResource = targetId,

View File

@@ -50,6 +50,7 @@ internal static class RegistryWebhookEndpoints
private static async Task<Results<Accepted<WebhookAcceptedResponse>, ProblemHttpResult>> HandleDockerRegistryWebhook(
[FromBody] DockerRegistryNotification notification,
IGateEvaluationQueue evaluationQueue,
TimeProvider timeProvider,
ILogger<RegistryWebhookEndpointMarker> logger,
CancellationToken ct)
{
@@ -77,7 +78,7 @@ internal static class RegistryWebhookEndpoints
Tag = evt.Target.Tag,
RegistryUrl = evt.Request?.Host,
Source = "docker-registry",
Timestamp = evt.Timestamp ?? DateTimeOffset.UtcNow
Timestamp = evt.Timestamp ?? timeProvider.GetUtcNow()
}, ct);
jobs.Add(jobId);
@@ -100,6 +101,7 @@ internal static class RegistryWebhookEndpoints
private static async Task<Results<Accepted<WebhookAcceptedResponse>, ProblemHttpResult>> HandleHarborWebhook(
[FromBody] HarborWebhookEvent notification,
IGateEvaluationQueue evaluationQueue,
TimeProvider timeProvider,
ILogger<RegistryWebhookEndpointMarker> logger,
CancellationToken ct)
{
@@ -136,7 +138,7 @@ internal static class RegistryWebhookEndpoints
Tag = resource.Tag,
RegistryUrl = notification.EventData.Repository?.RepoFullName,
Source = "harbor",
Timestamp = notification.OccurAt ?? DateTimeOffset.UtcNow
Timestamp = notification.OccurAt ?? timeProvider.GetUtcNow()
}, ct);
jobs.Add(jobId);
@@ -159,6 +161,7 @@ internal static class RegistryWebhookEndpoints
private static async Task<Results<Accepted<WebhookAcceptedResponse>, ProblemHttpResult>> HandleGenericWebhook(
[FromBody] GenericRegistryWebhook notification,
IGateEvaluationQueue evaluationQueue,
TimeProvider timeProvider,
ILogger<RegistryWebhookEndpointMarker> logger,
CancellationToken ct)
{
@@ -177,7 +180,7 @@ internal static class RegistryWebhookEndpoints
RegistryUrl = notification.RegistryUrl,
BaselineRef = notification.BaselineRef,
Source = notification.Source ?? "generic",
Timestamp = DateTimeOffset.UtcNow
Timestamp = timeProvider.GetUtcNow()
}, ct);
logger.LogInformation(

View File

@@ -5,6 +5,7 @@
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using StellaOps.Determinism.Abstractions;
using StellaOps.Policy.Exceptions.Models;
using StellaOps.Policy.Exceptions.Repositories;
@@ -21,6 +22,7 @@ public sealed class ExceptionService : IExceptionService
private readonly IExceptionRepository _repository;
private readonly IExceptionNotificationService _notificationService;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
private readonly ILogger<ExceptionService> _logger;
/// <summary>
@@ -30,11 +32,13 @@ public sealed class ExceptionService : IExceptionService
IExceptionRepository repository,
IExceptionNotificationService notificationService,
TimeProvider timeProvider,
IGuidProvider guidProvider,
ILogger<ExceptionService> logger)
{
_repository = repository;
_notificationService = notificationService;
_timeProvider = timeProvider;
_guidProvider = guidProvider;
_logger = logger;
}
@@ -537,10 +541,10 @@ public sealed class ExceptionService : IExceptionService
id.StartsWith("GO-", StringComparison.OrdinalIgnoreCase);
}
private static string GenerateExceptionId()
private string GenerateExceptionId()
{
// Format: EXC-{random alphanumeric}
return $"EXC-{Guid.NewGuid():N}"[..20];
return $"EXC-{_guidProvider.NewGuid():N}"[..20];
}
#endregion

View File

@@ -5,9 +5,11 @@
// Description: In-memory queue for gate evaluation jobs with background processing
// -----------------------------------------------------------------------------
using System.Globalization;
using System.Threading.Channels;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using StellaOps.Determinism.Abstractions;
using StellaOps.Policy.Engine.Gates;
using StellaOps.Policy.Gateway.Endpoints;
@@ -21,11 +23,15 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue
{
private readonly Channel<GateEvaluationJob> _channel;
private readonly ILogger<InMemoryGateEvaluationQueue> _logger;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
public InMemoryGateEvaluationQueue(ILogger<InMemoryGateEvaluationQueue> logger)
public InMemoryGateEvaluationQueue(ILogger<InMemoryGateEvaluationQueue> logger, TimeProvider timeProvider, IGuidProvider guidProvider)
{
ArgumentNullException.ThrowIfNull(logger);
_logger = logger;
_timeProvider = timeProvider;
_guidProvider = guidProvider;
// Bounded channel to prevent unbounded memory growth
_channel = Channel.CreateBounded<GateEvaluationJob>(new BoundedChannelOptions(1000)
@@ -46,7 +52,7 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue
{
JobId = jobId,
Request = request,
QueuedAt = DateTimeOffset.UtcNow
QueuedAt = _timeProvider.GetUtcNow()
};
await _channel.Writer.WriteAsync(job, cancellationToken).ConfigureAwait(false);
@@ -65,11 +71,11 @@ public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue
/// </summary>
public ChannelReader<GateEvaluationJob> Reader => _channel.Reader;
private static string GenerateJobId()
private string GenerateJobId()
{
// Format: gate-{timestamp}-{random}
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var random = Guid.NewGuid().ToString("N")[..8];
var timestamp = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture);
var random = _guidProvider.NewGuid().ToString("N", CultureInfo.InvariantCulture)[..8];
return $"gate-{timestamp}-{random}";
}
}

View File

@@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using StellaOps.Determinism.Abstractions;
using StellaOps.Policy.Gateway.Options;
namespace StellaOps.Policy.Gateway.Services;
@@ -17,6 +18,7 @@ internal sealed class PolicyGatewayDpopProofGenerator : IDisposable
private readonly IHostEnvironment hostEnvironment;
private readonly IOptionsMonitor<PolicyGatewayOptions> optionsMonitor;
private readonly TimeProvider timeProvider;
private readonly IGuidProvider guidProvider;
private readonly ILogger<PolicyGatewayDpopProofGenerator> logger;
private DpopKeyMaterial? keyMaterial;
private readonly object sync = new();
@@ -25,11 +27,13 @@ internal sealed class PolicyGatewayDpopProofGenerator : IDisposable
IHostEnvironment hostEnvironment,
IOptionsMonitor<PolicyGatewayOptions> optionsMonitor,
TimeProvider timeProvider,
IGuidProvider guidProvider,
ILogger<PolicyGatewayDpopProofGenerator> logger)
{
this.hostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment));
this.optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
this.timeProvider = timeProvider ?? TimeProvider.System;
this.guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -85,7 +89,7 @@ internal sealed class PolicyGatewayDpopProofGenerator : IDisposable
["htm"] = method.Method.ToUpperInvariant(),
["htu"] = NormalizeTarget(targetUri),
["iat"] = epochSeconds,
["jti"] = Guid.NewGuid().ToString("N")
["jti"] = guidProvider.NewGuid().ToString("N")
};
if (!string.IsNullOrWhiteSpace(accessToken))