feat: Add in-memory implementations for issuer audit, key, repository, and trust management
Some checks failed
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Some checks failed
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
- Introduced InMemoryIssuerAuditSink to retain audit entries for testing. - Implemented InMemoryIssuerKeyRepository for deterministic key storage. - Created InMemoryIssuerRepository to manage issuer records in memory. - Added InMemoryIssuerTrustRepository for managing issuer trust overrides. - Each repository utilizes concurrent collections for thread-safe operations. - Enhanced deprecation tracking with a comprehensive YAML schema for API governance.
This commit is contained in:
@@ -1,35 +0,0 @@
|
||||
using StellaOps.IssuerDirectory.Core.Abstractions;
|
||||
using StellaOps.IssuerDirectory.Core.Domain;
|
||||
using StellaOps.IssuerDirectory.Infrastructure.Documents;
|
||||
using StellaOps.IssuerDirectory.Infrastructure.Internal;
|
||||
|
||||
namespace StellaOps.IssuerDirectory.Infrastructure.Audit;
|
||||
|
||||
public sealed class MongoIssuerAuditSink : IIssuerAuditSink
|
||||
{
|
||||
private readonly IssuerDirectoryMongoContext _context;
|
||||
|
||||
public MongoIssuerAuditSink(IssuerDirectoryMongoContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
public async Task WriteAsync(IssuerAuditEntry entry, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entry);
|
||||
|
||||
var document = new IssuerAuditDocument
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
TenantId = entry.TenantId,
|
||||
IssuerId = entry.IssuerId,
|
||||
Action = entry.Action,
|
||||
TimestampUtc = entry.TimestampUtc,
|
||||
Actor = entry.Actor,
|
||||
Reason = entry.Reason,
|
||||
Metadata = new Dictionary<string, string>(entry.Metadata, StringComparer.OrdinalIgnoreCase)
|
||||
};
|
||||
|
||||
await _context.Audits.InsertOneAsync(document, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace StellaOps.IssuerDirectory.Infrastructure.Documents;
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
public sealed class IssuerAuditDocument
|
||||
{
|
||||
[BsonId]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
|
||||
[BsonElement("tenant_id")]
|
||||
public string TenantId { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("issuer_id")]
|
||||
public string IssuerId { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("action")]
|
||||
public string Action { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("timestamp")]
|
||||
public DateTimeOffset TimestampUtc { get; set; }
|
||||
|
||||
[BsonElement("actor")]
|
||||
public string Actor { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("reason")]
|
||||
public string? Reason { get; set; }
|
||||
|
||||
[BsonElement("metadata")]
|
||||
public Dictionary<string, string> Metadata { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace StellaOps.IssuerDirectory.Infrastructure.Documents;
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
public sealed class IssuerDocument
|
||||
{
|
||||
[BsonId]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("tenant_id")]
|
||||
public string TenantId { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("display_name")]
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("slug")]
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("description")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
[BsonElement("contact")]
|
||||
public IssuerContactDocument Contact { get; set; } = new();
|
||||
|
||||
[BsonElement("metadata")]
|
||||
public IssuerMetadataDocument Metadata { get; set; } = new();
|
||||
|
||||
[BsonElement("endpoints")]
|
||||
public List<IssuerEndpointDocument> Endpoints { get; set; } = new();
|
||||
|
||||
[BsonElement("tags")]
|
||||
public List<string> Tags { get; set; } = new();
|
||||
|
||||
[BsonElement("created_at")]
|
||||
public DateTimeOffset CreatedAtUtc { get; set; }
|
||||
|
||||
[BsonElement("created_by")]
|
||||
public string CreatedBy { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("updated_at")]
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; }
|
||||
|
||||
[BsonElement("updated_by")]
|
||||
public string UpdatedBy { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("is_seed")]
|
||||
public bool IsSystemSeed { get; set; }
|
||||
}
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
public sealed class IssuerContactDocument
|
||||
{
|
||||
[BsonElement("email")]
|
||||
public string? Email { get; set; }
|
||||
|
||||
[BsonElement("phone")]
|
||||
public string? Phone { get; set; }
|
||||
|
||||
[BsonElement("website")]
|
||||
public string? Website { get; set; }
|
||||
|
||||
[BsonElement("timezone")]
|
||||
public string? Timezone { get; set; }
|
||||
}
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
public sealed class IssuerMetadataDocument
|
||||
{
|
||||
[BsonElement("cve_org_id")]
|
||||
public string? CveOrgId { get; set; }
|
||||
|
||||
[BsonElement("csaf_publisher_id")]
|
||||
public string? CsafPublisherId { get; set; }
|
||||
|
||||
[BsonElement("security_advisories_url")]
|
||||
public string? SecurityAdvisoriesUrl { get; set; }
|
||||
|
||||
[BsonElement("catalog_url")]
|
||||
public string? CatalogUrl { get; set; }
|
||||
|
||||
[BsonElement("languages")]
|
||||
public List<string> Languages { get; set; } = new();
|
||||
|
||||
[BsonElement("attributes")]
|
||||
public Dictionary<string, string> Attributes { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
public sealed class IssuerEndpointDocument
|
||||
{
|
||||
[BsonElement("kind")]
|
||||
public string Kind { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("url")]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("format")]
|
||||
public string? Format { get; set; }
|
||||
|
||||
[BsonElement("requires_auth")]
|
||||
public bool RequiresAuthentication { get; set; }
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace StellaOps.IssuerDirectory.Infrastructure.Documents;
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
public sealed class IssuerKeyDocument
|
||||
{
|
||||
[BsonId]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("issuer_id")]
|
||||
public string IssuerId { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("tenant_id")]
|
||||
public string TenantId { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("status")]
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("material_format")]
|
||||
public string MaterialFormat { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("material_value")]
|
||||
public string MaterialValue { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("fingerprint")]
|
||||
public string Fingerprint { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("created_at")]
|
||||
public DateTimeOffset CreatedAtUtc { get; set; }
|
||||
|
||||
[BsonElement("created_by")]
|
||||
public string CreatedBy { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("updated_at")]
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; }
|
||||
|
||||
[BsonElement("updated_by")]
|
||||
public string UpdatedBy { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("expires_at")]
|
||||
public DateTimeOffset? ExpiresAtUtc { get; set; }
|
||||
|
||||
[BsonElement("retired_at")]
|
||||
public DateTimeOffset? RetiredAtUtc { get; set; }
|
||||
|
||||
[BsonElement("revoked_at")]
|
||||
public DateTimeOffset? RevokedAtUtc { get; set; }
|
||||
|
||||
[BsonElement("replaces_key_id")]
|
||||
public string? ReplacesKeyId { get; set; }
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace StellaOps.IssuerDirectory.Infrastructure.Documents;
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
public sealed class IssuerTrustDocument
|
||||
{
|
||||
[BsonId]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("issuer_id")]
|
||||
public string IssuerId { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("tenant_id")]
|
||||
public string TenantId { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("weight")]
|
||||
public decimal Weight { get; set; }
|
||||
|
||||
[BsonElement("reason")]
|
||||
public string? Reason { get; set; }
|
||||
|
||||
[BsonElement("created_at")]
|
||||
public DateTimeOffset CreatedAtUtc { get; set; }
|
||||
|
||||
[BsonElement("created_by")]
|
||||
public string CreatedBy { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("updated_at")]
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; }
|
||||
|
||||
[BsonElement("updated_by")]
|
||||
public string UpdatedBy { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Concurrent;
|
||||
using StellaOps.IssuerDirectory.Core.Abstractions;
|
||||
using StellaOps.IssuerDirectory.Core.Domain;
|
||||
|
||||
namespace StellaOps.IssuerDirectory.Infrastructure.InMemory;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory audit sink; retains last N entries for inspection/testing.
|
||||
/// </summary>
|
||||
internal sealed class InMemoryIssuerAuditSink : IIssuerAuditSink
|
||||
{
|
||||
private readonly ConcurrentQueue<IssuerAuditEntry> _entries = new();
|
||||
private const int MaxEntries = 1024;
|
||||
|
||||
public Task WriteAsync(IssuerAuditEntry entry, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entry);
|
||||
|
||||
_entries.Enqueue(entry);
|
||||
while (_entries.Count > MaxEntries && _entries.TryDequeue(out _))
|
||||
{
|
||||
// drop oldest to bound memory
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Collections.Concurrent;
|
||||
using StellaOps.IssuerDirectory.Core.Abstractions;
|
||||
using StellaOps.IssuerDirectory.Core.Domain;
|
||||
|
||||
namespace StellaOps.IssuerDirectory.Infrastructure.InMemory;
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic in-memory issuer key store used as a Mongo replacement.
|
||||
/// </summary>
|
||||
internal sealed class InMemoryIssuerKeyRepository : IIssuerKeyRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, IssuerKeyRecord>> _keys = new(StringComparer.Ordinal);
|
||||
|
||||
public Task<IssuerKeyRecord?> GetAsync(string tenantId, string issuerId, string keyId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(issuerId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(keyId);
|
||||
|
||||
var bucketKey = GetBucketKey(tenantId, issuerId);
|
||||
if (_keys.TryGetValue(bucketKey, out var map) && map.TryGetValue(keyId, out var record))
|
||||
{
|
||||
return Task.FromResult<IssuerKeyRecord?>(record);
|
||||
}
|
||||
|
||||
return Task.FromResult<IssuerKeyRecord?>(null);
|
||||
}
|
||||
|
||||
public Task<IssuerKeyRecord?> GetByFingerprintAsync(string tenantId, string issuerId, string fingerprint, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(issuerId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(fingerprint);
|
||||
|
||||
var bucketKey = GetBucketKey(tenantId, issuerId);
|
||||
if (_keys.TryGetValue(bucketKey, out var map))
|
||||
{
|
||||
var match = map.Values.FirstOrDefault(key => string.Equals(key.Fingerprint, fingerprint, StringComparison.Ordinal));
|
||||
return Task.FromResult<IssuerKeyRecord?>(match);
|
||||
}
|
||||
|
||||
return Task.FromResult<IssuerKeyRecord?>(null);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyCollection<IssuerKeyRecord>> ListAsync(string tenantId, string issuerId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(issuerId);
|
||||
|
||||
var bucketKey = GetBucketKey(tenantId, issuerId);
|
||||
if (_keys.TryGetValue(bucketKey, out var map))
|
||||
{
|
||||
var ordered = map.Values.OrderBy(k => k.Id, StringComparer.Ordinal).ToArray();
|
||||
return Task.FromResult<IReadOnlyCollection<IssuerKeyRecord>>(ordered);
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyCollection<IssuerKeyRecord>>(Array.Empty<IssuerKeyRecord>());
|
||||
}
|
||||
|
||||
public Task<IReadOnlyCollection<IssuerKeyRecord>> ListGlobalAsync(string issuerId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(issuerId);
|
||||
|
||||
var all = _keys.Values
|
||||
.SelectMany(dict => dict.Values)
|
||||
.Where(k => string.Equals(k.IssuerId, issuerId, StringComparison.Ordinal))
|
||||
.OrderBy(k => k.TenantId, StringComparer.Ordinal)
|
||||
.ThenBy(k => k.Id, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
return Task.FromResult<IReadOnlyCollection<IssuerKeyRecord>>(all);
|
||||
}
|
||||
|
||||
public Task UpsertAsync(IssuerKeyRecord record, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
|
||||
var bucketKey = GetBucketKey(record.TenantId, record.IssuerId);
|
||||
var map = _keys.GetOrAdd(bucketKey, _ => new ConcurrentDictionary<string, IssuerKeyRecord>(StringComparer.Ordinal));
|
||||
map.AddOrUpdate(record.Id, record, (_, _) => record);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string GetBucketKey(string tenantId, string issuerId)
|
||||
{
|
||||
return $"{tenantId}|{issuerId}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Collections.Concurrent;
|
||||
using StellaOps.IssuerDirectory.Core.Abstractions;
|
||||
using StellaOps.IssuerDirectory.Core.Domain;
|
||||
|
||||
namespace StellaOps.IssuerDirectory.Infrastructure.InMemory;
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic in-memory issuer store used as a Mongo replacement.
|
||||
/// </summary>
|
||||
internal sealed class InMemoryIssuerRepository : IIssuerRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, IssuerRecord>> _issuers = new(StringComparer.Ordinal);
|
||||
|
||||
public Task<IssuerRecord?> GetAsync(string tenantId, string issuerId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(issuerId);
|
||||
|
||||
if (_issuers.TryGetValue(tenantId, out var map) && map.TryGetValue(issuerId, out var record))
|
||||
{
|
||||
return Task.FromResult<IssuerRecord?>(record);
|
||||
}
|
||||
|
||||
return Task.FromResult<IssuerRecord?>(null);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyCollection<IssuerRecord>> ListAsync(string tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
|
||||
if (_issuers.TryGetValue(tenantId, out var map))
|
||||
{
|
||||
var ordered = map.Values.OrderBy(r => r.Id, StringComparer.Ordinal).ToArray();
|
||||
return Task.FromResult<IReadOnlyCollection<IssuerRecord>>(ordered);
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyCollection<IssuerRecord>>(Array.Empty<IssuerRecord>());
|
||||
}
|
||||
|
||||
public Task<IReadOnlyCollection<IssuerRecord>> ListGlobalAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var ordered = _issuers.Values
|
||||
.SelectMany(dict => dict.Values)
|
||||
.OrderBy(r => r.TenantId, StringComparer.Ordinal)
|
||||
.ThenBy(r => r.Id, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
return Task.FromResult<IReadOnlyCollection<IssuerRecord>>(ordered);
|
||||
}
|
||||
|
||||
public Task UpsertAsync(IssuerRecord record, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
|
||||
var tenantMap = _issuers.GetOrAdd(record.TenantId, _ => new ConcurrentDictionary<string, IssuerRecord>(StringComparer.Ordinal));
|
||||
tenantMap.AddOrUpdate(record.Id, record, (_, _) => record);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DeleteAsync(string tenantId, string issuerId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(issuerId);
|
||||
|
||||
if (_issuers.TryGetValue(tenantId, out var map))
|
||||
{
|
||||
map.TryRemove(issuerId, out _);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Concurrent;
|
||||
using StellaOps.IssuerDirectory.Core.Abstractions;
|
||||
using StellaOps.IssuerDirectory.Core.Domain;
|
||||
|
||||
namespace StellaOps.IssuerDirectory.Infrastructure.InMemory;
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic in-memory trust override store used as a Mongo replacement.
|
||||
/// </summary>
|
||||
internal sealed class InMemoryIssuerTrustRepository : IIssuerTrustRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, IssuerTrustOverrideRecord> _trust = new(StringComparer.Ordinal);
|
||||
|
||||
public Task<IssuerTrustOverrideRecord?> GetAsync(string tenantId, string issuerId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(issuerId);
|
||||
|
||||
var key = GetKey(tenantId, issuerId);
|
||||
return Task.FromResult(_trust.TryGetValue(key, out var record) ? record : null);
|
||||
}
|
||||
|
||||
public Task UpsertAsync(IssuerTrustOverrideRecord record, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
var key = GetKey(record.TenantId, record.IssuerId);
|
||||
_trust[key] = record;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DeleteAsync(string tenantId, string issuerId, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(issuerId);
|
||||
|
||||
var key = GetKey(tenantId, issuerId);
|
||||
_trust.TryRemove(key, out _);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string GetKey(string tenantId, string issuerId) => $"{tenantId}|{issuerId}";
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.IssuerDirectory.Infrastructure.Documents;
|
||||
using StellaOps.IssuerDirectory.Infrastructure.Options;
|
||||
|
||||
namespace StellaOps.IssuerDirectory.Infrastructure.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// MongoDB context for Issuer Directory persistence.
|
||||
/// </summary>
|
||||
public sealed class IssuerDirectoryMongoContext
|
||||
{
|
||||
public IssuerDirectoryMongoContext(
|
||||
IOptions<IssuerDirectoryMongoOptions> options,
|
||||
ILogger<IssuerDirectoryMongoContext> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
var value = options.Value ?? throw new InvalidOperationException("Mongo options must be provided.");
|
||||
value.Validate();
|
||||
|
||||
var mongoUrl = new MongoUrl(value.ConnectionString);
|
||||
var settings = MongoClientSettings.FromUrl(mongoUrl);
|
||||
if (mongoUrl.UseTls is true && settings.SslSettings is not null)
|
||||
{
|
||||
settings.SslSettings.CheckCertificateRevocation = true;
|
||||
}
|
||||
|
||||
var client = new MongoClient(settings);
|
||||
var database = client.GetDatabase(value.Database);
|
||||
|
||||
logger.LogDebug("IssuerDirectory Mongo connected to {Database}", value.Database);
|
||||
|
||||
Issuers = database.GetCollection<IssuerDocument>(value.IssuersCollection);
|
||||
IssuerKeys = database.GetCollection<IssuerKeyDocument>(value.IssuerKeysCollection);
|
||||
IssuerTrustOverrides = database.GetCollection<IssuerTrustDocument>(value.IssuerTrustCollection);
|
||||
Audits = database.GetCollection<IssuerAuditDocument>(value.AuditCollection);
|
||||
|
||||
EnsureIndexes().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public IMongoCollection<IssuerDocument> Issuers { get; }
|
||||
|
||||
public IMongoCollection<IssuerKeyDocument> IssuerKeys { get; }
|
||||
|
||||
public IMongoCollection<IssuerTrustDocument> IssuerTrustOverrides { get; }
|
||||
|
||||
public IMongoCollection<IssuerAuditDocument> Audits { get; }
|
||||
|
||||
private async Task EnsureIndexes()
|
||||
{
|
||||
var tenantSlugIndex = new CreateIndexModel<IssuerDocument>(
|
||||
Builders<IssuerDocument>.IndexKeys
|
||||
.Ascending(document => document.TenantId)
|
||||
.Ascending(document => document.Slug),
|
||||
new CreateIndexOptions<IssuerDocument>
|
||||
{
|
||||
Name = "tenant_slug_unique",
|
||||
Unique = true
|
||||
});
|
||||
|
||||
await Issuers.Indexes.CreateOneAsync(tenantSlugIndex).ConfigureAwait(false);
|
||||
|
||||
var keyIndex = new CreateIndexModel<IssuerKeyDocument>(
|
||||
Builders<IssuerKeyDocument>.IndexKeys
|
||||
.Ascending(document => document.TenantId)
|
||||
.Ascending(document => document.IssuerId)
|
||||
.Ascending(document => document.Id),
|
||||
new CreateIndexOptions<IssuerKeyDocument>
|
||||
{
|
||||
Name = "issuer_keys_unique",
|
||||
Unique = true
|
||||
});
|
||||
|
||||
var fingerprintIndex = new CreateIndexModel<IssuerKeyDocument>(
|
||||
Builders<IssuerKeyDocument>.IndexKeys
|
||||
.Ascending(document => document.TenantId)
|
||||
.Ascending(document => document.IssuerId)
|
||||
.Ascending(document => document.Fingerprint),
|
||||
new CreateIndexOptions<IssuerKeyDocument>
|
||||
{
|
||||
Name = "issuer_keys_fingerprint",
|
||||
Unique = true
|
||||
});
|
||||
|
||||
await IssuerKeys.Indexes.CreateOneAsync(keyIndex).ConfigureAwait(false);
|
||||
await IssuerKeys.Indexes.CreateOneAsync(fingerprintIndex).ConfigureAwait(false);
|
||||
|
||||
var trustIndex = new CreateIndexModel<IssuerTrustDocument>(
|
||||
Builders<IssuerTrustDocument>.IndexKeys
|
||||
.Ascending(document => document.TenantId)
|
||||
.Ascending(document => document.IssuerId),
|
||||
new CreateIndexOptions<IssuerTrustDocument>
|
||||
{
|
||||
Name = "issuer_trust_unique",
|
||||
Unique = true
|
||||
});
|
||||
|
||||
await IssuerTrustOverrides.Indexes.CreateOneAsync(trustIndex).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
namespace StellaOps.IssuerDirectory.Infrastructure.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Mongo persistence configuration for the Issuer Directory service.
|
||||
/// </summary>
|
||||
public sealed class IssuerDirectoryMongoOptions
|
||||
{
|
||||
public const string SectionName = "IssuerDirectory:Mongo";
|
||||
|
||||
public string ConnectionString { get; set; } = "mongodb://localhost:27017";
|
||||
|
||||
public string Database { get; set; } = "issuer-directory";
|
||||
|
||||
public string IssuersCollection { get; set; } = "issuers";
|
||||
|
||||
public string IssuerKeysCollection { get; set; } = "issuer_keys";
|
||||
|
||||
public string IssuerTrustCollection { get; set; } = "issuer_trust_overrides";
|
||||
|
||||
public string AuditCollection { get; set; } = "issuer_audit";
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ConnectionString))
|
||||
{
|
||||
throw new InvalidOperationException("IssuerDirectory Mongo connection string must be configured.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Database))
|
||||
{
|
||||
throw new InvalidOperationException("IssuerDirectory Mongo database must be configured.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(IssuersCollection))
|
||||
{
|
||||
throw new InvalidOperationException("IssuerDirectory Mongo issuers collection must be configured.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(IssuerKeysCollection))
|
||||
{
|
||||
throw new InvalidOperationException("IssuerDirectory Mongo issuer keys collection must be configured.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(IssuerTrustCollection))
|
||||
{
|
||||
throw new InvalidOperationException("IssuerDirectory Mongo issuer trust collection must be configured.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(AuditCollection))
|
||||
{
|
||||
throw new InvalidOperationException("IssuerDirectory Mongo audit collection must be configured.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.IssuerDirectory.Core.Abstractions;
|
||||
using StellaOps.IssuerDirectory.Core.Domain;
|
||||
using StellaOps.IssuerDirectory.Infrastructure.Documents;
|
||||
using StellaOps.IssuerDirectory.Infrastructure.Internal;
|
||||
|
||||
namespace StellaOps.IssuerDirectory.Infrastructure.Repositories;
|
||||
|
||||
public sealed class MongoIssuerKeyRepository : IIssuerKeyRepository
|
||||
{
|
||||
private readonly IssuerDirectoryMongoContext _context;
|
||||
|
||||
public MongoIssuerKeyRepository(IssuerDirectoryMongoContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
public async Task<IssuerKeyRecord?> GetAsync(string tenantId, string issuerId, string keyId, CancellationToken cancellationToken)
|
||||
{
|
||||
var filter = Builders<IssuerKeyDocument>.Filter.And(
|
||||
Builders<IssuerKeyDocument>.Filter.Eq(doc => doc.TenantId, tenantId),
|
||||
Builders<IssuerKeyDocument>.Filter.Eq(doc => doc.IssuerId, issuerId),
|
||||
Builders<IssuerKeyDocument>.Filter.Eq(doc => doc.Id, keyId));
|
||||
|
||||
var document = await _context.IssuerKeys.Find(filter).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
return document is null ? null : MapToDomain(document);
|
||||
}
|
||||
|
||||
public async Task<IssuerKeyRecord?> GetByFingerprintAsync(string tenantId, string issuerId, string fingerprint, CancellationToken cancellationToken)
|
||||
{
|
||||
var filter = Builders<IssuerKeyDocument>.Filter.And(
|
||||
Builders<IssuerKeyDocument>.Filter.Eq(doc => doc.TenantId, tenantId),
|
||||
Builders<IssuerKeyDocument>.Filter.Eq(doc => doc.IssuerId, issuerId),
|
||||
Builders<IssuerKeyDocument>.Filter.Eq(doc => doc.Fingerprint, fingerprint));
|
||||
|
||||
var document = await _context.IssuerKeys.Find(filter).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
return document is null ? null : MapToDomain(document);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<IssuerKeyRecord>> ListAsync(string tenantId, string issuerId, CancellationToken cancellationToken)
|
||||
{
|
||||
var filter = Builders<IssuerKeyDocument>.Filter.And(
|
||||
Builders<IssuerKeyDocument>.Filter.Eq(doc => doc.TenantId, tenantId),
|
||||
Builders<IssuerKeyDocument>.Filter.Eq(doc => doc.IssuerId, issuerId));
|
||||
|
||||
var documents = await _context.IssuerKeys
|
||||
.Find(filter)
|
||||
.SortBy(document => document.CreatedAtUtc)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return documents.Select(MapToDomain).ToArray();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<IssuerKeyRecord>> ListGlobalAsync(string issuerId, CancellationToken cancellationToken)
|
||||
{
|
||||
var filter = Builders<IssuerKeyDocument>.Filter.And(
|
||||
Builders<IssuerKeyDocument>.Filter.Eq(doc => doc.TenantId, IssuerTenants.Global),
|
||||
Builders<IssuerKeyDocument>.Filter.Eq(doc => doc.IssuerId, issuerId));
|
||||
|
||||
var documents = await _context.IssuerKeys
|
||||
.Find(filter)
|
||||
.SortBy(document => document.CreatedAtUtc)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return documents.Select(MapToDomain).ToArray();
|
||||
}
|
||||
|
||||
public async Task UpsertAsync(IssuerKeyRecord record, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
|
||||
var document = MapToDocument(record);
|
||||
var filter = Builders<IssuerKeyDocument>.Filter.And(
|
||||
Builders<IssuerKeyDocument>.Filter.Eq(doc => doc.TenantId, record.TenantId),
|
||||
Builders<IssuerKeyDocument>.Filter.Eq(doc => doc.IssuerId, record.IssuerId),
|
||||
Builders<IssuerKeyDocument>.Filter.Eq(doc => doc.Id, record.Id));
|
||||
|
||||
await _context.IssuerKeys.ReplaceOneAsync(
|
||||
filter,
|
||||
document,
|
||||
new ReplaceOptions { IsUpsert = true },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static IssuerKeyRecord MapToDomain(IssuerKeyDocument document)
|
||||
{
|
||||
return new IssuerKeyRecord
|
||||
{
|
||||
Id = document.Id,
|
||||
IssuerId = document.IssuerId,
|
||||
TenantId = document.TenantId,
|
||||
Type = Enum.Parse<IssuerKeyType>(document.Type, ignoreCase: true),
|
||||
Status = Enum.Parse<IssuerKeyStatus>(document.Status, ignoreCase: true),
|
||||
Material = new IssuerKeyMaterial(document.MaterialFormat, document.MaterialValue),
|
||||
Fingerprint = document.Fingerprint,
|
||||
CreatedAtUtc = document.CreatedAtUtc,
|
||||
CreatedBy = document.CreatedBy,
|
||||
UpdatedAtUtc = document.UpdatedAtUtc,
|
||||
UpdatedBy = document.UpdatedBy,
|
||||
ExpiresAtUtc = document.ExpiresAtUtc,
|
||||
RetiredAtUtc = document.RetiredAtUtc,
|
||||
RevokedAtUtc = document.RevokedAtUtc,
|
||||
ReplacesKeyId = document.ReplacesKeyId
|
||||
};
|
||||
}
|
||||
|
||||
private static IssuerKeyDocument MapToDocument(IssuerKeyRecord record)
|
||||
{
|
||||
return new IssuerKeyDocument
|
||||
{
|
||||
Id = record.Id,
|
||||
IssuerId = record.IssuerId,
|
||||
TenantId = record.TenantId,
|
||||
Type = record.Type.ToString(),
|
||||
Status = record.Status.ToString(),
|
||||
MaterialFormat = record.Material.Format,
|
||||
MaterialValue = record.Material.Value,
|
||||
Fingerprint = record.Fingerprint,
|
||||
CreatedAtUtc = record.CreatedAtUtc,
|
||||
CreatedBy = record.CreatedBy,
|
||||
UpdatedAtUtc = record.UpdatedAtUtc,
|
||||
UpdatedBy = record.UpdatedBy,
|
||||
ExpiresAtUtc = record.ExpiresAtUtc,
|
||||
RetiredAtUtc = record.RetiredAtUtc,
|
||||
RevokedAtUtc = record.RevokedAtUtc,
|
||||
ReplacesKeyId = record.ReplacesKeyId
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.IssuerDirectory.Core.Abstractions;
|
||||
using StellaOps.IssuerDirectory.Core.Domain;
|
||||
using StellaOps.IssuerDirectory.Infrastructure.Documents;
|
||||
using StellaOps.IssuerDirectory.Infrastructure.Internal;
|
||||
|
||||
namespace StellaOps.IssuerDirectory.Infrastructure.Repositories;
|
||||
|
||||
public sealed class MongoIssuerRepository : IIssuerRepository
|
||||
{
|
||||
private readonly IssuerDirectoryMongoContext _context;
|
||||
|
||||
public MongoIssuerRepository(IssuerDirectoryMongoContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
public async Task<IssuerRecord?> GetAsync(string tenantId, string issuerId, CancellationToken cancellationToken)
|
||||
{
|
||||
var filter = Builders<IssuerDocument>.Filter.And(
|
||||
Builders<IssuerDocument>.Filter.Eq(doc => doc.TenantId, tenantId),
|
||||
Builders<IssuerDocument>.Filter.Eq(doc => doc.Id, issuerId));
|
||||
|
||||
var cursor = await _context.Issuers
|
||||
.Find(filter)
|
||||
.Limit(1)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return cursor is null ? null : MapToDomain(cursor);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<IssuerRecord>> ListAsync(string tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var filter = Builders<IssuerDocument>.Filter.Eq(doc => doc.TenantId, tenantId);
|
||||
var documents = await _context.Issuers.Find(filter)
|
||||
.SortBy(doc => doc.Slug)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return documents.Select(MapToDomain).ToArray();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<IssuerRecord>> ListGlobalAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var documents = await _context.Issuers
|
||||
.Find(doc => doc.TenantId == IssuerTenants.Global)
|
||||
.SortBy(doc => doc.Slug)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return documents.Select(MapToDomain).ToArray();
|
||||
}
|
||||
|
||||
public async Task UpsertAsync(IssuerRecord record, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
|
||||
var document = MapToDocument(record);
|
||||
var filter = Builders<IssuerDocument>.Filter.And(
|
||||
Builders<IssuerDocument>.Filter.Eq(doc => doc.TenantId, record.TenantId),
|
||||
Builders<IssuerDocument>.Filter.Eq(doc => doc.Id, record.Id));
|
||||
|
||||
await _context.Issuers
|
||||
.ReplaceOneAsync(
|
||||
filter,
|
||||
document,
|
||||
new ReplaceOptions { IsUpsert = true },
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string tenantId, string issuerId, CancellationToken cancellationToken)
|
||||
{
|
||||
var filter = Builders<IssuerDocument>.Filter.And(
|
||||
Builders<IssuerDocument>.Filter.Eq(doc => doc.TenantId, tenantId),
|
||||
Builders<IssuerDocument>.Filter.Eq(doc => doc.Id, issuerId));
|
||||
|
||||
await _context.Issuers.DeleteOneAsync(filter, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static IssuerRecord MapToDomain(IssuerDocument document)
|
||||
{
|
||||
var contact = new IssuerContact(
|
||||
document.Contact.Email,
|
||||
document.Contact.Phone,
|
||||
string.IsNullOrWhiteSpace(document.Contact.Website) ? null : new Uri(document.Contact.Website),
|
||||
document.Contact.Timezone);
|
||||
|
||||
var metadata = new IssuerMetadata(
|
||||
document.Metadata.CveOrgId,
|
||||
document.Metadata.CsafPublisherId,
|
||||
string.IsNullOrWhiteSpace(document.Metadata.SecurityAdvisoriesUrl)
|
||||
? null
|
||||
: new Uri(document.Metadata.SecurityAdvisoriesUrl),
|
||||
string.IsNullOrWhiteSpace(document.Metadata.CatalogUrl)
|
||||
? null
|
||||
: new Uri(document.Metadata.CatalogUrl),
|
||||
document.Metadata.Languages,
|
||||
document.Metadata.Attributes);
|
||||
|
||||
var endpoints = document.Endpoints
|
||||
.Select(endpoint => new IssuerEndpoint(
|
||||
endpoint.Kind,
|
||||
new Uri(endpoint.Url),
|
||||
endpoint.Format,
|
||||
endpoint.RequiresAuthentication))
|
||||
.ToArray();
|
||||
|
||||
return new IssuerRecord
|
||||
{
|
||||
Id = document.Id,
|
||||
TenantId = document.TenantId,
|
||||
DisplayName = document.DisplayName,
|
||||
Slug = document.Slug,
|
||||
Description = document.Description,
|
||||
Contact = contact,
|
||||
Metadata = metadata,
|
||||
Endpoints = endpoints,
|
||||
Tags = document.Tags,
|
||||
CreatedAtUtc = document.CreatedAtUtc,
|
||||
CreatedBy = document.CreatedBy,
|
||||
UpdatedAtUtc = document.UpdatedAtUtc,
|
||||
UpdatedBy = document.UpdatedBy,
|
||||
IsSystemSeed = document.IsSystemSeed
|
||||
};
|
||||
}
|
||||
|
||||
private static IssuerDocument MapToDocument(IssuerRecord record)
|
||||
{
|
||||
var contact = new IssuerContactDocument
|
||||
{
|
||||
Email = record.Contact.Email,
|
||||
Phone = record.Contact.Phone,
|
||||
Website = record.Contact.Website?.ToString(),
|
||||
Timezone = record.Contact.Timezone
|
||||
};
|
||||
|
||||
var metadataDocument = new IssuerMetadataDocument
|
||||
{
|
||||
CveOrgId = record.Metadata.CveOrgId,
|
||||
CsafPublisherId = record.Metadata.CsafPublisherId,
|
||||
SecurityAdvisoriesUrl = record.Metadata.SecurityAdvisoriesUrl?.ToString(),
|
||||
CatalogUrl = record.Metadata.CatalogUrl?.ToString(),
|
||||
Languages = record.Metadata.SupportedLanguages.ToList(),
|
||||
Attributes = new Dictionary<string, string>(record.Metadata.Attributes, StringComparer.OrdinalIgnoreCase)
|
||||
};
|
||||
|
||||
var endpoints = record.Endpoints
|
||||
.Select(endpoint => new IssuerEndpointDocument
|
||||
{
|
||||
Kind = endpoint.Kind,
|
||||
Url = endpoint.Url.ToString(),
|
||||
Format = endpoint.Format,
|
||||
RequiresAuthentication = endpoint.RequiresAuthentication
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return new IssuerDocument
|
||||
{
|
||||
Id = record.Id,
|
||||
TenantId = record.TenantId,
|
||||
DisplayName = record.DisplayName,
|
||||
Slug = record.Slug,
|
||||
Description = record.Description,
|
||||
Contact = contact,
|
||||
Metadata = metadataDocument,
|
||||
Endpoints = endpoints,
|
||||
Tags = record.Tags.ToList(),
|
||||
CreatedAtUtc = record.CreatedAtUtc,
|
||||
CreatedBy = record.CreatedBy,
|
||||
UpdatedAtUtc = record.UpdatedAtUtc,
|
||||
UpdatedBy = record.UpdatedBy,
|
||||
IsSystemSeed = record.IsSystemSeed
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
using System.Globalization;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.IssuerDirectory.Core.Abstractions;
|
||||
using StellaOps.IssuerDirectory.Core.Domain;
|
||||
using StellaOps.IssuerDirectory.Infrastructure.Documents;
|
||||
using StellaOps.IssuerDirectory.Infrastructure.Internal;
|
||||
|
||||
namespace StellaOps.IssuerDirectory.Infrastructure.Repositories;
|
||||
|
||||
public sealed class MongoIssuerTrustRepository : IIssuerTrustRepository
|
||||
{
|
||||
private readonly IssuerDirectoryMongoContext _context;
|
||||
|
||||
public MongoIssuerTrustRepository(IssuerDirectoryMongoContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
public async Task<IssuerTrustOverrideRecord?> GetAsync(string tenantId, string issuerId, CancellationToken cancellationToken)
|
||||
{
|
||||
var filter = Builders<IssuerTrustDocument>.Filter.And(
|
||||
Builders<IssuerTrustDocument>.Filter.Eq(doc => doc.TenantId, tenantId),
|
||||
Builders<IssuerTrustDocument>.Filter.Eq(doc => doc.IssuerId, issuerId));
|
||||
|
||||
var document = await _context.IssuerTrustOverrides
|
||||
.Find(filter)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return document is null ? null : MapToDomain(document);
|
||||
}
|
||||
|
||||
public async Task UpsertAsync(IssuerTrustOverrideRecord record, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
|
||||
var document = MapToDocument(record);
|
||||
var filter = Builders<IssuerTrustDocument>.Filter.And(
|
||||
Builders<IssuerTrustDocument>.Filter.Eq(doc => doc.TenantId, record.TenantId),
|
||||
Builders<IssuerTrustDocument>.Filter.Eq(doc => doc.IssuerId, record.IssuerId));
|
||||
|
||||
await _context.IssuerTrustOverrides.ReplaceOneAsync(
|
||||
filter,
|
||||
document,
|
||||
new ReplaceOptions { IsUpsert = true },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string tenantId, string issuerId, CancellationToken cancellationToken)
|
||||
{
|
||||
var filter = Builders<IssuerTrustDocument>.Filter.And(
|
||||
Builders<IssuerTrustDocument>.Filter.Eq(doc => doc.TenantId, tenantId),
|
||||
Builders<IssuerTrustDocument>.Filter.Eq(doc => doc.IssuerId, issuerId));
|
||||
|
||||
await _context.IssuerTrustOverrides.DeleteOneAsync(filter, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static IssuerTrustOverrideRecord MapToDomain(IssuerTrustDocument document)
|
||||
{
|
||||
return new IssuerTrustOverrideRecord
|
||||
{
|
||||
IssuerId = document.IssuerId,
|
||||
TenantId = document.TenantId,
|
||||
Weight = document.Weight,
|
||||
Reason = document.Reason,
|
||||
CreatedAtUtc = document.CreatedAtUtc,
|
||||
CreatedBy = document.CreatedBy,
|
||||
UpdatedAtUtc = document.UpdatedAtUtc,
|
||||
UpdatedBy = document.UpdatedBy
|
||||
};
|
||||
}
|
||||
|
||||
private static IssuerTrustDocument MapToDocument(IssuerTrustOverrideRecord record)
|
||||
{
|
||||
return new IssuerTrustDocument
|
||||
{
|
||||
Id = string.Create(CultureInfo.InvariantCulture, $"{record.TenantId}:{record.IssuerId}"),
|
||||
IssuerId = record.IssuerId,
|
||||
TenantId = record.TenantId,
|
||||
Weight = record.Weight,
|
||||
Reason = record.Reason,
|
||||
CreatedAtUtc = record.CreatedAtUtc,
|
||||
CreatedBy = record.CreatedBy,
|
||||
UpdatedAtUtc = record.UpdatedAtUtc,
|
||||
UpdatedBy = record.UpdatedBy
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.IssuerDirectory.Core.Abstractions;
|
||||
using StellaOps.IssuerDirectory.Infrastructure.Audit;
|
||||
using StellaOps.IssuerDirectory.Infrastructure.Internal;
|
||||
using StellaOps.IssuerDirectory.Infrastructure.Options;
|
||||
using StellaOps.IssuerDirectory.Infrastructure.Repositories;
|
||||
using StellaOps.IssuerDirectory.Infrastructure.InMemory;
|
||||
|
||||
namespace StellaOps.IssuerDirectory.Infrastructure;
|
||||
|
||||
@@ -17,19 +14,10 @@ public static class ServiceCollectionExtensions
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
services.AddOptions<IssuerDirectoryMongoOptions>()
|
||||
.Bind(configuration.GetSection(IssuerDirectoryMongoOptions.SectionName))
|
||||
.Validate(options =>
|
||||
{
|
||||
options.Validate();
|
||||
return true;
|
||||
});
|
||||
|
||||
services.AddSingleton<IssuerDirectoryMongoContext>();
|
||||
services.AddSingleton<IIssuerRepository, MongoIssuerRepository>();
|
||||
services.AddSingleton<IIssuerKeyRepository, MongoIssuerKeyRepository>();
|
||||
services.AddSingleton<IIssuerTrustRepository, MongoIssuerTrustRepository>();
|
||||
services.AddSingleton<IIssuerAuditSink, MongoIssuerAuditSink>();
|
||||
services.AddSingleton<IIssuerRepository, InMemoryIssuerRepository>();
|
||||
services.AddSingleton<IIssuerKeyRepository, InMemoryIssuerKeyRepository>();
|
||||
services.AddSingleton<IIssuerTrustRepository, InMemoryIssuerTrustRepository>();
|
||||
services.AddSingleton<IIssuerAuditSink, InMemoryIssuerAuditSink>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
|
||||
<PackageReference Include="MongoDB.Bson" Version="3.5.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\StellaOps.IssuerDirectory.Core\\StellaOps.IssuerDirectory.Core.csproj" />
|
||||
|
||||
@@ -121,7 +121,7 @@ static void ConfigurePersistence(
|
||||
WebApplicationBuilder builder,
|
||||
IssuerDirectoryWebServiceOptions options)
|
||||
{
|
||||
var provider = options.Persistence.Provider?.Trim().ToLowerInvariant() ?? "mongo";
|
||||
var provider = options.Persistence.Provider?.Trim().ToLowerInvariant() ?? "postgres";
|
||||
|
||||
if (provider == "postgres")
|
||||
{
|
||||
@@ -134,7 +134,7 @@ static void ConfigurePersistence(
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Information("Using MongoDB persistence for IssuerDirectory.");
|
||||
Log.Information("Using in-memory persistence for IssuerDirectory (non-production).");
|
||||
builder.Services.AddIssuerDirectoryInfrastructure(builder.Configuration);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user