This commit is contained in:
StellaOps Bot
2025-12-11 08:20:04 +02:00
parent 49922dff5a
commit b8b493913a
82 changed files with 14053 additions and 1705 deletions

View File

@@ -0,0 +1,58 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Attestor.Core.Bulk;
namespace StellaOps.Attestor.Infrastructure.Bulk;
internal sealed class InMemoryBulkVerificationJobStore : IBulkVerificationJobStore
{
private readonly ConcurrentQueue<BulkVerificationJob> _queue = new();
private readonly ConcurrentDictionary<string, BulkVerificationJob> _jobs = new(StringComparer.OrdinalIgnoreCase);
public Task<BulkVerificationJob> CreateAsync(BulkVerificationJob job, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(job);
_jobs[job.Id] = job;
_queue.Enqueue(job);
return Task.FromResult(job);
}
public Task<BulkVerificationJob?> GetAsync(string jobId, CancellationToken cancellationToken = default)
{
_jobs.TryGetValue(jobId, out var job);
return Task.FromResult(job);
}
public Task<BulkVerificationJob?> TryAcquireAsync(CancellationToken cancellationToken = default)
{
while (_queue.TryDequeue(out var job))
{
if (job.Status != BulkVerificationJobStatus.Queued)
{
continue;
}
job.Status = BulkVerificationJobStatus.Running;
job.StartedAt ??= DateTimeOffset.UtcNow;
return Task.FromResult<BulkVerificationJob?>(job);
}
return Task.FromResult<BulkVerificationJob?>(null);
}
public Task<bool> TryUpdateAsync(BulkVerificationJob job, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(job);
_jobs[job.Id] = job;
return Task.FromResult(true);
}
public Task<int> CountQueuedAsync(CancellationToken cancellationToken = default)
{
var count = _jobs.Values.Count(j => j.Status == BulkVerificationJobStatus.Queued);
return Task.FromResult(count);
}
}

View File

@@ -1,343 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using StellaOps.Attestor.Core.Bulk;
using StellaOps.Attestor.Core.Verification;
namespace StellaOps.Attestor.Infrastructure.Bulk;
internal sealed class MongoBulkVerificationJobStore : IBulkVerificationJobStore
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
private readonly IMongoCollection<JobDocument> _collection;
public MongoBulkVerificationJobStore(IMongoCollection<JobDocument> collection)
{
_collection = collection ?? throw new ArgumentNullException(nameof(collection));
}
public async Task<BulkVerificationJob> CreateAsync(BulkVerificationJob job, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(job);
job.Version = 0;
var document = JobDocument.FromDomain(job, SerializerOptions);
await _collection.InsertOneAsync(document, cancellationToken: cancellationToken).ConfigureAwait(false);
job.Version = document.Version;
return job;
}
public async Task<BulkVerificationJob?> GetAsync(string jobId, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(jobId))
{
return null;
}
var filter = Builders<JobDocument>.Filter.Eq(doc => doc.Id, jobId);
var document = await _collection.Find(filter).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
return document?.ToDomain(SerializerOptions);
}
public async Task<BulkVerificationJob?> TryAcquireAsync(CancellationToken cancellationToken = default)
{
var filter = Builders<JobDocument>.Filter.Eq(doc => doc.Status, BulkVerificationJobStatus.Queued);
var update = Builders<JobDocument>.Update
.Set(doc => doc.Status, BulkVerificationJobStatus.Running)
.Set(doc => doc.StartedAt, DateTimeOffset.UtcNow.UtcDateTime)
.Inc(doc => doc.Version, 1);
var options = new FindOneAndUpdateOptions<JobDocument>
{
Sort = Builders<JobDocument>.Sort.Ascending(doc => doc.CreatedAt),
ReturnDocument = ReturnDocument.After
};
var document = await _collection.FindOneAndUpdateAsync(filter, update, options, cancellationToken).ConfigureAwait(false);
return document?.ToDomain(SerializerOptions);
}
public async Task<bool> TryUpdateAsync(BulkVerificationJob job, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(job);
var currentVersion = job.Version;
var replacement = JobDocument.FromDomain(job, SerializerOptions);
replacement.Version = currentVersion + 1;
var filter = Builders<JobDocument>.Filter.Where(doc => doc.Id == job.Id && doc.Version == currentVersion);
var result = await _collection.ReplaceOneAsync(filter, replacement, cancellationToken: cancellationToken).ConfigureAwait(false);
if (result.ModifiedCount == 0)
{
return false;
}
job.Version = replacement.Version;
return true;
}
public async Task<int> CountQueuedAsync(CancellationToken cancellationToken = default)
{
var filter = Builders<JobDocument>.Filter.Eq(doc => doc.Status, BulkVerificationJobStatus.Queued);
var count = await _collection.CountDocumentsAsync(filter, cancellationToken: cancellationToken).ConfigureAwait(false);
return Convert.ToInt32(count);
}
internal sealed class JobDocument
{
[BsonId]
[BsonElement("_id")]
public string Id { get; set; } = string.Empty;
[BsonElement("version")]
public int Version { get; set; }
[BsonElement("status")]
[BsonRepresentation(BsonType.String)]
public BulkVerificationJobStatus Status { get; set; }
[BsonElement("createdAt")]
public DateTime CreatedAt { get; set; }
[BsonElement("startedAt")]
[BsonIgnoreIfNull]
public DateTime? StartedAt { get; set; }
[BsonElement("completedAt")]
[BsonIgnoreIfNull]
public DateTime? CompletedAt { get; set; }
[BsonElement("context")]
public JobContextDocument Context { get; set; } = new();
[BsonElement("items")]
public List<JobItemDocument> Items { get; set; } = new();
[BsonElement("processed")]
public int ProcessedCount { get; set; }
[BsonElement("succeeded")]
public int SucceededCount { get; set; }
[BsonElement("failed")]
public int FailedCount { get; set; }
[BsonElement("failureReason")]
[BsonIgnoreIfNull]
public string? FailureReason { get; set; }
public static JobDocument FromDomain(BulkVerificationJob job, JsonSerializerOptions serializerOptions)
{
return new JobDocument
{
Id = job.Id,
Version = job.Version,
Status = job.Status,
CreatedAt = job.CreatedAt.UtcDateTime,
StartedAt = job.StartedAt?.UtcDateTime,
CompletedAt = job.CompletedAt?.UtcDateTime,
Context = JobContextDocument.FromDomain(job.Context),
Items = JobItemDocument.FromDomain(job.Items, serializerOptions),
ProcessedCount = job.ProcessedCount,
SucceededCount = job.SucceededCount,
FailedCount = job.FailedCount,
FailureReason = job.FailureReason
};
}
public BulkVerificationJob ToDomain(JsonSerializerOptions serializerOptions)
{
return new BulkVerificationJob
{
Id = Id,
Version = Version,
Status = Status,
CreatedAt = DateTime.SpecifyKind(CreatedAt, DateTimeKind.Utc),
StartedAt = StartedAt is null ? null : DateTime.SpecifyKind(StartedAt.Value, DateTimeKind.Utc),
CompletedAt = CompletedAt is null ? null : DateTime.SpecifyKind(CompletedAt.Value, DateTimeKind.Utc),
Context = Context.ToDomain(),
Items = JobItemDocument.ToDomain(Items, serializerOptions),
ProcessedCount = ProcessedCount,
SucceededCount = SucceededCount,
FailedCount = FailedCount,
FailureReason = FailureReason
};
}
}
internal sealed class JobContextDocument
{
[BsonElement("tenant")]
[BsonIgnoreIfNull]
public string? Tenant { get; set; }
[BsonElement("requestedBy")]
[BsonIgnoreIfNull]
public string? RequestedBy { get; set; }
[BsonElement("clientId")]
[BsonIgnoreIfNull]
public string? ClientId { get; set; }
[BsonElement("scopes")]
public List<string> Scopes { get; set; } = new();
public static JobContextDocument FromDomain(BulkVerificationJobContext context)
{
return new JobContextDocument
{
Tenant = context.Tenant,
RequestedBy = context.RequestedBy,
ClientId = context.ClientId,
Scopes = new List<string>(context.Scopes)
};
}
public BulkVerificationJobContext ToDomain()
{
return new BulkVerificationJobContext
{
Tenant = Tenant,
RequestedBy = RequestedBy,
ClientId = ClientId,
Scopes = new List<string>(Scopes ?? new List<string>())
};
}
}
internal sealed class JobItemDocument
{
[BsonElement("index")]
public int Index { get; set; }
[BsonElement("request")]
public ItemRequestDocument Request { get; set; } = new();
[BsonElement("status")]
[BsonRepresentation(BsonType.String)]
public BulkVerificationItemStatus Status { get; set; }
[BsonElement("startedAt")]
[BsonIgnoreIfNull]
public DateTime? StartedAt { get; set; }
[BsonElement("completedAt")]
[BsonIgnoreIfNull]
public DateTime? CompletedAt { get; set; }
[BsonElement("result")]
[BsonIgnoreIfNull]
public string? ResultJson { get; set; }
[BsonElement("error")]
[BsonIgnoreIfNull]
public string? Error { get; set; }
public static List<JobItemDocument> FromDomain(IEnumerable<BulkVerificationJobItem> items, JsonSerializerOptions serializerOptions)
{
var list = new List<JobItemDocument>();
foreach (var item in items)
{
list.Add(new JobItemDocument
{
Index = item.Index,
Request = ItemRequestDocument.FromDomain(item.Request),
Status = item.Status,
StartedAt = item.StartedAt?.UtcDateTime,
CompletedAt = item.CompletedAt?.UtcDateTime,
ResultJson = item.Result is null ? null : JsonSerializer.Serialize(item.Result, serializerOptions),
Error = item.Error
});
}
return list;
}
public static IList<BulkVerificationJobItem> ToDomain(IEnumerable<JobItemDocument> documents, JsonSerializerOptions serializerOptions)
{
var list = new List<BulkVerificationJobItem>();
foreach (var document in documents)
{
AttestorVerificationResult? result = null;
if (!string.IsNullOrWhiteSpace(document.ResultJson))
{
result = JsonSerializer.Deserialize<AttestorVerificationResult>(document.ResultJson, serializerOptions);
}
list.Add(new BulkVerificationJobItem
{
Index = document.Index,
Request = document.Request.ToDomain(),
Status = document.Status,
StartedAt = document.StartedAt is null ? null : DateTime.SpecifyKind(document.StartedAt.Value, DateTimeKind.Utc),
CompletedAt = document.CompletedAt is null ? null : DateTime.SpecifyKind(document.CompletedAt.Value, DateTimeKind.Utc),
Result = result,
Error = document.Error
});
}
return list;
}
}
internal sealed class ItemRequestDocument
{
[BsonElement("uuid")]
[BsonIgnoreIfNull]
public string? Uuid { get; set; }
[BsonElement("artifactSha256")]
[BsonIgnoreIfNull]
public string? ArtifactSha256 { get; set; }
[BsonElement("subject")]
[BsonIgnoreIfNull]
public string? Subject { get; set; }
[BsonElement("envelopeId")]
[BsonIgnoreIfNull]
public string? EnvelopeId { get; set; }
[BsonElement("policyVersion")]
[BsonIgnoreIfNull]
public string? PolicyVersion { get; set; }
[BsonElement("refreshProof")]
public bool RefreshProof { get; set; }
public static ItemRequestDocument FromDomain(BulkVerificationItemRequest request)
{
return new ItemRequestDocument
{
Uuid = request.Uuid,
ArtifactSha256 = request.ArtifactSha256,
Subject = request.Subject,
EnvelopeId = request.EnvelopeId,
PolicyVersion = request.PolicyVersion,
RefreshProof = request.RefreshProof
};
}
public BulkVerificationItemRequest ToDomain()
{
return new BulkVerificationItemRequest
{
Uuid = Uuid,
ArtifactSha256 = ArtifactSha256,
Subject = Subject,
EnvelopeId = EnvelopeId,
PolicyVersion = PolicyVersion,
RefreshProof = RefreshProof
};
}
}
}

View File

@@ -1,11 +1,10 @@
using System;
using System;
using Amazon.Runtime;
using Amazon.S3;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using StackExchange.Redis;
using StellaOps.Attestor.Core.Options;
using StellaOps.Attestor.Core.Observability;
@@ -19,25 +18,26 @@ using StellaOps.Attestor.Infrastructure.Storage;
using StellaOps.Attestor.Infrastructure.Submission;
using StellaOps.Attestor.Infrastructure.Transparency;
using StellaOps.Attestor.Infrastructure.Verification;
namespace StellaOps.Attestor.Infrastructure;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddAttestorInfrastructure(this IServiceCollection services)
{
using StellaOps.Attestor.Infrastructure.Bulk;
namespace StellaOps.Attestor.Infrastructure;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddAttestorInfrastructure(this IServiceCollection services)
{
services.AddMemoryCache();
services.AddSingleton<IDsseCanonicalizer, DefaultDsseCanonicalizer>();
services.AddSingleton(sp =>
{
var canonicalizer = sp.GetRequiredService<IDsseCanonicalizer>();
var options = sp.GetRequiredService<IOptions<AttestorOptions>>().Value;
return new AttestorSubmissionValidator(canonicalizer, options.Security.SignerIdentity.Mode);
});
services.AddSingleton<AttestorMetrics>();
services.AddSingleton<IAttestorSubmissionService, AttestorSubmissionService>();
services.AddSingleton<IAttestorVerificationService, AttestorVerificationService>();
services.AddSingleton(sp =>
{
var canonicalizer = sp.GetRequiredService<IDsseCanonicalizer>();
var options = sp.GetRequiredService<IOptions<AttestorOptions>>().Value;
return new AttestorSubmissionValidator(canonicalizer, options.Security.SignerIdentity.Mode);
});
services.AddSingleton<AttestorMetrics>();
services.AddSingleton<IAttestorSubmissionService, AttestorSubmissionService>();
services.AddSingleton<IAttestorVerificationService, AttestorVerificationService>();
services.AddHttpClient<HttpRekorClient>(client =>
{
client.Timeout = TimeSpan.FromSeconds(30);
@@ -66,86 +66,55 @@ public static class ServiceCollectionExtensions
return sp.GetRequiredService<HttpTransparencyWitnessClient>();
});
services.AddSingleton<IMongoClient>(sp =>
{
var options = sp.GetRequiredService<IOptions<AttestorOptions>>().Value;
if (string.IsNullOrWhiteSpace(options.Mongo.Uri))
{
throw new InvalidOperationException("Attestor MongoDB connection string is not configured.");
}
return new MongoClient(options.Mongo.Uri);
});
services.AddSingleton(sp =>
{
var opts = sp.GetRequiredService<IOptions<AttestorOptions>>().Value;
var client = sp.GetRequiredService<IMongoClient>();
var databaseName = MongoUrl.Create(opts.Mongo.Uri).DatabaseName ?? opts.Mongo.Database;
return client.GetDatabase(databaseName);
});
services.AddSingleton(sp =>
{
var opts = sp.GetRequiredService<IOptions<AttestorOptions>>().Value;
var database = sp.GetRequiredService<IMongoDatabase>();
return database.GetCollection<MongoAttestorEntryRepository.AttestorEntryDocument>(opts.Mongo.EntriesCollection);
});
services.AddSingleton(sp =>
{
var opts = sp.GetRequiredService<IOptions<AttestorOptions>>().Value;
var database = sp.GetRequiredService<IMongoDatabase>();
return database.GetCollection<MongoAttestorAuditSink.AttestorAuditDocument>(opts.Mongo.AuditCollection);
});
services.AddSingleton<IAttestorEntryRepository, MongoAttestorEntryRepository>();
services.AddSingleton<IAttestorAuditSink, MongoAttestorAuditSink>();
services.AddSingleton<IAttestorDedupeStore>(sp =>
{
var options = sp.GetRequiredService<IOptions<AttestorOptions>>().Value;
if (string.IsNullOrWhiteSpace(options.Redis.Url))
{
return new InMemoryAttestorDedupeStore();
}
var multiplexer = sp.GetRequiredService<IConnectionMultiplexer>();
return new RedisAttestorDedupeStore(multiplexer, sp.GetRequiredService<IOptions<AttestorOptions>>());
});
services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var options = sp.GetRequiredService<IOptions<AttestorOptions>>().Value;
if (string.IsNullOrWhiteSpace(options.Redis.Url))
{
throw new InvalidOperationException("Redis connection string is required when redis dedupe is enabled.");
}
return ConnectionMultiplexer.Connect(options.Redis.Url);
});
services.AddSingleton<IAttestorArchiveStore>(sp =>
{
var options = sp.GetRequiredService<IOptions<AttestorOptions>>().Value;
if (options.S3.Enabled && !string.IsNullOrWhiteSpace(options.S3.Endpoint) && !string.IsNullOrWhiteSpace(options.S3.Bucket))
{
var config = new AmazonS3Config
{
ServiceURL = options.S3.Endpoint,
ForcePathStyle = true,
UseHttp = !options.S3.UseTls
};
var client = new AmazonS3Client(FallbackCredentialsFactory.GetCredentials(), config);
return new S3AttestorArchiveStore(client, sp.GetRequiredService<IOptions<AttestorOptions>>(), sp.GetRequiredService<ILogger<S3AttestorArchiveStore>>());
}
return new NullAttestorArchiveStore(sp.GetRequiredService<ILogger<NullAttestorArchiveStore>>());
});
return services;
}
}
services.AddSingleton<IAttestorEntryRepository, InMemoryAttestorEntryRepository>();
services.AddSingleton<IAttestorAuditSink, InMemoryAttestorAuditSink>();
services.AddSingleton<IAttestorDedupeStore>(sp =>
{
var options = sp.GetRequiredService<IOptions<AttestorOptions>>().Value;
if (string.IsNullOrWhiteSpace(options.Redis.Url))
{
return new InMemoryAttestorDedupeStore();
}
var multiplexer = sp.GetRequiredService<IConnectionMultiplexer>();
return new RedisAttestorDedupeStore(multiplexer, sp.GetRequiredService<IOptions<AttestorOptions>>());
});
services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var options = sp.GetRequiredService<IOptions<AttestorOptions>>().Value;
if (string.IsNullOrWhiteSpace(options.Redis.Url))
{
throw new InvalidOperationException("Redis connection string is required when redis dedupe is enabled.");
}
return ConnectionMultiplexer.Connect(options.Redis.Url);
});
services.AddSingleton<IAttestorArchiveStore>(sp =>
{
var options = sp.GetRequiredService<IOptions<AttestorOptions>>().Value;
if (options.S3.Enabled && !string.IsNullOrWhiteSpace(options.S3.Endpoint) && !string.IsNullOrWhiteSpace(options.S3.Bucket))
{
var config = new AmazonS3Config
{
ServiceURL = options.S3.Endpoint,
ForcePathStyle = true,
UseHttp = !options.S3.UseTls
};
var client = new AmazonS3Client(FallbackCredentialsFactory.GetCredentials(), config);
return new S3AttestorArchiveStore(client, sp.GetRequiredService<IOptions<AttestorOptions>>(), sp.GetRequiredService<ILogger<S3AttestorArchiveStore>>());
}
return new NullAttestorArchiveStore(sp.GetRequiredService<ILogger<NullAttestorArchiveStore>>());
});
services.AddSingleton<IBulkVerificationJobStore, InMemoryBulkVerificationJobStore>();
return services;
}
}

View File

@@ -22,7 +22,6 @@
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageReference Include="MongoDB.Driver" Version="3.5.0" />
<PackageReference Include="StackExchange.Redis" Version="2.8.24" />
<PackageReference Include="AWSSDK.S3" Version="4.0.2" />
</ItemGroup>

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Attestor.Core.Audit;
using StellaOps.Attestor.Core.Storage;
namespace StellaOps.Attestor.Infrastructure.Storage;
internal sealed class InMemoryAttestorAuditSink : IAttestorAuditSink
{
public List<AttestorAuditRecord> Records { get; } = new();
public Task WriteAsync(AttestorAuditRecord record, CancellationToken cancellationToken = default)
{
Records.Add(record);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,170 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Attestor.Core.Storage;
namespace StellaOps.Attestor.Infrastructure.Storage;
internal sealed class InMemoryAttestorEntryRepository : IAttestorEntryRepository
{
private readonly ConcurrentDictionary<string, AttestorEntry> _entries = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, string> _bundleIndex = new(StringComparer.OrdinalIgnoreCase);
private readonly object _sync = new();
public Task<AttestorEntry?> GetByBundleShaAsync(string bundleSha256, CancellationToken cancellationToken = default)
{
string? uuid;
lock (_sync)
{
_bundleIndex.TryGetValue(bundleSha256, out uuid);
}
if (uuid is not null && _entries.TryGetValue(uuid, out var entry))
{
return Task.FromResult<AttestorEntry?>(entry);
}
return Task.FromResult<AttestorEntry?>(null);
}
public Task<AttestorEntry?> GetByUuidAsync(string rekorUuid, CancellationToken cancellationToken = default)
{
_entries.TryGetValue(rekorUuid, out var entry);
return Task.FromResult(entry);
}
public Task<IReadOnlyList<AttestorEntry>> GetByArtifactShaAsync(string artifactSha256, CancellationToken cancellationToken = default)
{
List<AttestorEntry> snapshot;
lock (_sync)
{
snapshot = _entries.Values.ToList();
}
var entries = snapshot
.Where(e => string.Equals(e.Artifact.Sha256, artifactSha256, StringComparison.OrdinalIgnoreCase))
.OrderBy(e => e.CreatedAt)
.ToList();
return Task.FromResult<IReadOnlyList<AttestorEntry>>(entries);
}
public Task SaveAsync(AttestorEntry entry, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(entry);
lock (_sync)
{
if (_bundleIndex.TryGetValue(entry.BundleSha256, out var existingUuid) &&
!string.Equals(existingUuid, entry.RekorUuid, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Bundle SHA '{entry.BundleSha256}' already exists.");
}
if (_entries.TryGetValue(entry.RekorUuid, out var existing) &&
!string.Equals(existing.BundleSha256, entry.BundleSha256, StringComparison.OrdinalIgnoreCase))
{
_bundleIndex.Remove(existing.BundleSha256);
}
_entries[entry.RekorUuid] = entry;
_bundleIndex[entry.BundleSha256] = entry.RekorUuid;
}
return Task.CompletedTask;
}
public Task<AttestorEntryQueryResult> QueryAsync(AttestorEntryQuery query, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(query);
var pageSize = query.PageSize <= 0 ? 50 : Math.Min(query.PageSize, 200);
List<AttestorEntry> snapshot;
lock (_sync)
{
snapshot = _entries.Values.ToList();
}
IEnumerable<AttestorEntry> sequence = snapshot;
if (!string.IsNullOrWhiteSpace(query.Subject))
{
var subject = query.Subject;
sequence = sequence.Where(e =>
string.Equals(e.Artifact.Sha256, subject, StringComparison.OrdinalIgnoreCase) ||
string.Equals(e.Artifact.ImageDigest, subject, StringComparison.OrdinalIgnoreCase) ||
string.Equals(e.Artifact.SubjectUri, subject, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(query.Type))
{
sequence = sequence.Where(e => string.Equals(e.Artifact.Kind, query.Type, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(query.Issuer))
{
sequence = sequence.Where(e => string.Equals(e.SignerIdentity.SubjectAlternativeName, query.Issuer, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(query.Scope))
{
sequence = sequence.Where(e => string.Equals(e.SignerIdentity.Issuer, query.Scope, StringComparison.OrdinalIgnoreCase));
}
if (query.CreatedAfter is { } createdAfter)
{
sequence = sequence.Where(e => e.CreatedAt >= createdAfter);
}
if (query.CreatedBefore is { } createdBefore)
{
sequence = sequence.Where(e => e.CreatedAt <= createdBefore);
}
if (!string.IsNullOrWhiteSpace(query.ContinuationToken))
{
var continuation = AttestorEntryContinuationToken.Parse(query.ContinuationToken);
sequence = sequence.Where(e =>
{
var createdAt = e.CreatedAt;
if (createdAt < continuation.CreatedAt)
{
return true;
}
if (createdAt > continuation.CreatedAt)
{
return false;
}
return string.CompareOrdinal(e.RekorUuid, continuation.RekorUuid) >= 0;
});
}
var ordered = sequence
.OrderByDescending(e => e.CreatedAt)
.ThenBy(e => e.RekorUuid, StringComparer.Ordinal);
var page = ordered.Take(pageSize + 1).ToList();
AttestorEntry? next = null;
if (page.Count > pageSize)
{
next = page[^1];
page.RemoveAt(page.Count - 1);
}
var result = new AttestorEntryQueryResult
{
Items = page,
ContinuationToken = next is null
? null
: AttestorEntryContinuationToken.Encode(next.CreatedAt, next.RekorUuid)
};
return Task.FromResult(result);
}
}

View File

@@ -1,131 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using StellaOps.Attestor.Core.Audit;
using StellaOps.Attestor.Core.Storage;
namespace StellaOps.Attestor.Infrastructure.Storage;
internal sealed class MongoAttestorAuditSink : IAttestorAuditSink
{
private readonly IMongoCollection<AttestorAuditDocument> _collection;
private static int _indexesInitialized;
public MongoAttestorAuditSink(IMongoCollection<AttestorAuditDocument> collection)
{
_collection = collection;
EnsureIndexes();
}
public Task WriteAsync(AttestorAuditRecord record, CancellationToken cancellationToken = default)
{
var document = AttestorAuditDocument.FromRecord(record);
return _collection.InsertOneAsync(document, cancellationToken: cancellationToken);
}
private void EnsureIndexes()
{
if (Interlocked.Exchange(ref _indexesInitialized, 1) == 1)
{
return;
}
var index = new CreateIndexModel<AttestorAuditDocument>(
Builders<AttestorAuditDocument>.IndexKeys.Descending(x => x.Timestamp),
new CreateIndexOptions { Name = "ts_desc" });
_collection.Indexes.CreateOne(index);
}
internal sealed class AttestorAuditDocument
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("ts")]
public BsonDateTime Timestamp { get; set; } = BsonDateTime.Create(DateTime.UtcNow);
[BsonElement("action")]
public string Action { get; set; } = string.Empty;
[BsonElement("result")]
public string Result { get; set; } = string.Empty;
[BsonElement("rekorUuid")]
public string? RekorUuid { get; set; }
[BsonElement("index")]
public long? Index { get; set; }
[BsonElement("artifactSha256")]
public string ArtifactSha256 { get; set; } = string.Empty;
[BsonElement("bundleSha256")]
public string BundleSha256 { get; set; } = string.Empty;
[BsonElement("backend")]
public string Backend { get; set; } = string.Empty;
[BsonElement("latencyMs")]
public long LatencyMs { get; set; }
[BsonElement("caller")]
public CallerDocument Caller { get; set; } = new();
[BsonElement("metadata")]
public BsonDocument Metadata { get; set; } = new();
public static AttestorAuditDocument FromRecord(AttestorAuditRecord record)
{
var metadata = new BsonDocument();
foreach (var kvp in record.Metadata)
{
metadata[kvp.Key] = kvp.Value;
}
return new AttestorAuditDocument
{
Id = ObjectId.GenerateNewId(),
Timestamp = BsonDateTime.Create(record.Timestamp.UtcDateTime),
Action = record.Action,
Result = record.Result,
RekorUuid = record.RekorUuid,
Index = record.Index,
ArtifactSha256 = record.ArtifactSha256,
BundleSha256 = record.BundleSha256,
Backend = record.Backend,
LatencyMs = record.LatencyMs,
Caller = new CallerDocument
{
Subject = record.Caller.Subject,
Audience = record.Caller.Audience,
ClientId = record.Caller.ClientId,
MtlsThumbprint = record.Caller.MtlsThumbprint,
Tenant = record.Caller.Tenant
},
Metadata = metadata
};
}
internal sealed class CallerDocument
{
[BsonElement("subject")]
public string? Subject { get; set; }
[BsonElement("audience")]
public string? Audience { get; set; }
[BsonElement("clientId")]
public string? ClientId { get; set; }
[BsonElement("mtlsThumbprint")]
public string? MtlsThumbprint { get; set; }
[BsonElement("tenant")]
public string? Tenant { get; set; }
}
}
}

View File

@@ -1,111 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using StellaOps.Attestor.Core.Storage;
namespace StellaOps.Attestor.Infrastructure.Storage;
internal sealed class MongoAttestorDedupeStore : IAttestorDedupeStore
{
private readonly IMongoCollection<AttestorDedupeDocument> _collection;
private readonly TimeProvider _timeProvider;
private static int _indexesInitialized;
public MongoAttestorDedupeStore(
IMongoCollection<AttestorDedupeDocument> collection,
TimeProvider timeProvider)
{
_collection = collection;
_timeProvider = timeProvider;
EnsureIndexes();
}
public async Task<string?> TryGetExistingAsync(string bundleSha256, CancellationToken cancellationToken = default)
{
var key = BuildKey(bundleSha256);
var now = _timeProvider.GetUtcNow().UtcDateTime;
var filter = Builders<AttestorDedupeDocument>.Filter.Eq(x => x.Key, key);
var document = await _collection
.Find(filter)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (document is null)
{
return null;
}
if (document.TtlAt <= now)
{
await _collection.DeleteOneAsync(filter, cancellationToken).ConfigureAwait(false);
return null;
}
return document.RekorUuid;
}
public Task SetAsync(string bundleSha256, string rekorUuid, TimeSpan ttl, CancellationToken cancellationToken = default)
{
var now = _timeProvider.GetUtcNow().UtcDateTime;
var expiresAt = now.Add(ttl);
var key = BuildKey(bundleSha256);
var filter = Builders<AttestorDedupeDocument>.Filter.Eq(x => x.Key, key);
var update = Builders<AttestorDedupeDocument>.Update
.SetOnInsert(x => x.Key, key)
.Set(x => x.RekorUuid, rekorUuid)
.Set(x => x.CreatedAt, now)
.Set(x => x.TtlAt, expiresAt);
return _collection.UpdateOneAsync(
filter,
update,
new UpdateOptions { IsUpsert = true },
cancellationToken);
}
private static string BuildKey(string bundleSha256) => $"bundle:{bundleSha256}";
private void EnsureIndexes()
{
if (Interlocked.Exchange(ref _indexesInitialized, 1) == 1)
{
return;
}
var indexes = new[]
{
new CreateIndexModel<AttestorDedupeDocument>(
Builders<AttestorDedupeDocument>.IndexKeys.Ascending(x => x.Key),
new CreateIndexOptions { Unique = true, Name = "dedupe_key_unique" }),
new CreateIndexModel<AttestorDedupeDocument>(
Builders<AttestorDedupeDocument>.IndexKeys.Ascending(x => x.TtlAt),
new CreateIndexOptions { ExpireAfter = TimeSpan.Zero, Name = "dedupe_ttl" })
};
_collection.Indexes.CreateMany(indexes);
}
[BsonIgnoreExtraElements]
internal sealed class AttestorDedupeDocument
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("key")]
public string Key { get; set; } = string.Empty;
[BsonElement("rekorUuid")]
public string RekorUuid { get; set; } = string.Empty;
[BsonElement("createdAt")]
public DateTime CreatedAt { get; set; }
[BsonElement("ttlAt")]
public DateTime TtlAt { get; set; }
}
}

View File

@@ -1,609 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using StellaOps.Attestor.Core.Storage;
namespace StellaOps.Attestor.Infrastructure.Storage;
internal sealed class MongoAttestorEntryRepository : IAttestorEntryRepository
{
private const int DefaultPageSize = 50;
private const int MaxPageSize = 200;
private readonly IMongoCollection<AttestorEntryDocument> _entries;
public MongoAttestorEntryRepository(IMongoCollection<AttestorEntryDocument> entries)
{
_entries = entries ?? throw new ArgumentNullException(nameof(entries));
EnsureIndexes();
}
public async Task<AttestorEntry?> GetByBundleShaAsync(string bundleSha256, CancellationToken cancellationToken = default)
{
var filter = Builders<AttestorEntryDocument>.Filter.Eq(x => x.BundleSha256, bundleSha256);
var document = await _entries.Find(filter).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
return document?.ToDomain();
}
public async Task<AttestorEntry?> GetByUuidAsync(string rekorUuid, CancellationToken cancellationToken = default)
{
var filter = Builders<AttestorEntryDocument>.Filter.Eq(x => x.Id, rekorUuid);
var document = await _entries.Find(filter).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
return document?.ToDomain();
}
public async Task<IReadOnlyList<AttestorEntry>> GetByArtifactShaAsync(string artifactSha256, CancellationToken cancellationToken = default)
{
var filter = Builders<AttestorEntryDocument>.Filter.Eq(x => x.Artifact.Sha256, artifactSha256);
var documents = await _entries.Find(filter)
.Sort(Builders<AttestorEntryDocument>.Sort.Descending(x => x.CreatedAt))
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return documents.ConvertAll(static doc => doc.ToDomain());
}
public async Task SaveAsync(AttestorEntry entry, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(entry);
var document = AttestorEntryDocument.FromDomain(entry);
var filter = Builders<AttestorEntryDocument>.Filter.Eq(x => x.Id, document.Id);
await _entries.ReplaceOneAsync(filter, document, new ReplaceOptions { IsUpsert = true }, cancellationToken).ConfigureAwait(false);
}
public async Task<AttestorEntryQueryResult> QueryAsync(AttestorEntryQuery query, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(query);
var pageSize = query.PageSize <= 0 ? DefaultPageSize : Math.Min(query.PageSize, MaxPageSize);
var filterBuilder = Builders<AttestorEntryDocument>.Filter;
var filter = filterBuilder.Empty;
if (!string.IsNullOrWhiteSpace(query.Subject))
{
var subject = query.Subject;
var subjectFilter = filterBuilder.Or(
filterBuilder.Eq(x => x.Artifact.Sha256, subject),
filterBuilder.Eq(x => x.Artifact.ImageDigest, subject),
filterBuilder.Eq(x => x.Artifact.SubjectUri, subject));
filter &= subjectFilter;
}
if (!string.IsNullOrWhiteSpace(query.Type))
{
filter &= filterBuilder.Eq(x => x.Artifact.Kind, query.Type);
}
if (!string.IsNullOrWhiteSpace(query.Issuer))
{
filter &= filterBuilder.Eq(x => x.SignerIdentity.SubjectAlternativeName, query.Issuer);
}
if (!string.IsNullOrWhiteSpace(query.Scope))
{
filter &= filterBuilder.Eq(x => x.SignerIdentity.Issuer, query.Scope);
}
if (query.CreatedAfter is { } createdAfter)
{
filter &= filterBuilder.Gte(x => x.CreatedAt, createdAfter.UtcDateTime);
}
if (query.CreatedBefore is { } createdBefore)
{
filter &= filterBuilder.Lte(x => x.CreatedAt, createdBefore.UtcDateTime);
}
if (!string.IsNullOrWhiteSpace(query.ContinuationToken))
{
if (!AttestorEntryContinuationToken.TryParse(query.ContinuationToken, out var cursor))
{
throw new FormatException("Invalid continuation token.");
}
var cursorInstant = cursor.CreatedAt.UtcDateTime;
var continuationFilter = filterBuilder.Or(
filterBuilder.Lt(x => x.CreatedAt, cursorInstant),
filterBuilder.And(
filterBuilder.Eq(x => x.CreatedAt, cursorInstant),
filterBuilder.Gt(x => x.Id, cursor.RekorUuid)));
filter &= continuationFilter;
}
var sort = Builders<AttestorEntryDocument>.Sort
.Descending(x => x.CreatedAt)
.Ascending(x => x.Id);
var documents = await _entries.Find(filter)
.Sort(sort)
.Limit(pageSize + 1)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
string? continuation = null;
if (documents.Count > pageSize)
{
var cursorDocument = documents[pageSize];
var nextCreatedAt = DateTime.SpecifyKind(cursorDocument.CreatedAt, DateTimeKind.Utc);
continuation = AttestorEntryContinuationToken.Encode(new DateTimeOffset(nextCreatedAt), cursorDocument.Id);
documents.RemoveRange(pageSize, documents.Count - pageSize);
}
var items = documents.ConvertAll(static doc => doc.ToDomain());
return new AttestorEntryQueryResult
{
Items = items,
ContinuationToken = continuation
};
}
private void EnsureIndexes()
{
var keys = Builders<AttestorEntryDocument>.IndexKeys;
var models = new[]
{
new CreateIndexModel<AttestorEntryDocument>(
keys.Ascending(x => x.BundleSha256),
new CreateIndexOptions { Name = "bundle_sha_unique", Unique = true }),
new CreateIndexModel<AttestorEntryDocument>(
keys.Descending(x => x.CreatedAt).Ascending(x => x.Id),
new CreateIndexOptions { Name = "created_at_uuid" }),
new CreateIndexModel<AttestorEntryDocument>(
keys.Ascending(x => x.Artifact.Sha256),
new CreateIndexOptions { Name = "artifact_sha" }),
new CreateIndexModel<AttestorEntryDocument>(
keys.Ascending(x => x.Artifact.ImageDigest),
new CreateIndexOptions { Name = "artifact_image_digest" }),
new CreateIndexModel<AttestorEntryDocument>(
keys.Ascending(x => x.Artifact.SubjectUri),
new CreateIndexOptions { Name = "artifact_subject_uri" }),
new CreateIndexModel<AttestorEntryDocument>(
keys.Ascending(x => x.SignerIdentity.Issuer)
.Ascending(x => x.Artifact.Kind)
.Descending(x => x.CreatedAt)
.Ascending(x => x.Id),
new CreateIndexOptions { Name = "scope_kind_created_at" }),
new CreateIndexModel<AttestorEntryDocument>(
keys.Ascending(x => x.SignerIdentity.SubjectAlternativeName),
new CreateIndexOptions { Name = "issuer_san" })
};
_entries.Indexes.CreateMany(models);
}
[BsonIgnoreExtraElements]
internal sealed class AttestorEntryDocument
{
[BsonId]
public string Id { get; set; } = string.Empty;
[BsonElement("artifact")]
public ArtifactDocument Artifact { get; set; } = new();
[BsonElement("bundleSha256")]
public string BundleSha256 { get; set; } = string.Empty;
[BsonElement("index")]
public long? Index { get; set; }
[BsonElement("proof")]
public ProofDocument? Proof { get; set; }
[BsonElement("witness")]
public WitnessDocument? Witness { get; set; }
[BsonElement("log")]
public LogDocument Log { get; set; } = new();
[BsonElement("createdAt")]
[BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
public DateTime CreatedAt { get; set; }
[BsonElement("status")]
public string Status { get; set; } = "pending";
[BsonElement("signer")]
public SignerIdentityDocument SignerIdentity { get; set; } = new();
[BsonElement("mirror")]
public MirrorDocument? Mirror { get; set; }
public static AttestorEntryDocument FromDomain(AttestorEntry entry)
{
ArgumentNullException.ThrowIfNull(entry);
return new AttestorEntryDocument
{
Id = entry.RekorUuid,
Artifact = ArtifactDocument.FromDomain(entry.Artifact),
BundleSha256 = entry.BundleSha256,
Index = entry.Index,
Proof = ProofDocument.FromDomain(entry.Proof),
Witness = WitnessDocument.FromDomain(entry.Witness),
Log = LogDocument.FromDomain(entry.Log),
CreatedAt = entry.CreatedAt.UtcDateTime,
Status = entry.Status,
SignerIdentity = SignerIdentityDocument.FromDomain(entry.SignerIdentity),
Mirror = MirrorDocument.FromDomain(entry.Mirror)
};
}
public AttestorEntry ToDomain()
{
var createdAtUtc = DateTime.SpecifyKind(CreatedAt, DateTimeKind.Utc);
return new AttestorEntry
{
RekorUuid = Id,
Artifact = Artifact.ToDomain(),
BundleSha256 = BundleSha256,
Index = Index,
Proof = Proof?.ToDomain(),
Witness = Witness?.ToDomain(),
Log = Log.ToDomain(),
CreatedAt = new DateTimeOffset(createdAtUtc),
Status = Status,
SignerIdentity = SignerIdentity.ToDomain(),
Mirror = Mirror?.ToDomain()
};
}
}
internal sealed class ArtifactDocument
{
[BsonElement("sha256")]
public string Sha256 { get; set; } = string.Empty;
[BsonElement("kind")]
public string Kind { get; set; } = string.Empty;
[BsonElement("imageDigest")]
public string? ImageDigest { get; set; }
[BsonElement("subjectUri")]
public string? SubjectUri { get; set; }
public static ArtifactDocument FromDomain(AttestorEntry.ArtifactDescriptor artifact)
{
ArgumentNullException.ThrowIfNull(artifact);
return new ArtifactDocument
{
Sha256 = artifact.Sha256,
Kind = artifact.Kind,
ImageDigest = artifact.ImageDigest,
SubjectUri = artifact.SubjectUri
};
}
public AttestorEntry.ArtifactDescriptor ToDomain()
{
return new AttestorEntry.ArtifactDescriptor
{
Sha256 = Sha256,
Kind = Kind,
ImageDigest = ImageDigest,
SubjectUri = SubjectUri
};
}
}
internal sealed class ProofDocument
{
[BsonElement("checkpoint")]
public CheckpointDocument? Checkpoint { get; set; }
[BsonElement("inclusion")]
public InclusionDocument? Inclusion { get; set; }
public static ProofDocument? FromDomain(AttestorEntry.ProofDescriptor? proof)
{
if (proof is null)
{
return null;
}
return new ProofDocument
{
Checkpoint = CheckpointDocument.FromDomain(proof.Checkpoint),
Inclusion = InclusionDocument.FromDomain(proof.Inclusion)
};
}
public AttestorEntry.ProofDescriptor ToDomain()
{
return new AttestorEntry.ProofDescriptor
{
Checkpoint = Checkpoint?.ToDomain(),
Inclusion = Inclusion?.ToDomain()
};
}
}
internal sealed class WitnessDocument
{
[BsonElement("aggregator")]
public string? Aggregator { get; set; }
[BsonElement("status")]
public string Status { get; set; } = "unknown";
[BsonElement("rootHash")]
public string? RootHash { get; set; }
[BsonElement("retrievedAt")]
[BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
public DateTime RetrievedAt { get; set; }
[BsonElement("statement")]
public string? Statement { get; set; }
[BsonElement("signature")]
public string? Signature { get; set; }
[BsonElement("keyId")]
public string? KeyId { get; set; }
[BsonElement("error")]
public string? Error { get; set; }
public static WitnessDocument? FromDomain(AttestorEntry.WitnessDescriptor? witness)
{
if (witness is null)
{
return null;
}
return new WitnessDocument
{
Aggregator = witness.Aggregator,
Status = witness.Status,
RootHash = witness.RootHash,
RetrievedAt = witness.RetrievedAt.UtcDateTime,
Statement = witness.Statement,
Signature = witness.Signature,
KeyId = witness.KeyId,
Error = witness.Error
};
}
public AttestorEntry.WitnessDescriptor ToDomain()
{
return new AttestorEntry.WitnessDescriptor
{
Aggregator = Aggregator ?? string.Empty,
Status = string.IsNullOrWhiteSpace(Status) ? "unknown" : Status,
RootHash = RootHash,
RetrievedAt = new DateTimeOffset(DateTime.SpecifyKind(RetrievedAt, DateTimeKind.Utc)),
Statement = Statement,
Signature = Signature,
KeyId = KeyId,
Error = Error
};
}
}
internal sealed class CheckpointDocument
{
[BsonElement("origin")]
public string? Origin { get; set; }
[BsonElement("size")]
public long Size { get; set; }
[BsonElement("rootHash")]
public string? RootHash { get; set; }
[BsonElement("timestamp")]
[BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
public DateTime? Timestamp { get; set; }
public static CheckpointDocument? FromDomain(AttestorEntry.CheckpointDescriptor? checkpoint)
{
if (checkpoint is null)
{
return null;
}
return new CheckpointDocument
{
Origin = checkpoint.Origin,
Size = checkpoint.Size,
RootHash = checkpoint.RootHash,
Timestamp = checkpoint.Timestamp?.UtcDateTime
};
}
public AttestorEntry.CheckpointDescriptor ToDomain()
{
return new AttestorEntry.CheckpointDescriptor
{
Origin = Origin,
Size = Size,
RootHash = RootHash,
Timestamp = Timestamp is null ? null : new DateTimeOffset(DateTime.SpecifyKind(Timestamp.Value, DateTimeKind.Utc))
};
}
}
internal sealed class InclusionDocument
{
[BsonElement("leafHash")]
public string? LeafHash { get; set; }
[BsonElement("path")]
public IReadOnlyList<string> Path { get; set; } = Array.Empty<string>();
public static InclusionDocument? FromDomain(AttestorEntry.InclusionDescriptor? inclusion)
{
if (inclusion is null)
{
return null;
}
return new InclusionDocument
{
LeafHash = inclusion.LeafHash,
Path = inclusion.Path
};
}
public AttestorEntry.InclusionDescriptor ToDomain()
{
return new AttestorEntry.InclusionDescriptor
{
LeafHash = LeafHash,
Path = Path
};
}
}
internal sealed class LogDocument
{
[BsonElement("backend")]
public string Backend { get; set; } = "primary";
[BsonElement("url")]
public string Url { get; set; } = string.Empty;
[BsonElement("logId")]
public string? LogId { get; set; }
public static LogDocument FromDomain(AttestorEntry.LogDescriptor log)
{
ArgumentNullException.ThrowIfNull(log);
return new LogDocument
{
Backend = log.Backend,
Url = log.Url,
LogId = log.LogId
};
}
public AttestorEntry.LogDescriptor ToDomain()
{
return new AttestorEntry.LogDescriptor
{
Backend = Backend,
Url = Url,
LogId = LogId
};
}
}
internal sealed class SignerIdentityDocument
{
[BsonElement("mode")]
public string Mode { get; set; } = string.Empty;
[BsonElement("issuer")]
public string? Issuer { get; set; }
[BsonElement("san")]
public string? SubjectAlternativeName { get; set; }
[BsonElement("kid")]
public string? KeyId { get; set; }
public static SignerIdentityDocument FromDomain(AttestorEntry.SignerIdentityDescriptor signer)
{
ArgumentNullException.ThrowIfNull(signer);
return new SignerIdentityDocument
{
Mode = signer.Mode,
Issuer = signer.Issuer,
SubjectAlternativeName = signer.SubjectAlternativeName,
KeyId = signer.KeyId
};
}
public AttestorEntry.SignerIdentityDescriptor ToDomain()
{
return new AttestorEntry.SignerIdentityDescriptor
{
Mode = Mode,
Issuer = Issuer,
SubjectAlternativeName = SubjectAlternativeName,
KeyId = KeyId
};
}
}
internal sealed class MirrorDocument
{
[BsonElement("backend")]
public string Backend { get; set; } = string.Empty;
[BsonElement("url")]
public string Url { get; set; } = string.Empty;
[BsonElement("uuid")]
public string? Uuid { get; set; }
[BsonElement("index")]
public long? Index { get; set; }
[BsonElement("status")]
public string Status { get; set; } = "pending";
[BsonElement("proof")]
public ProofDocument? Proof { get; set; }
[BsonElement("witness")]
public WitnessDocument? Witness { get; set; }
[BsonElement("logId")]
public string? LogId { get; set; }
[BsonElement("error")]
public string? Error { get; set; }
public static MirrorDocument? FromDomain(AttestorEntry.LogReplicaDescriptor? mirror)
{
if (mirror is null)
{
return null;
}
return new MirrorDocument
{
Backend = mirror.Backend,
Url = mirror.Url,
Uuid = mirror.Uuid,
Index = mirror.Index,
Status = mirror.Status,
Proof = ProofDocument.FromDomain(mirror.Proof),
Witness = WitnessDocument.FromDomain(mirror.Witness),
LogId = mirror.LogId,
Error = mirror.Error
};
}
public AttestorEntry.LogReplicaDescriptor ToDomain()
{
return new AttestorEntry.LogReplicaDescriptor
{
Backend = Backend,
Url = Url,
Uuid = Uuid,
Index = Index,
Status = Status,
Proof = Proof?.ToDomain(),
Witness = Witness?.ToDomain(),
LogId = LogId,
Error = Error
};
}
}
}

View File

@@ -22,7 +22,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.TestHost;
using MongoDB.Driver;
using StackExchange.Redis;
using StellaOps.Attestor.Core.Offline;
using StellaOps.Attestor.Core.Storage;

View File

@@ -1,9 +1,8 @@
#if false
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using StackExchange.Redis;
using StellaOps.Attestor.Core.Options;
using StellaOps.Attestor.Infrastructure.Storage;
@@ -15,54 +14,6 @@ public sealed class LiveDedupeStoreTests
{
private const string Category = "LiveTTL";
[Fact]
[Trait("Category", Category)]
public async Task Mongo_dedupe_document_expires_via_ttl_index()
{
var mongoUri = Environment.GetEnvironmentVariable("ATTESTOR_LIVE_MONGO_URI");
if (string.IsNullOrWhiteSpace(mongoUri))
{
return;
}
var mongoUrl = new MongoUrl(mongoUri);
var client = new MongoClient(mongoUrl);
var databaseName = $"{(string.IsNullOrWhiteSpace(mongoUrl.DatabaseName) ? "attestor_live_ttl" : mongoUrl.DatabaseName)}_{Guid.NewGuid():N}";
var database = client.GetDatabase(databaseName);
var collection = database.GetCollection<MongoAttestorDedupeStore.AttestorDedupeDocument>("dedupe");
try
{
var store = new MongoAttestorDedupeStore(collection, TimeProvider.System);
var indexes = await (await collection.Indexes.ListAsync()).ToListAsync();
Assert.Contains(indexes, doc => doc.TryGetElement("name", out var element) && element.Value == "dedupe_ttl");
var bundle = Guid.NewGuid().ToString("N");
var ttl = TimeSpan.FromSeconds(20);
await store.SetAsync(bundle, "rekor-live", ttl);
var filter = Builders<MongoAttestorDedupeStore.AttestorDedupeDocument>.Filter.Eq(x => x.Key, $"bundle:{bundle}");
Assert.True(await collection.Find(filter).AnyAsync(), "Seed document was not written.");
var deadline = DateTime.UtcNow + ttl + TimeSpan.FromMinutes(2);
while (DateTime.UtcNow < deadline)
{
if (!await collection.Find(filter).AnyAsync())
{
return;
}
await Task.Delay(TimeSpan.FromSeconds(5));
}
throw new TimeoutException("TTL document remained in MongoDB after waiting for expiry.");
}
finally
{
await client.DropDatabaseAsync(databaseName);
}
}
[Fact]
[Trait("Category", Category)]
public async Task Redis_dedupe_entry_sets_time_to_live()
@@ -106,5 +57,5 @@ public sealed class LiveDedupeStoreTests
await multiplexer.DisposeAsync();
}
}
}
#endif

View File

@@ -9,7 +9,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageReference Include="MongoDB.Driver" Version="3.5.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
@@ -28,4 +27,4 @@
<ProjectReference Include="../../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />
<ProjectReference Include="../../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />
</ItemGroup>
</Project>
</Project>