139 lines
4.4 KiB
C#
139 lines
4.4 KiB
C#
|
|
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Provcache.Entities;
|
|
using System.Collections.Concurrent;
|
|
using System.Text.Json;
|
|
|
|
namespace StellaOps.Provcache;
|
|
|
|
/// <summary>
|
|
/// In-memory implementation of the revocation ledger for testing and non-persistent scenarios.
|
|
/// For production use, inject a PostgreSQL-backed implementation from StellaOps.Provcache.Postgres.
|
|
/// </summary>
|
|
public sealed class InMemoryRevocationLedger : IRevocationLedger
|
|
{
|
|
private readonly ConcurrentDictionary<long, RevocationEntry> _entries = new();
|
|
private readonly ILogger<InMemoryRevocationLedger> _logger;
|
|
private long _currentSeqNo;
|
|
|
|
public InMemoryRevocationLedger(ILogger<InMemoryRevocationLedger> logger)
|
|
{
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<RevocationEntry> RecordAsync(
|
|
RevocationEntry entry,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entry);
|
|
|
|
var seqNo = Interlocked.Increment(ref _currentSeqNo);
|
|
var recordedEntry = entry with { SeqNo = seqNo };
|
|
|
|
_entries[seqNo] = recordedEntry;
|
|
|
|
_logger.LogInformation(
|
|
"Recorded revocation {RevocationId} of type {Type} for key {Key}, invalidated {Count} entries",
|
|
entry.RevocationId, entry.RevocationType, entry.RevokedKey, entry.EntriesInvalidated);
|
|
|
|
return Task.FromResult(recordedEntry);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<IReadOnlyList<RevocationEntry>> GetEntriesSinceAsync(
|
|
long sinceSeqNo,
|
|
int limit = 1000,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var entries = _entries.Values
|
|
.Where(e => e.SeqNo > sinceSeqNo)
|
|
.OrderBy(e => e.SeqNo)
|
|
.Take(limit)
|
|
.ToList();
|
|
|
|
return Task.FromResult<IReadOnlyList<RevocationEntry>>(entries);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<IReadOnlyList<RevocationEntry>> GetEntriesByTypeAsync(
|
|
string revocationType,
|
|
DateTimeOffset? since = null,
|
|
int limit = 1000,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(revocationType);
|
|
|
|
var query = _entries.Values
|
|
.Where(e => e.RevocationType == revocationType);
|
|
|
|
if (since.HasValue)
|
|
{
|
|
query = query.Where(e => e.RevokedAt > since.Value);
|
|
}
|
|
|
|
var entries = query
|
|
.OrderBy(e => e.SeqNo)
|
|
.Take(limit)
|
|
.ToList();
|
|
|
|
return Task.FromResult<IReadOnlyList<RevocationEntry>>(entries);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<long> GetLatestSeqNoAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
return Task.FromResult(Interlocked.Read(ref _currentSeqNo));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<IReadOnlyList<RevocationEntry>> GetRevocationsForKeyAsync(
|
|
string revokedKey,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(revokedKey);
|
|
|
|
var entries = _entries.Values
|
|
.Where(e => e.RevokedKey == revokedKey)
|
|
.OrderBy(e => e.SeqNo)
|
|
.ToList();
|
|
|
|
return Task.FromResult<IReadOnlyList<RevocationEntry>>(entries);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<RevocationLedgerStats> GetStatsAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var allEntries = _entries.Values.ToList();
|
|
var totalEntries = allEntries.Count;
|
|
var latestSeqNo = Interlocked.Read(ref _currentSeqNo);
|
|
var totalInvalidated = allEntries.Sum(e => (long)e.EntriesInvalidated);
|
|
|
|
var entriesByType = allEntries
|
|
.GroupBy(e => e.RevocationType)
|
|
.ToDictionary(g => g.Key, g => (long)g.Count());
|
|
|
|
var oldestEntry = allEntries.MinBy(e => e.SeqNo)?.RevokedAt;
|
|
var newestEntry = allEntries.MaxBy(e => e.SeqNo)?.RevokedAt;
|
|
|
|
return Task.FromResult(new RevocationLedgerStats
|
|
{
|
|
TotalEntries = totalEntries,
|
|
LatestSeqNo = latestSeqNo,
|
|
EntriesByType = entriesByType,
|
|
TotalEntriesInvalidated = totalInvalidated,
|
|
OldestEntryAt = oldestEntry,
|
|
NewestEntryAt = newestEntry
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all entries (for testing).
|
|
/// </summary>
|
|
public void Clear()
|
|
{
|
|
_entries.Clear();
|
|
Interlocked.Exchange(ref _currentSeqNo, 0);
|
|
}
|
|
}
|