Some checks failed
api-governance / spectral-lint (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
79 lines
2.6 KiB
C#
79 lines
2.6 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using StellaOps.Policy.Engine.Domain;
|
|
|
|
namespace StellaOps.Policy.Engine.Services;
|
|
|
|
/// <summary>
|
|
/// Deterministic runtime evaluator with per-digest caching.
|
|
/// </summary>
|
|
internal sealed class PolicyRuntimeEvaluator
|
|
{
|
|
private readonly IPolicyPackRepository _repository;
|
|
private readonly ConcurrentDictionary<string, PolicyEvaluationResponse> _cache = new(StringComparer.Ordinal);
|
|
|
|
public PolicyRuntimeEvaluator(IPolicyPackRepository repository)
|
|
{
|
|
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
|
}
|
|
|
|
public async Task<PolicyEvaluationResponse> EvaluateAsync(PolicyEvaluationRequest request, CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
if (string.IsNullOrWhiteSpace(request.PackId))
|
|
{
|
|
throw new ArgumentException("packId required", nameof(request));
|
|
}
|
|
|
|
if (request.Version <= 0)
|
|
{
|
|
throw new ArgumentException("version must be positive", nameof(request));
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(request.Subject))
|
|
{
|
|
throw new ArgumentException("subject required", nameof(request));
|
|
}
|
|
|
|
var bundle = await _repository.GetBundleAsync(request.PackId, request.Version, cancellationToken).ConfigureAwait(false);
|
|
if (bundle is null)
|
|
{
|
|
throw new InvalidOperationException("Bundle not found for requested revision.");
|
|
}
|
|
|
|
var cacheKey = $"{bundle.Digest}|{request.Subject}";
|
|
if (_cache.TryGetValue(cacheKey, out var cached))
|
|
{
|
|
return cached with { Cached = true };
|
|
}
|
|
|
|
var decision = ComputeDecision(bundle.Digest, request.Subject);
|
|
var correlationId = ComputeCorrelationId(cacheKey);
|
|
var response = new PolicyEvaluationResponse(
|
|
request.PackId,
|
|
request.Version,
|
|
bundle.Digest,
|
|
decision,
|
|
correlationId,
|
|
Cached: false);
|
|
|
|
_cache.TryAdd(cacheKey, response);
|
|
return response;
|
|
}
|
|
|
|
private static string ComputeDecision(string digest, string subject)
|
|
{
|
|
Span<byte> hash = stackalloc byte[32];
|
|
SHA256.HashData(Encoding.UTF8.GetBytes($"{digest}|{subject}"), hash);
|
|
return (hash[0] & 1) == 0 ? "allow" : "deny";
|
|
}
|
|
|
|
private static string ComputeCorrelationId(string value)
|
|
{
|
|
Span<byte> hash = stackalloc byte[32];
|
|
SHA256.HashData(Encoding.UTF8.GetBytes(value), hash);
|
|
return Convert.ToHexString(hash);
|
|
}
|
|
}
|