Repair first-time identity and trust operator journeys
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user