Resolve Concelier/Excititor merge conflicts
This commit is contained in:
		@@ -0,0 +1,33 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using StellaOps.Attestor.Core.Storage;
 | 
			
		||||
 | 
			
		||||
namespace StellaOps.Attestor.Infrastructure.Storage;
 | 
			
		||||
 | 
			
		||||
internal sealed class InMemoryAttestorDedupeStore : IAttestorDedupeStore
 | 
			
		||||
{
 | 
			
		||||
    private readonly ConcurrentDictionary<string, (string Uuid, DateTimeOffset ExpiresAt)> _store = new();
 | 
			
		||||
 | 
			
		||||
    public Task<string?> TryGetExistingAsync(string bundleSha256, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        if (_store.TryGetValue(bundleSha256, out var entry))
 | 
			
		||||
        {
 | 
			
		||||
            if (entry.ExpiresAt > DateTimeOffset.UtcNow)
 | 
			
		||||
            {
 | 
			
		||||
                return Task.FromResult<string?>(entry.Uuid);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _store.TryRemove(bundleSha256, out _);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Task.FromResult<string?>(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task SetAsync(string bundleSha256, string rekorUuid, TimeSpan ttl, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        _store[bundleSha256] = (rekorUuid, DateTimeOffset.UtcNow.Add(ttl));
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,115 @@
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
    public MongoAttestorAuditSink(IMongoCollection<AttestorAuditDocument> collection)
 | 
			
		||||
    {
 | 
			
		||||
        _collection = collection;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task WriteAsync(AttestorAuditRecord record, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        var document = AttestorAuditDocument.FromRecord(record);
 | 
			
		||||
        return _collection.InsertOneAsync(document, cancellationToken: cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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; }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,342 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
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 MongoAttestorEntryRepository : IAttestorEntryRepository
 | 
			
		||||
{
 | 
			
		||||
    private readonly IMongoCollection<AttestorEntryDocument> _entries;
 | 
			
		||||
 | 
			
		||||
    public MongoAttestorEntryRepository(IMongoCollection<AttestorEntryDocument> entries)
 | 
			
		||||
    {
 | 
			
		||||
        _entries = entries;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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).ToListAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        return documents.ConvertAll(static doc => doc.ToDomain());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task SaveAsync(AttestorEntry entry, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [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("log")]
 | 
			
		||||
        public LogDocument Log { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
        [BsonElement("createdAt")]
 | 
			
		||||
        public BsonDateTime CreatedAt { get; set; } = BsonDateTime.Create(System.DateTimeOffset.UtcNow);
 | 
			
		||||
 | 
			
		||||
        [BsonElement("status")]
 | 
			
		||||
        public string Status { get; set; } = "pending";
 | 
			
		||||
 | 
			
		||||
        [BsonElement("signerIdentity")]
 | 
			
		||||
        public SignerIdentityDocument SignerIdentity { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
        [BsonElement("mirror")]
 | 
			
		||||
        public MirrorDocument? Mirror { get; set; }
 | 
			
		||||
 | 
			
		||||
        public static AttestorEntryDocument FromDomain(AttestorEntry entry)
 | 
			
		||||
        {
 | 
			
		||||
            return new AttestorEntryDocument
 | 
			
		||||
            {
 | 
			
		||||
                Id = entry.RekorUuid,
 | 
			
		||||
                Artifact = new ArtifactDocument
 | 
			
		||||
                {
 | 
			
		||||
                    Sha256 = entry.Artifact.Sha256,
 | 
			
		||||
                    Kind = entry.Artifact.Kind,
 | 
			
		||||
                    ImageDigest = entry.Artifact.ImageDigest,
 | 
			
		||||
                    SubjectUri = entry.Artifact.SubjectUri
 | 
			
		||||
                },
 | 
			
		||||
                BundleSha256 = entry.BundleSha256,
 | 
			
		||||
                Index = entry.Index,
 | 
			
		||||
                Proof = entry.Proof is null ? null : new ProofDocument
 | 
			
		||||
                {
 | 
			
		||||
                    Checkpoint = entry.Proof.Checkpoint is null ? null : new CheckpointDocument
 | 
			
		||||
                    {
 | 
			
		||||
                        Origin = entry.Proof.Checkpoint.Origin,
 | 
			
		||||
                        Size = entry.Proof.Checkpoint.Size,
 | 
			
		||||
                        RootHash = entry.Proof.Checkpoint.RootHash,
 | 
			
		||||
                        Timestamp = entry.Proof.Checkpoint.Timestamp is null
 | 
			
		||||
                            ? null
 | 
			
		||||
                            : BsonDateTime.Create(entry.Proof.Checkpoint.Timestamp.Value)
 | 
			
		||||
                    },
 | 
			
		||||
                    Inclusion = entry.Proof.Inclusion is null ? null : new InclusionDocument
 | 
			
		||||
                    {
 | 
			
		||||
                        LeafHash = entry.Proof.Inclusion.LeafHash,
 | 
			
		||||
                        Path = entry.Proof.Inclusion.Path
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                Log = new LogDocument
 | 
			
		||||
                {
 | 
			
		||||
                    Backend = entry.Log.Backend,
 | 
			
		||||
                    Url = entry.Log.Url,
 | 
			
		||||
                    LogId = entry.Log.LogId
 | 
			
		||||
                },
 | 
			
		||||
                CreatedAt = BsonDateTime.Create(entry.CreatedAt.UtcDateTime),
 | 
			
		||||
                Status = entry.Status,
 | 
			
		||||
                SignerIdentity = new SignerIdentityDocument
 | 
			
		||||
                {
 | 
			
		||||
                    Mode = entry.SignerIdentity.Mode,
 | 
			
		||||
                    Issuer = entry.SignerIdentity.Issuer,
 | 
			
		||||
                    SubjectAlternativeName = entry.SignerIdentity.SubjectAlternativeName,
 | 
			
		||||
                    KeyId = entry.SignerIdentity.KeyId
 | 
			
		||||
                },
 | 
			
		||||
                Mirror = entry.Mirror is null ? null : MirrorDocument.FromDomain(entry.Mirror)
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public AttestorEntry ToDomain()
 | 
			
		||||
        {
 | 
			
		||||
            return new AttestorEntry
 | 
			
		||||
            {
 | 
			
		||||
                RekorUuid = Id,
 | 
			
		||||
                Artifact = new AttestorEntry.ArtifactDescriptor
 | 
			
		||||
                {
 | 
			
		||||
                    Sha256 = Artifact.Sha256,
 | 
			
		||||
                    Kind = Artifact.Kind,
 | 
			
		||||
                    ImageDigest = Artifact.ImageDigest,
 | 
			
		||||
                    SubjectUri = Artifact.SubjectUri
 | 
			
		||||
                },
 | 
			
		||||
                BundleSha256 = BundleSha256,
 | 
			
		||||
                Index = Index,
 | 
			
		||||
                Proof = Proof is null ? null : new AttestorEntry.ProofDescriptor
 | 
			
		||||
                {
 | 
			
		||||
                    Checkpoint = Proof.Checkpoint is null ? null : new AttestorEntry.CheckpointDescriptor
 | 
			
		||||
                    {
 | 
			
		||||
                        Origin = Proof.Checkpoint.Origin,
 | 
			
		||||
                        Size = Proof.Checkpoint.Size,
 | 
			
		||||
                        RootHash = Proof.Checkpoint.RootHash,
 | 
			
		||||
                        Timestamp = Proof.Checkpoint.Timestamp?.ToUniversalTime()
 | 
			
		||||
                    },
 | 
			
		||||
                    Inclusion = Proof.Inclusion is null ? null : new AttestorEntry.InclusionDescriptor
 | 
			
		||||
                    {
 | 
			
		||||
                        LeafHash = Proof.Inclusion.LeafHash,
 | 
			
		||||
                        Path = Proof.Inclusion.Path
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                Log = new AttestorEntry.LogDescriptor
 | 
			
		||||
                {
 | 
			
		||||
                    Backend = Log.Backend,
 | 
			
		||||
                    Url = Log.Url,
 | 
			
		||||
                    LogId = Log.LogId
 | 
			
		||||
                },
 | 
			
		||||
                CreatedAt = CreatedAt.ToUniversalTime(),
 | 
			
		||||
                Status = Status,
 | 
			
		||||
                SignerIdentity = new AttestorEntry.SignerIdentityDescriptor
 | 
			
		||||
                {
 | 
			
		||||
                    Mode = SignerIdentity.Mode,
 | 
			
		||||
                    Issuer = SignerIdentity.Issuer,
 | 
			
		||||
                    SubjectAlternativeName = SignerIdentity.SubjectAlternativeName,
 | 
			
		||||
                    KeyId = SignerIdentity.KeyId
 | 
			
		||||
                },
 | 
			
		||||
                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; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        internal sealed class ProofDocument
 | 
			
		||||
        {
 | 
			
		||||
            [BsonElement("checkpoint")]
 | 
			
		||||
            public CheckpointDocument? Checkpoint { get; set; }
 | 
			
		||||
 | 
			
		||||
            [BsonElement("inclusion")]
 | 
			
		||||
            public InclusionDocument? Inclusion { get; set; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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")]
 | 
			
		||||
            public BsonDateTime? Timestamp { get; set; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        internal sealed class InclusionDocument
 | 
			
		||||
        {
 | 
			
		||||
            [BsonElement("leafHash")]
 | 
			
		||||
            public string? LeafHash { get; set; }
 | 
			
		||||
 | 
			
		||||
            [BsonElement("path")]
 | 
			
		||||
            public IReadOnlyList<string> Path { get; set; } = System.Array.Empty<string>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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("logId")]
 | 
			
		||||
            public string? LogId { get; set; }
 | 
			
		||||
 | 
			
		||||
            [BsonElement("error")]
 | 
			
		||||
            public string? Error { get; set; }
 | 
			
		||||
 | 
			
		||||
            public static MirrorDocument FromDomain(AttestorEntry.LogReplicaDescriptor mirror)
 | 
			
		||||
            {
 | 
			
		||||
                return new MirrorDocument
 | 
			
		||||
                {
 | 
			
		||||
                    Backend = mirror.Backend,
 | 
			
		||||
                    Url = mirror.Url,
 | 
			
		||||
                    Uuid = mirror.Uuid,
 | 
			
		||||
                    Index = mirror.Index,
 | 
			
		||||
                    Status = mirror.Status,
 | 
			
		||||
                    Proof = mirror.Proof is null ? null : new ProofDocument
 | 
			
		||||
                    {
 | 
			
		||||
                        Checkpoint = mirror.Proof.Checkpoint is null ? null : new CheckpointDocument
 | 
			
		||||
                        {
 | 
			
		||||
                            Origin = mirror.Proof.Checkpoint.Origin,
 | 
			
		||||
                            Size = mirror.Proof.Checkpoint.Size,
 | 
			
		||||
                            RootHash = mirror.Proof.Checkpoint.RootHash,
 | 
			
		||||
                            Timestamp = mirror.Proof.Checkpoint.Timestamp is null
 | 
			
		||||
                                ? null
 | 
			
		||||
                                : BsonDateTime.Create(mirror.Proof.Checkpoint.Timestamp.Value)
 | 
			
		||||
                        },
 | 
			
		||||
                        Inclusion = mirror.Proof.Inclusion is null ? null : new InclusionDocument
 | 
			
		||||
                        {
 | 
			
		||||
                            LeafHash = mirror.Proof.Inclusion.LeafHash,
 | 
			
		||||
                            Path = mirror.Proof.Inclusion.Path
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    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 is null ? null : new AttestorEntry.ProofDescriptor
 | 
			
		||||
                    {
 | 
			
		||||
                        Checkpoint = Proof.Checkpoint is null ? null : new AttestorEntry.CheckpointDescriptor
 | 
			
		||||
                        {
 | 
			
		||||
                            Origin = Proof.Checkpoint.Origin,
 | 
			
		||||
                            Size = Proof.Checkpoint.Size,
 | 
			
		||||
                            RootHash = Proof.Checkpoint.RootHash,
 | 
			
		||||
                            Timestamp = Proof.Checkpoint.Timestamp?.ToUniversalTime()
 | 
			
		||||
                        },
 | 
			
		||||
                        Inclusion = Proof.Inclusion is null ? null : new AttestorEntry.InclusionDescriptor
 | 
			
		||||
                        {
 | 
			
		||||
                            LeafHash = Proof.Inclusion.LeafHash,
 | 
			
		||||
                            Path = Proof.Inclusion.Path
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    LogId = LogId,
 | 
			
		||||
                    Error = Error
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
using StellaOps.Attestor.Core.Storage;
 | 
			
		||||
 | 
			
		||||
namespace StellaOps.Attestor.Infrastructure.Storage;
 | 
			
		||||
 | 
			
		||||
internal sealed class NullAttestorArchiveStore : IAttestorArchiveStore
 | 
			
		||||
{
 | 
			
		||||
    private readonly ILogger<NullAttestorArchiveStore> _logger;
 | 
			
		||||
 | 
			
		||||
    public NullAttestorArchiveStore(ILogger<NullAttestorArchiveStore> logger)
 | 
			
		||||
    {
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task ArchiveBundleAsync(AttestorArchiveBundle bundle, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        _logger.LogDebug("Archive disabled; skipping bundle {BundleSha}", bundle.BundleSha256);
 | 
			
		||||
        return Task.CompletedTask;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,34 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Microsoft.Extensions.Options;
 | 
			
		||||
using StackExchange.Redis;
 | 
			
		||||
using StellaOps.Attestor.Core.Options;
 | 
			
		||||
using StellaOps.Attestor.Core.Storage;
 | 
			
		||||
 | 
			
		||||
namespace StellaOps.Attestor.Infrastructure.Storage;
 | 
			
		||||
 | 
			
		||||
internal sealed class RedisAttestorDedupeStore : IAttestorDedupeStore
 | 
			
		||||
{
 | 
			
		||||
    private readonly IDatabase _database;
 | 
			
		||||
    private readonly string _prefix;
 | 
			
		||||
 | 
			
		||||
    public RedisAttestorDedupeStore(IConnectionMultiplexer multiplexer, IOptions<AttestorOptions> options)
 | 
			
		||||
    {
 | 
			
		||||
        _database = multiplexer.GetDatabase();
 | 
			
		||||
        _prefix = options.Value.Redis.DedupePrefix ?? "attestor:dedupe:";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<string?> TryGetExistingAsync(string bundleSha256, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        var value = await _database.StringGetAsync(BuildKey(bundleSha256)).ConfigureAwait(false);
 | 
			
		||||
        return value.HasValue ? value.ToString() : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task SetAsync(string bundleSha256, string rekorUuid, TimeSpan ttl, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        return _database.StringSetAsync(BuildKey(bundleSha256), rekorUuid, ttl);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private RedisKey BuildKey(string bundleSha256) => new RedisKey(_prefix + bundleSha256);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,72 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Amazon.S3;
 | 
			
		||||
using Amazon.S3.Model;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
using Microsoft.Extensions.Options;
 | 
			
		||||
using StellaOps.Attestor.Core.Options;
 | 
			
		||||
using StellaOps.Attestor.Core.Storage;
 | 
			
		||||
 | 
			
		||||
namespace StellaOps.Attestor.Infrastructure.Storage;
 | 
			
		||||
 | 
			
		||||
internal sealed class S3AttestorArchiveStore : IAttestorArchiveStore, IDisposable
 | 
			
		||||
{
 | 
			
		||||
    private readonly IAmazonS3 _s3;
 | 
			
		||||
    private readonly AttestorOptions.S3Options _options;
 | 
			
		||||
    private readonly ILogger<S3AttestorArchiveStore> _logger;
 | 
			
		||||
    private bool _disposed;
 | 
			
		||||
 | 
			
		||||
    public S3AttestorArchiveStore(IAmazonS3 s3, IOptions<AttestorOptions> options, ILogger<S3AttestorArchiveStore> logger)
 | 
			
		||||
    {
 | 
			
		||||
        _s3 = s3;
 | 
			
		||||
        _options = options.Value.S3;
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task ArchiveBundleAsync(AttestorArchiveBundle bundle, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(_options.Bucket))
 | 
			
		||||
        {
 | 
			
		||||
            _logger.LogWarning("S3 archive bucket is not configured; skipping archive for bundle {Bundle}", bundle.BundleSha256);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var prefix = _options.Prefix ?? "attest/";
 | 
			
		||||
 | 
			
		||||
        await PutObjectAsync(prefix + "dsse/" + bundle.BundleSha256 + ".json", bundle.CanonicalBundleJson, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        if (bundle.ProofJson.Length > 0)
 | 
			
		||||
        {
 | 
			
		||||
            await PutObjectAsync(prefix + "proof/" + bundle.RekorUuid + ".json", bundle.ProofJson, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var metadataObject = JsonSerializer.SerializeToUtf8Bytes(bundle.Metadata);
 | 
			
		||||
        await PutObjectAsync(prefix + "meta/" + bundle.RekorUuid + ".json", metadataObject, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task PutObjectAsync(string key, byte[] content, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        using var stream = new MemoryStream(content);
 | 
			
		||||
        var request = new PutObjectRequest
 | 
			
		||||
        {
 | 
			
		||||
            BucketName = _options.Bucket,
 | 
			
		||||
            Key = key,
 | 
			
		||||
            InputStream = stream,
 | 
			
		||||
            AutoCloseStream = false
 | 
			
		||||
        };
 | 
			
		||||
        return _s3.PutObjectAsync(request, cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        if (_disposed)
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _s3.Dispose();
 | 
			
		||||
        _disposed = true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user