up
Some checks failed
Some checks failed
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.AirGap.Controller.Options;
|
||||
using StellaOps.AirGap.Controller.Services;
|
||||
using StellaOps.AirGap.Controller.Stores;
|
||||
@@ -15,7 +13,6 @@ public static class AirGapControllerServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddAirGapController(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.Configure<AirGapControllerMongoOptions>(configuration.GetSection("AirGap:Mongo"));
|
||||
services.Configure<AirGapStartupOptions>(configuration.GetSection("AirGap:Startup"));
|
||||
|
||||
services.AddSingleton<AirGapTelemetry>();
|
||||
@@ -28,19 +25,9 @@ public static class AirGapControllerServiceCollectionExtensions
|
||||
|
||||
services.AddSingleton<IAirGapStateStore>(sp =>
|
||||
{
|
||||
var opts = sp.GetRequiredService<IOptions<AirGapControllerMongoOptions>>().Value;
|
||||
var logger = sp.GetRequiredService<ILogger<MongoAirGapStateStore>>();
|
||||
if (string.IsNullOrWhiteSpace(opts.ConnectionString))
|
||||
{
|
||||
logger.LogInformation("AirGap controller using in-memory state store (Mongo connection string not configured).");
|
||||
return new InMemoryAirGapStateStore();
|
||||
}
|
||||
|
||||
var mongoClient = new MongoClient(opts.ConnectionString);
|
||||
var database = mongoClient.GetDatabase(string.IsNullOrWhiteSpace(opts.Database) ? "stellaops_airgap" : opts.Database);
|
||||
var collection = MongoAirGapStateStore.EnsureCollection(database);
|
||||
logger.LogInformation("AirGap controller using Mongo state store (db={Database}, collection={Collection}).", opts.Database, opts.Collection);
|
||||
return new MongoAirGapStateStore(collection);
|
||||
var logger = sp.GetRequiredService<ILogger<InMemoryAirGapStateStore>>();
|
||||
logger.LogWarning("AirGap controller using in-memory state store; state resets on process restart.");
|
||||
return new InMemoryAirGapStateStore();
|
||||
});
|
||||
|
||||
services.AddHostedService<AirGapStartupDiagnosticsHostedService>();
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace StellaOps.AirGap.Controller.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Mongo configuration for the air-gap controller state store.
|
||||
/// </summary>
|
||||
public sealed class AirGapControllerMongoOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Mongo connection string; when missing, the controller falls back to the in-memory store.
|
||||
/// </summary>
|
||||
public string? ConnectionString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Database name. Default: "stellaops_airgap".
|
||||
/// </summary>
|
||||
public string Database { get; set; } = "stellaops_airgap";
|
||||
|
||||
/// <summary>
|
||||
/// Collection name for state documents. Default: "airgap_state".
|
||||
/// </summary>
|
||||
public string Collection { get; set; } = "airgap_state";
|
||||
}
|
||||
@@ -9,7 +9,4 @@
|
||||
<ProjectReference Include="../StellaOps.AirGap.Time/StellaOps.AirGap.Time.csproj" />
|
||||
<ProjectReference Include="../StellaOps.AirGap.Importer/StellaOps.AirGap.Importer.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.5.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
using System.Collections.Concurrent;
|
||||
using StellaOps.AirGap.Controller.Domain;
|
||||
|
||||
namespace StellaOps.AirGap.Controller.Stores;
|
||||
|
||||
public sealed class InMemoryAirGapStateStore : IAirGapStateStore
|
||||
{
|
||||
private readonly Dictionary<string, AirGapState> _states = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, AirGapState> _states = new(StringComparer.Ordinal);
|
||||
|
||||
public Task<AirGapState> GetAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (_states.TryGetValue(tenantId, out var state))
|
||||
{
|
||||
return Task.FromResult(state);
|
||||
return Task.FromResult(state with { });
|
||||
}
|
||||
|
||||
return Task.FromResult(new AirGapState { TenantId = tenantId });
|
||||
@@ -20,7 +21,7 @@ public sealed class InMemoryAirGapStateStore : IAirGapStateStore
|
||||
public Task SetAsync(AirGapState state, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_states[state.TenantId] = state;
|
||||
_states[state.TenantId] = state with { };
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.AirGap.Controller.Domain;
|
||||
using StellaOps.AirGap.Time.Models;
|
||||
|
||||
namespace StellaOps.AirGap.Controller.Stores;
|
||||
|
||||
/// <summary>
|
||||
/// Mongo-backed air-gap state store; single document per tenant.
|
||||
/// </summary>
|
||||
internal sealed class MongoAirGapStateStore : IAirGapStateStore
|
||||
{
|
||||
private readonly IMongoCollection<AirGapStateDocument> _collection;
|
||||
|
||||
public MongoAirGapStateStore(IMongoCollection<AirGapStateDocument> collection)
|
||||
{
|
||||
_collection = collection;
|
||||
}
|
||||
|
||||
public async Task<AirGapState> GetAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = Builders<AirGapStateDocument>.Filter.And(
|
||||
Builders<AirGapStateDocument>.Filter.Eq(x => x.TenantId, tenantId),
|
||||
Builders<AirGapStateDocument>.Filter.Eq(x => x.Id, AirGapState.SingletonId));
|
||||
|
||||
var doc = await _collection.Find(filter).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
return doc?.ToDomain() ?? new AirGapState { TenantId = tenantId };
|
||||
}
|
||||
|
||||
public async Task SetAsync(AirGapState state, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var doc = AirGapStateDocument.FromDomain(state);
|
||||
var filter = Builders<AirGapStateDocument>.Filter.And(
|
||||
Builders<AirGapStateDocument>.Filter.Eq(x => x.TenantId, state.TenantId),
|
||||
Builders<AirGapStateDocument>.Filter.Eq(x => x.Id, AirGapState.SingletonId));
|
||||
|
||||
var options = new ReplaceOptions { IsUpsert = true };
|
||||
await _collection.ReplaceOneAsync(filter, doc, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal static IMongoCollection<AirGapStateDocument> EnsureCollection(IMongoDatabase database)
|
||||
{
|
||||
var collectionName = "airgap_state";
|
||||
var exists = database.ListCollectionNames().ToList().Contains(collectionName);
|
||||
if (!exists)
|
||||
{
|
||||
database.CreateCollection(collectionName);
|
||||
}
|
||||
|
||||
var collection = database.GetCollection<AirGapStateDocument>(collectionName);
|
||||
|
||||
var keys = Builders<AirGapStateDocument>.IndexKeys
|
||||
.Ascending(x => x.TenantId)
|
||||
.Ascending(x => x.Id);
|
||||
var model = new CreateIndexModel<AirGapStateDocument>(keys, new CreateIndexOptions { Unique = true });
|
||||
collection.Indexes.CreateOne(model);
|
||||
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class AirGapStateDocument
|
||||
{
|
||||
[BsonId]
|
||||
public string Id { get; init; } = AirGapState.SingletonId;
|
||||
|
||||
[BsonElement("tenant_id")]
|
||||
public string TenantId { get; init; } = "default";
|
||||
|
||||
[BsonElement("sealed")]
|
||||
public bool Sealed { get; init; }
|
||||
= false;
|
||||
|
||||
[BsonElement("policy_hash")]
|
||||
public string? PolicyHash { get; init; }
|
||||
= null;
|
||||
|
||||
[BsonElement("time_anchor")]
|
||||
public AirGapTimeAnchorDocument TimeAnchor { get; init; } = new();
|
||||
|
||||
[BsonElement("staleness_budget")]
|
||||
public StalenessBudgetDocument StalenessBudget { get; init; } = new();
|
||||
|
||||
[BsonElement("last_transition_at")]
|
||||
public DateTimeOffset LastTransitionAt { get; init; }
|
||||
= DateTimeOffset.MinValue;
|
||||
|
||||
public AirGapState ToDomain() => new()
|
||||
{
|
||||
TenantId = TenantId,
|
||||
Sealed = Sealed,
|
||||
PolicyHash = PolicyHash,
|
||||
TimeAnchor = TimeAnchor.ToDomain(),
|
||||
StalenessBudget = StalenessBudget.ToDomain(),
|
||||
LastTransitionAt = LastTransitionAt
|
||||
};
|
||||
|
||||
public static AirGapStateDocument FromDomain(AirGapState state) => new()
|
||||
{
|
||||
TenantId = state.TenantId,
|
||||
Sealed = state.Sealed,
|
||||
PolicyHash = state.PolicyHash,
|
||||
TimeAnchor = AirGapTimeAnchorDocument.FromDomain(state.TimeAnchor),
|
||||
StalenessBudget = StalenessBudgetDocument.FromDomain(state.StalenessBudget),
|
||||
LastTransitionAt = state.LastTransitionAt
|
||||
};
|
||||
}
|
||||
|
||||
internal sealed class AirGapTimeAnchorDocument
|
||||
{
|
||||
[BsonElement("anchor_time")]
|
||||
public DateTimeOffset AnchorTime { get; init; }
|
||||
= DateTimeOffset.MinValue;
|
||||
|
||||
[BsonElement("source")]
|
||||
public string Source { get; init; } = "unknown";
|
||||
|
||||
[BsonElement("format")]
|
||||
public string Format { get; init; } = "unknown";
|
||||
|
||||
[BsonElement("signature_fp")]
|
||||
public string SignatureFingerprint { get; init; } = string.Empty;
|
||||
|
||||
[BsonElement("token_digest")]
|
||||
public string TokenDigest { get; init; } = string.Empty;
|
||||
|
||||
public StellaOps.AirGap.Time.Models.TimeAnchor ToDomain() =>
|
||||
new(AnchorTime, Source, Format, SignatureFingerprint, TokenDigest);
|
||||
|
||||
public static AirGapTimeAnchorDocument FromDomain(StellaOps.AirGap.Time.Models.TimeAnchor anchor) => new()
|
||||
{
|
||||
AnchorTime = anchor.AnchorTime,
|
||||
Source = anchor.Source,
|
||||
Format = anchor.Format,
|
||||
SignatureFingerprint = anchor.SignatureFingerprint,
|
||||
TokenDigest = anchor.TokenDigest
|
||||
};
|
||||
}
|
||||
|
||||
internal sealed class StalenessBudgetDocument
|
||||
{
|
||||
[BsonElement("warning_seconds")]
|
||||
public long WarningSeconds { get; init; } = StalenessBudget.Default.WarningSeconds;
|
||||
|
||||
[BsonElement("breach_seconds")]
|
||||
public long BreachSeconds { get; init; } = StalenessBudget.Default.BreachSeconds;
|
||||
|
||||
public StalenessBudget ToDomain() => new(WarningSeconds, BreachSeconds);
|
||||
|
||||
public static StalenessBudgetDocument FromDomain(StalenessBudget budget) => new()
|
||||
{
|
||||
WarningSeconds = budget.WarningSeconds,
|
||||
BreachSeconds = budget.BreachSeconds
|
||||
};
|
||||
}
|
||||
@@ -15,3 +15,6 @@
|
||||
| AIRGAP-IMP-56-002 | DONE | Root rotation policy (dual approval) + trust store; integrated into import validator; tests passing. | 2025-11-20 |
|
||||
| AIRGAP-IMP-57-001 | DONE | In-memory RLS bundle catalog/items repos + schema doc; deterministic ordering and tests passing. | 2025-11-20 |
|
||||
| AIRGAP-TIME-57-001 | DONE | Staleness calc, loader/fixtures, TimeStatusService/store, sealed validator, Ed25519 Roughtime + RFC3161 SignedCms verification, APIs + config sample delivered; awaiting final trust roots. | 2025-11-20 |
|
||||
| MR-T10.6.1 | DONE | Removed Mongo-backed air-gap state store; controller now uses in-memory store only. | 2025-12-11 |
|
||||
| MR-T10.6.2 | DONE | DI simplified to register in-memory air-gap state store (no Mongo options or client). | 2025-12-11 |
|
||||
| MR-T10.6.3 | DONE | Converted controller tests to in-memory store; dropped Mongo2Go dependency. | 2025-12-11 |
|
||||
|
||||
Reference in New Issue
Block a user