Close admin trust audit gaps and stabilize live sweeps
This commit is contained in:
@@ -223,11 +223,29 @@ public static class PackAdapterEndpoints
|
||||
.WithSummary("Pack v2 administration A5 policy governance projection.")
|
||||
.RequireAuthorization(PlatformPolicies.SetupRead);
|
||||
|
||||
administration.MapGet("/trust-signing", (
|
||||
administration.MapGet("/trust-signing", async (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver) =>
|
||||
PlatformRequestContextResolver resolver,
|
||||
IAdministrationTrustSigningStore trustSigningStore,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
return BuildAdministrationItem(context, resolver, BuildAdministrationTrustSigning);
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var payload = await BuildAdministrationTrustSigningAsync(
|
||||
requestContext!.TenantId,
|
||||
trustSigningStore,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new PlatformItemResponse<AdministrationTrustSigningDto>(
|
||||
requestContext.TenantId,
|
||||
requestContext.ActorId,
|
||||
SnapshotAt,
|
||||
Cached: false,
|
||||
CacheTtlSeconds: 0,
|
||||
payload));
|
||||
})
|
||||
.WithName("GetAdministrationTrustSigning")
|
||||
.WithSummary("Pack v2 administration A6 trust and signing projection.")
|
||||
@@ -540,14 +558,41 @@ public static class PackAdapterEndpoints
|
||||
]);
|
||||
}
|
||||
|
||||
private static AdministrationTrustSigningDto BuildAdministrationTrustSigning()
|
||||
private static async Task<AdministrationTrustSigningDto> BuildAdministrationTrustSigningAsync(
|
||||
string tenantId,
|
||||
IAdministrationTrustSigningStore trustSigningStore,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var keys = await trustSigningStore.ListKeysAsync(tenantId, 500, 0, cancellationToken).ConfigureAwait(false);
|
||||
var issuers = await trustSigningStore.ListIssuersAsync(tenantId, 500, 0, cancellationToken).ConfigureAwait(false);
|
||||
var certificates = await trustSigningStore.ListCertificatesAsync(tenantId, 500, 0, cancellationToken).ConfigureAwait(false);
|
||||
var transparencyConfig = await trustSigningStore.GetTransparencyLogConfigAsync(tenantId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var expiringCertificateCount = certificates.Count(certificate =>
|
||||
!string.Equals(certificate.Status, "revoked", StringComparison.OrdinalIgnoreCase)
|
||||
&& certificate.NotAfter <= SnapshotAt.AddDays(10));
|
||||
|
||||
var signals = new[]
|
||||
{
|
||||
new AdministrationTrustSignalDto("audit-log", "healthy", "Audit log ingestion is current."),
|
||||
new AdministrationTrustSignalDto("certificate-expiry", "warning", "1 certificate expires within 10 days."),
|
||||
new AdministrationTrustSignalDto("transparency-log", "healthy", "Rekor witness is reachable."),
|
||||
new AdministrationTrustSignalDto("trust-scoring", "healthy", "Issuer trust score recalculation completed."),
|
||||
new AdministrationTrustSignalDto("audit-log", "healthy", "Trust-signing configuration changes are being recorded."),
|
||||
new AdministrationTrustSignalDto(
|
||||
"certificate-expiry",
|
||||
expiringCertificateCount > 0 ? "warning" : "healthy",
|
||||
expiringCertificateCount > 0
|
||||
? $"{expiringCertificateCount} certificate{(expiringCertificateCount == 1 ? string.Empty : "s")} expire within 10 days."
|
||||
: "No certificate expirations are due in the next 10 days."),
|
||||
new AdministrationTrustSignalDto(
|
||||
"transparency-log",
|
||||
transparencyConfig is null ? "warning" : "healthy",
|
||||
transparencyConfig is null
|
||||
? "Transparency log is not configured for this tenant."
|
||||
: $"Transparency log witness points to {transparencyConfig.LogUrl}."),
|
||||
new AdministrationTrustSignalDto(
|
||||
"trust-scoring",
|
||||
issuers.Count == 0 ? "warning" : "healthy",
|
||||
issuers.Count == 0
|
||||
? "No trusted issuers are registered yet."
|
||||
: "Issuer trust inventory is available for scoring and review."),
|
||||
}.OrderBy(signal => signal.SignalId, StringComparer.Ordinal).ToList();
|
||||
|
||||
var aliases = BuildAdministrationAliases(
|
||||
@@ -558,7 +603,7 @@ public static class PackAdapterEndpoints
|
||||
]);
|
||||
|
||||
return new AdministrationTrustSigningDto(
|
||||
Inventory: new AdministrationTrustInventoryDto(Keys: 14, Issuers: 7, Certificates: 23),
|
||||
Inventory: new AdministrationTrustInventoryDto(Keys: keys.Count, Issuers: issuers.Count, Certificates: certificates.Count),
|
||||
Signals: signals,
|
||||
LegacyAliases: aliases,
|
||||
EvidenceConsumerPath: "/evidence-audit/proofs");
|
||||
|
||||
@@ -32,7 +32,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
var normalizedLimit = NormalizeLimit(limit);
|
||||
var normalizedOffset = NormalizeOffset(offset);
|
||||
|
||||
@@ -83,7 +83,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
|
||||
var actor = NormalizeActor(actorId);
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var keyId = Guid.NewGuid();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
|
||||
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
@@ -164,7 +164,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
|
||||
|
||||
var actor = NormalizeActor(actorId);
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
|
||||
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
|
||||
var existingStatus = await GetKeyStatusAsync(connection, tenantGuid, keyId, cancellationToken).ConfigureAwait(false);
|
||||
@@ -226,7 +226,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
|
||||
_ = NormalizeRequired(request.Reason, "reason_required");
|
||||
var actor = NormalizeActor(actorId);
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
|
||||
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
|
||||
await using var command = new NpgsqlCommand(
|
||||
@@ -270,7 +270,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
var normalizedLimit = NormalizeLimit(limit);
|
||||
var normalizedOffset = NormalizeOffset(offset);
|
||||
|
||||
@@ -323,7 +323,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
|
||||
var actor = NormalizeActor(actorId);
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var issuerId = Guid.NewGuid();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
|
||||
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
@@ -397,7 +397,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
var normalizedLimit = NormalizeLimit(limit);
|
||||
var normalizedOffset = NormalizeOffset(offset);
|
||||
|
||||
@@ -454,7 +454,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
|
||||
var actor = NormalizeActor(actorId);
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var certificateId = Guid.NewGuid();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
|
||||
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -571,7 +571,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
|
||||
_ = NormalizeRequired(request.Reason, "reason_required");
|
||||
var actor = NormalizeActor(actorId);
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
|
||||
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
|
||||
await using var command = new NpgsqlCommand(
|
||||
@@ -615,7 +615,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
|
||||
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
|
||||
await using var command = new NpgsqlCommand(
|
||||
@@ -660,7 +660,7 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
|
||||
|
||||
var actor = NormalizeActor(actorId);
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
|
||||
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
|
||||
await using var command = new NpgsqlCommand(
|
||||
@@ -762,21 +762,6 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
|
||||
return connection;
|
||||
}
|
||||
|
||||
private static Guid ParseTenantId(string tenantId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
throw new InvalidOperationException("tenant_required");
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(tenantId, out var tenantGuid))
|
||||
{
|
||||
throw new InvalidOperationException("tenant_id_invalid");
|
||||
}
|
||||
|
||||
return tenantGuid;
|
||||
}
|
||||
|
||||
private static int NormalizeLimit(int limit) => limit < 1 ? 1 : limit;
|
||||
|
||||
private static int NormalizeOffset(int offset) => offset < 0 ? 0 : offset;
|
||||
|
||||
@@ -70,7 +70,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
|
||||
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
|
||||
await using var command = new NpgsqlCommand(
|
||||
@@ -137,7 +137,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
|
||||
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
|
||||
await using var command = new NpgsqlCommand(
|
||||
@@ -220,7 +220,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
|
||||
throw new InvalidOperationException("bundle_name_required");
|
||||
}
|
||||
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var bundleId = Guid.NewGuid();
|
||||
var createdBy = NormalizeActor(actorId);
|
||||
@@ -273,7 +273,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
|
||||
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -325,7 +325,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
|
||||
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -395,7 +395,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
|
||||
throw new InvalidOperationException("request_required");
|
||||
}
|
||||
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
var createdBy = NormalizeActor(actorId);
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var normalizedComponents = ReleaseControlBundleDigest.NormalizeComponents(request.Components);
|
||||
@@ -544,7 +544,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
|
||||
throw new InvalidOperationException("request_required");
|
||||
}
|
||||
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
var requestedBy = NormalizeActor(actorId);
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var normalizedIdempotencyKey = NormalizeIdempotencyKey(request.IdempotencyKey);
|
||||
@@ -656,7 +656,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await using var command = new NpgsqlCommand(ListMaterializationRunsSql, connection);
|
||||
@@ -682,7 +682,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await using var command = new NpgsqlCommand(ListMaterializationRunsByBundleSql, connection);
|
||||
@@ -707,7 +707,7 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var tenantGuid = ParseTenantId(tenantId);
|
||||
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
|
||||
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await using var command = new NpgsqlCommand(
|
||||
@@ -772,24 +772,6 @@ public sealed class PostgresReleaseControlBundleStore : IReleaseControlBundleSto
|
||||
UpdatedAt: reader.GetFieldValue<DateTimeOffset>(9));
|
||||
}
|
||||
|
||||
private static Guid ParseTenantId(string tenantId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
throw new InvalidOperationException("tenant_required");
|
||||
}
|
||||
|
||||
if (Guid.TryParse(tenantId, out var tenantGuid))
|
||||
{
|
||||
return tenantGuid;
|
||||
}
|
||||
|
||||
// Derive deterministic GUID from string tenant identifier (e.g. "default", "demo-prod")
|
||||
var hash = System.Security.Cryptography.SHA256.HashData(
|
||||
System.Text.Encoding.UTF8.GetBytes(tenantId));
|
||||
return new Guid(hash.AsSpan(0, 16));
|
||||
}
|
||||
|
||||
private async Task<NpgsqlConnection> OpenTenantConnectionAsync(Guid tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var connection = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Services;
|
||||
|
||||
internal static class TenantStorageKey
|
||||
{
|
||||
public static Guid ParseTenantGuid(string tenantId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
throw new InvalidOperationException("tenant_required");
|
||||
}
|
||||
|
||||
var normalizedTenantId = tenantId.Trim();
|
||||
if (Guid.TryParse(normalizedTenantId, out var tenantGuid))
|
||||
{
|
||||
return tenantGuid;
|
||||
}
|
||||
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(normalizedTenantId));
|
||||
return new Guid(hash.AsSpan(0, 16));
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,10 @@
|
||||
<EmbeddedResource Include="Translations\*.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="StellaOps.Platform.WebService.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Label="StellaOpsReleaseVersion">
|
||||
<Version>1.0.0-alpha1</Version>
|
||||
<InformationalVersion>1.0.0-alpha1</InformationalVersion>
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
-- Scratch-install trust-signing seed for canonical demo tenants.
|
||||
-- Tenant GUIDs are derived with the same deterministic SHA-256 -> Guid mapping used in Platform stores.
|
||||
|
||||
INSERT INTO release.trust_keys (
|
||||
id,
|
||||
tenant_id,
|
||||
key_alias,
|
||||
algorithm,
|
||||
status,
|
||||
current_version,
|
||||
metadata_json,
|
||||
created_at,
|
||||
updated_at,
|
||||
created_by,
|
||||
updated_by
|
||||
)
|
||||
SELECT
|
||||
'a9adf36f-2f4d-4f31-9b0b-138e3f6f1f61'::uuid,
|
||||
'3a5e72b6-ae6a-f8a4-2b6a-df2960d63016'::uuid,
|
||||
'demo-prod-core-signing',
|
||||
'ed25519',
|
||||
'active',
|
||||
3,
|
||||
'{"owner":"secops","region":"us-east"}'::jsonb,
|
||||
'2026-03-01T00:00:00Z'::timestamptz,
|
||||
'2026-03-09T00:00:00Z'::timestamptz,
|
||||
'system',
|
||||
'system'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM release.trust_keys
|
||||
WHERE tenant_id = '3a5e72b6-ae6a-f8a4-2b6a-df2960d63016'::uuid
|
||||
AND lower(key_alias) = lower('demo-prod-core-signing')
|
||||
);
|
||||
|
||||
INSERT INTO release.trust_issuers (
|
||||
id,
|
||||
tenant_id,
|
||||
issuer_name,
|
||||
issuer_uri,
|
||||
trust_level,
|
||||
status,
|
||||
created_at,
|
||||
updated_at,
|
||||
created_by,
|
||||
updated_by
|
||||
)
|
||||
SELECT
|
||||
'4ac7e1d4-7a2e-4b4d-9e12-5d42e3168a91'::uuid,
|
||||
'3a5e72b6-ae6a-f8a4-2b6a-df2960d63016'::uuid,
|
||||
'Demo Prod Root CA',
|
||||
'https://issuer.demo-prod.stella-ops.local/root',
|
||||
'high',
|
||||
'active',
|
||||
'2026-03-01T00:00:00Z'::timestamptz,
|
||||
'2026-03-09T00:00:00Z'::timestamptz,
|
||||
'system',
|
||||
'system'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM release.trust_issuers
|
||||
WHERE tenant_id = '3a5e72b6-ae6a-f8a4-2b6a-df2960d63016'::uuid
|
||||
AND lower(issuer_uri) = lower('https://issuer.demo-prod.stella-ops.local/root')
|
||||
);
|
||||
|
||||
INSERT INTO release.trust_certificates (
|
||||
id,
|
||||
tenant_id,
|
||||
key_id,
|
||||
issuer_id,
|
||||
serial_number,
|
||||
status,
|
||||
not_before,
|
||||
not_after,
|
||||
created_at,
|
||||
updated_at,
|
||||
created_by,
|
||||
updated_by
|
||||
)
|
||||
SELECT
|
||||
'8d1f0b75-3d56-4d8f-a40b-52f5e52e7fb1'::uuid,
|
||||
'3a5e72b6-ae6a-f8a4-2b6a-df2960d63016'::uuid,
|
||||
'a9adf36f-2f4d-4f31-9b0b-138e3f6f1f61'::uuid,
|
||||
'4ac7e1d4-7a2e-4b4d-9e12-5d42e3168a91'::uuid,
|
||||
'DEMO-PROD-SER-0001',
|
||||
'active',
|
||||
'2026-03-01T00:00:00Z'::timestamptz,
|
||||
'2026-03-18T00:00:00Z'::timestamptz,
|
||||
'2026-03-01T00:00:00Z'::timestamptz,
|
||||
'2026-03-09T00:00:00Z'::timestamptz,
|
||||
'system',
|
||||
'system'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM release.trust_certificates
|
||||
WHERE tenant_id = '3a5e72b6-ae6a-f8a4-2b6a-df2960d63016'::uuid
|
||||
AND lower(serial_number) = lower('DEMO-PROD-SER-0001')
|
||||
);
|
||||
|
||||
INSERT INTO release.trust_transparency_configs (
|
||||
tenant_id,
|
||||
log_url,
|
||||
witness_url,
|
||||
enforce_inclusion,
|
||||
updated_at,
|
||||
updated_by
|
||||
)
|
||||
VALUES (
|
||||
'3a5e72b6-ae6a-f8a4-2b6a-df2960d63016'::uuid,
|
||||
'https://rekor.demo-prod.stella-ops.local',
|
||||
'https://rekor-witness.demo-prod.stella-ops.local',
|
||||
true,
|
||||
'2026-03-09T00:00:00Z'::timestamptz,
|
||||
'system'
|
||||
)
|
||||
ON CONFLICT (tenant_id) DO UPDATE
|
||||
SET
|
||||
log_url = EXCLUDED.log_url,
|
||||
witness_url = EXCLUDED.witness_url,
|
||||
enforce_inclusion = EXCLUDED.enforce_inclusion,
|
||||
updated_at = EXCLUDED.updated_at,
|
||||
updated_by = EXCLUDED.updated_by;
|
||||
|
||||
INSERT INTO release.trust_keys (
|
||||
id,
|
||||
tenant_id,
|
||||
key_alias,
|
||||
algorithm,
|
||||
status,
|
||||
current_version,
|
||||
metadata_json,
|
||||
created_at,
|
||||
updated_at,
|
||||
created_by,
|
||||
updated_by
|
||||
)
|
||||
SELECT
|
||||
'5fdf7b2d-9d32-4b69-874f-9b22a4d22f21'::uuid,
|
||||
'c1eea837-19ce-7d68-132f-e29051dca629'::uuid,
|
||||
'default-core-signing',
|
||||
'ed25519',
|
||||
'active',
|
||||
1,
|
||||
'{"owner":"bootstrap","region":"global"}'::jsonb,
|
||||
'2026-03-01T00:00:00Z'::timestamptz,
|
||||
'2026-03-01T00:00:00Z'::timestamptz,
|
||||
'system',
|
||||
'system'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM release.trust_keys
|
||||
WHERE tenant_id = 'c1eea837-19ce-7d68-132f-e29051dca629'::uuid
|
||||
AND lower(key_alias) = lower('default-core-signing')
|
||||
);
|
||||
|
||||
INSERT INTO release.trust_issuers (
|
||||
id,
|
||||
tenant_id,
|
||||
issuer_name,
|
||||
issuer_uri,
|
||||
trust_level,
|
||||
status,
|
||||
created_at,
|
||||
updated_at,
|
||||
created_by,
|
||||
updated_by
|
||||
)
|
||||
SELECT
|
||||
'f7d0505e-4a94-4688-a046-87f7a9c7cf76'::uuid,
|
||||
'c1eea837-19ce-7d68-132f-e29051dca629'::uuid,
|
||||
'Default Root CA',
|
||||
'https://issuer.default.stella-ops.local/root',
|
||||
'high',
|
||||
'active',
|
||||
'2026-03-01T00:00:00Z'::timestamptz,
|
||||
'2026-03-01T00:00:00Z'::timestamptz,
|
||||
'system',
|
||||
'system'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM release.trust_issuers
|
||||
WHERE tenant_id = 'c1eea837-19ce-7d68-132f-e29051dca629'::uuid
|
||||
AND lower(issuer_uri) = lower('https://issuer.default.stella-ops.local/root')
|
||||
);
|
||||
|
||||
INSERT INTO release.trust_certificates (
|
||||
id,
|
||||
tenant_id,
|
||||
key_id,
|
||||
issuer_id,
|
||||
serial_number,
|
||||
status,
|
||||
not_before,
|
||||
not_after,
|
||||
created_at,
|
||||
updated_at,
|
||||
created_by,
|
||||
updated_by
|
||||
)
|
||||
SELECT
|
||||
'c0d9c7db-a0c8-41e9-a7f4-e8f03e4b31e3'::uuid,
|
||||
'c1eea837-19ce-7d68-132f-e29051dca629'::uuid,
|
||||
'5fdf7b2d-9d32-4b69-874f-9b22a4d22f21'::uuid,
|
||||
'f7d0505e-4a94-4688-a046-87f7a9c7cf76'::uuid,
|
||||
'DEFAULT-SER-0001',
|
||||
'active',
|
||||
'2026-03-01T00:00:00Z'::timestamptz,
|
||||
'2026-09-01T00:00:00Z'::timestamptz,
|
||||
'2026-03-01T00:00:00Z'::timestamptz,
|
||||
'2026-03-01T00:00:00Z'::timestamptz,
|
||||
'system',
|
||||
'system'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM release.trust_certificates
|
||||
WHERE tenant_id = 'c1eea837-19ce-7d68-132f-e29051dca629'::uuid
|
||||
AND lower(serial_number) = lower('DEFAULT-SER-0001')
|
||||
);
|
||||
|
||||
INSERT INTO release.trust_transparency_configs (
|
||||
tenant_id,
|
||||
log_url,
|
||||
witness_url,
|
||||
enforce_inclusion,
|
||||
updated_at,
|
||||
updated_by
|
||||
)
|
||||
VALUES (
|
||||
'c1eea837-19ce-7d68-132f-e29051dca629'::uuid,
|
||||
'https://rekor.default.stella-ops.local',
|
||||
'https://rekor-witness.default.stella-ops.local',
|
||||
true,
|
||||
'2026-03-01T00:00:00Z'::timestamptz,
|
||||
'system'
|
||||
)
|
||||
ON CONFLICT (tenant_id) DO UPDATE
|
||||
SET
|
||||
log_url = EXCLUDED.log_url,
|
||||
witness_url = EXCLUDED.witness_url,
|
||||
enforce_inclusion = EXCLUDED.enforce_inclusion,
|
||||
updated_at = EXCLUDED.updated_at,
|
||||
updated_by = EXCLUDED.updated_by;
|
||||
@@ -1,8 +1,10 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Platform.WebService.Constants;
|
||||
using StellaOps.TestKit;
|
||||
@@ -146,6 +148,75 @@ public sealed class PackAdapterEndpointsTests : IClassFixture<PlatformWebApplica
|
||||
Assert.DoesNotContain(PlatformPolicies.SetupRead, policies);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TrustSigningOverview_Uses_live_inventory_counts_for_selected_tenant()
|
||||
{
|
||||
using var client = CreateTenantClient("demo-prod");
|
||||
|
||||
var keyResponse = await client.PostAsJsonAsync(
|
||||
"/api/v1/administration/trust-signing/keys",
|
||||
new CreateAdministrationTrustKeyRequest(
|
||||
Alias: "tenant-live-key",
|
||||
Algorithm: "ed25519",
|
||||
MetadataJson: "{\"owner\":\"secops\"}"),
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.Created, keyResponse.StatusCode);
|
||||
var key = await keyResponse.Content.ReadFromJsonAsync<AdministrationTrustKeySummary>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(key);
|
||||
|
||||
var issuerResponse = await client.PostAsJsonAsync(
|
||||
"/api/v1/administration/trust-signing/issuers",
|
||||
new RegisterAdministrationTrustIssuerRequest(
|
||||
Name: "Tenant Live Root CA",
|
||||
IssuerUri: "https://issuer.demo-prod.stella-ops.local/live",
|
||||
TrustLevel: "high"),
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.Created, issuerResponse.StatusCode);
|
||||
var issuer = await issuerResponse.Content.ReadFromJsonAsync<AdministrationTrustIssuerSummary>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(issuer);
|
||||
|
||||
var certificateResponse = await client.PostAsJsonAsync(
|
||||
"/api/v1/administration/trust-signing/certificates",
|
||||
new RegisterAdministrationTrustCertificateRequest(
|
||||
KeyId: key!.KeyId,
|
||||
IssuerId: issuer!.IssuerId,
|
||||
SerialNumber: "TENANT-LIVE-SER-0001",
|
||||
NotBefore: DateTimeOffset.Parse("2026-02-01T00:00:00Z"),
|
||||
NotAfter: DateTimeOffset.Parse("2026-02-25T00:00:00Z")),
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.Created, certificateResponse.StatusCode);
|
||||
|
||||
var configureResponse = await client.PutAsJsonAsync(
|
||||
"/api/v1/administration/trust-signing/transparency-log",
|
||||
new ConfigureAdministrationTransparencyLogRequest(
|
||||
LogUrl: "https://rekor.demo-prod.stella-ops.local",
|
||||
WitnessUrl: "https://rekor-witness.demo-prod.stella-ops.local",
|
||||
EnforceInclusion: true),
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, configureResponse.StatusCode);
|
||||
|
||||
var overviewResponse = await client.GetAsync("/api/v1/administration/trust-signing", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, overviewResponse.StatusCode);
|
||||
|
||||
using var document = JsonDocument.Parse(await overviewResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
|
||||
var item = document.RootElement.GetProperty("item");
|
||||
Assert.Equal(1, item.GetProperty("inventory").GetProperty("keys").GetInt32());
|
||||
Assert.Equal(1, item.GetProperty("inventory").GetProperty("issuers").GetInt32());
|
||||
Assert.Equal(1, item.GetProperty("inventory").GetProperty("certificates").GetInt32());
|
||||
|
||||
var signals = item
|
||||
.GetProperty("signals")
|
||||
.EnumerateArray()
|
||||
.ToDictionary(
|
||||
signal => signal.GetProperty("signalId").GetString()!,
|
||||
signal => signal.GetProperty("status").GetString()!,
|
||||
StringComparer.Ordinal);
|
||||
|
||||
Assert.Equal("warning", signals["certificate-expiry"]);
|
||||
Assert.Equal("healthy", signals["transparency-log"]);
|
||||
}
|
||||
|
||||
private HttpClient CreateTenantClient(string tenantId)
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
using StellaOps.TestKit;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Tests;
|
||||
|
||||
public sealed class TenantStorageKeyTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("demo-prod", "3a5e72b6-ae6a-f8a4-2b6a-df2960d63016")]
|
||||
[InlineData("default", "c1eea837-19ce-7d68-132f-e29051dca629")]
|
||||
public void ParseTenantGuid_derives_deterministic_guid_for_slug_tenants(string tenantId, string expectedGuid)
|
||||
{
|
||||
TenantStorageKey.ParseTenantGuid(tenantId).Should().Be(Guid.Parse(expectedGuid));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseTenantGuid_returns_existing_guid_without_rehashing()
|
||||
{
|
||||
var tenantGuid = Guid.NewGuid();
|
||||
|
||||
TenantStorageKey.ParseTenantGuid(tenantGuid.ToString("D")).Should().Be(tenantGuid);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user