up
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user