Repair first-time identity and trust operator journeys

This commit is contained in:
master
2026-03-15 12:33:56 +02:00
parent 7bdfcd5055
commit 08390f0ca4
27 changed files with 5814 additions and 2425 deletions

View File

@@ -57,6 +57,14 @@ public sealed record RegisterAdministrationTrustIssuerRequest(
string IssuerUri,
string TrustLevel);
public sealed record BlockAdministrationTrustIssuerRequest(
string Reason,
string? Ticket);
public sealed record UnblockAdministrationTrustIssuerRequest(
string? TrustLevel,
string? Ticket);
public sealed record RegisterAdministrationTrustCertificateRequest(
Guid? KeyId,
Guid? IssuerId,

View File

@@ -226,6 +226,72 @@ public static class AdministrationTrustSigningMutationEndpoints
.WithSummary("Register trust issuer")
.RequireAuthorization(PlatformPolicies.TrustWrite);
group.MapPost("/issuers/{issuerId:guid}/block", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
IAdministrationTrustSigningStore store,
Guid issuerId,
BlockAdministrationTrustIssuerRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
try
{
var updated = await store.BlockIssuerAsync(
requestContext!.TenantId,
requestContext.ActorId,
issuerId,
request,
cancellationToken).ConfigureAwait(false);
return Results.Ok(updated);
}
catch (InvalidOperationException ex)
{
return MapStoreError(ex, keyId: null, certificateId: null);
}
})
.WithName("BlockAdministrationTrustIssuer")
.WithSummary("Block trust issuer")
.RequireAuthorization(PlatformPolicies.TrustAdmin);
group.MapPost("/issuers/{issuerId:guid}/unblock", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
IAdministrationTrustSigningStore store,
Guid issuerId,
UnblockAdministrationTrustIssuerRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
try
{
var updated = await store.UnblockIssuerAsync(
requestContext!.TenantId,
requestContext.ActorId,
issuerId,
request,
cancellationToken).ConfigureAwait(false);
return Results.Ok(updated);
}
catch (InvalidOperationException ex)
{
return MapStoreError(ex, keyId: null, certificateId: null);
}
})
.WithName("UnblockAdministrationTrustIssuer")
.WithSummary("Unblock trust issuer")
.RequireAuthorization(PlatformPolicies.TrustAdmin);
group.MapGet("/certificates", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,

View File

@@ -42,6 +42,20 @@ public interface IAdministrationTrustSigningStore
RegisterAdministrationTrustIssuerRequest request,
CancellationToken cancellationToken = default);
Task<AdministrationTrustIssuerSummary> BlockIssuerAsync(
string tenantId,
string actorId,
Guid issuerId,
BlockAdministrationTrustIssuerRequest request,
CancellationToken cancellationToken = default);
Task<AdministrationTrustIssuerSummary> UnblockIssuerAsync(
string tenantId,
string actorId,
Guid issuerId,
UnblockAdministrationTrustIssuerRequest request,
CancellationToken cancellationToken = default);
Task<IReadOnlyList<AdministrationTrustCertificateSummary>> ListCertificatesAsync(
string tenantId,
int limit,

View File

@@ -214,6 +214,66 @@ public sealed class InMemoryAdministrationTrustSigningStore : IAdministrationTru
}
}
public Task<AdministrationTrustIssuerSummary> BlockIssuerAsync(
string tenantId,
string actorId,
Guid issuerId,
BlockAdministrationTrustIssuerRequest request,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (request is null) throw new InvalidOperationException("request_required");
_ = NormalizeRequired(request.Reason, "reason_required");
var actor = NormalizeActor(actorId);
var now = _timeProvider.GetUtcNow();
var state = GetState(tenantId);
lock (state.Sync)
{
if (!state.Issuers.TryGetValue(issuerId, out var issuer))
{
throw new InvalidOperationException("issuer_not_found");
}
issuer.TrustLevel = "blocked";
issuer.Status = "blocked";
issuer.UpdatedAt = now;
issuer.UpdatedBy = actor;
return Task.FromResult(ToSummary(issuer));
}
}
public Task<AdministrationTrustIssuerSummary> UnblockIssuerAsync(
string tenantId,
string actorId,
Guid issuerId,
UnblockAdministrationTrustIssuerRequest request,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (request is null) throw new InvalidOperationException("request_required");
var trustLevel = NormalizeOptional(request.TrustLevel) ?? "minimal";
var actor = NormalizeActor(actorId);
var now = _timeProvider.GetUtcNow();
var state = GetState(tenantId);
lock (state.Sync)
{
if (!state.Issuers.TryGetValue(issuerId, out var issuer))
{
throw new InvalidOperationException("issuer_not_found");
}
issuer.TrustLevel = trustLevel;
issuer.Status = "active";
issuer.UpdatedAt = now;
issuer.UpdatedBy = actor;
return Task.FromResult(ToSummary(issuer));
}
}
public Task<IReadOnlyList<AdministrationTrustCertificateSummary>> ListCertificatesAsync(
string tenantId,
int limit,

View File

@@ -390,6 +390,109 @@ public sealed class PostgresAdministrationTrustSigningStore : IAdministrationTru
}
}
public async Task<AdministrationTrustIssuerSummary> BlockIssuerAsync(
string tenantId,
string actorId,
Guid issuerId,
BlockAdministrationTrustIssuerRequest request,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (request is null) throw new InvalidOperationException("request_required");
_ = NormalizeRequired(request.Reason, "reason_required");
var actor = NormalizeActor(actorId);
var now = _timeProvider.GetUtcNow();
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
await using var command = new NpgsqlCommand(
"""
UPDATE release.trust_issuers
SET
trust_level = 'blocked',
status = 'blocked',
updated_at = @updated_at,
updated_by = @updated_by
WHERE tenant_id = @tenant_id AND id = @id
RETURNING
id,
issuer_name,
issuer_uri,
trust_level,
status,
created_at,
updated_at,
updated_by
""",
connection);
command.Parameters.AddWithValue("tenant_id", tenantGuid);
command.Parameters.AddWithValue("id", issuerId);
command.Parameters.AddWithValue("updated_at", now);
command.Parameters.AddWithValue("updated_by", actor);
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
throw new InvalidOperationException("issuer_not_found");
}
return MapIssuerSummary(reader);
}
public async Task<AdministrationTrustIssuerSummary> UnblockIssuerAsync(
string tenantId,
string actorId,
Guid issuerId,
UnblockAdministrationTrustIssuerRequest request,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (request is null) throw new InvalidOperationException("request_required");
var trustLevel = NormalizeOptional(request.TrustLevel) ?? "minimal";
var actor = NormalizeActor(actorId);
var now = _timeProvider.GetUtcNow();
var tenantGuid = TenantStorageKey.ParseTenantGuid(tenantId);
await using var connection = await OpenTenantConnectionAsync(tenantGuid, cancellationToken).ConfigureAwait(false);
await using var command = new NpgsqlCommand(
"""
UPDATE release.trust_issuers
SET
trust_level = @trust_level,
status = 'active',
updated_at = @updated_at,
updated_by = @updated_by
WHERE tenant_id = @tenant_id AND id = @id
RETURNING
id,
issuer_name,
issuer_uri,
trust_level,
status,
created_at,
updated_at,
updated_by
""",
connection);
command.Parameters.AddWithValue("tenant_id", tenantGuid);
command.Parameters.AddWithValue("id", issuerId);
command.Parameters.AddWithValue("trust_level", trustLevel);
command.Parameters.AddWithValue("updated_at", now);
command.Parameters.AddWithValue("updated_by", actor);
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
throw new InvalidOperationException("issuer_not_found");
}
return MapIssuerSummary(reader);
}
public async Task<IReadOnlyList<AdministrationTrustCertificateSummary>> ListCertificatesAsync(
string tenantId,
int limit,

View File

@@ -64,6 +64,31 @@ public sealed class AdministrationTrustSigningMutationEndpointsTests : IClassFix
var issuer = await issuerResponse.Content.ReadFromJsonAsync<AdministrationTrustIssuerSummary>(
TestContext.Current.CancellationToken);
Assert.NotNull(issuer);
Assert.Equal("active", issuer!.Status);
var blockIssuerResponse = await client.PostAsJsonAsync(
$"/api/v1/administration/trust-signing/issuers/{issuer.IssuerId}/block",
new BlockAdministrationTrustIssuerRequest("publisher compromised", "IR-51"),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, blockIssuerResponse.StatusCode);
var blockedIssuer = await blockIssuerResponse.Content.ReadFromJsonAsync<AdministrationTrustIssuerSummary>(
TestContext.Current.CancellationToken);
Assert.NotNull(blockedIssuer);
Assert.Equal("blocked", blockedIssuer!.TrustLevel);
Assert.Equal("blocked", blockedIssuer.Status);
var unblockIssuerResponse = await client.PostAsJsonAsync(
$"/api/v1/administration/trust-signing/issuers/{issuer.IssuerId}/unblock",
new UnblockAdministrationTrustIssuerRequest("partial", "IR-52"),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, unblockIssuerResponse.StatusCode);
var unblockedIssuer = await unblockIssuerResponse.Content.ReadFromJsonAsync<AdministrationTrustIssuerSummary>(
TestContext.Current.CancellationToken);
Assert.NotNull(unblockedIssuer);
Assert.Equal("partial", unblockedIssuer!.TrustLevel);
Assert.Equal("active", unblockedIssuer.Status);
var certificateResponse = await client.PostAsJsonAsync(
"/api/v1/administration/trust-signing/certificates",
@@ -194,6 +219,8 @@ public sealed class AdministrationTrustSigningMutationEndpointsTests : IClassFix
AssertPolicy(endpoints, "/api/v1/administration/trust-signing/keys/{keyId:guid}/rotate", "POST", PlatformPolicies.TrustWrite);
AssertPolicy(endpoints, "/api/v1/administration/trust-signing/keys/{keyId:guid}/revoke", "POST", PlatformPolicies.TrustAdmin);
AssertPolicy(endpoints, "/api/v1/administration/trust-signing/issuers", "POST", PlatformPolicies.TrustWrite);
AssertPolicy(endpoints, "/api/v1/administration/trust-signing/issuers/{issuerId:guid}/block", "POST", PlatformPolicies.TrustAdmin);
AssertPolicy(endpoints, "/api/v1/administration/trust-signing/issuers/{issuerId:guid}/unblock", "POST", PlatformPolicies.TrustAdmin);
AssertPolicy(endpoints, "/api/v1/administration/trust-signing/certificates/{certificateId:guid}/revoke", "POST", PlatformPolicies.TrustAdmin);
AssertPolicy(endpoints, "/api/v1/administration/trust-signing/transparency-log", "GET", PlatformPolicies.TrustRead);
AssertPolicy(endpoints, "/api/v1/administration/trust-signing/transparency-log", "PUT", PlatformPolicies.TrustAdmin);