feat(management): Admin-gated actor dispatch for cert-trust commands (arch-review C4)

Made CommunicationService.TrustServerCertAsync/RemoveServerCertAsync/ListServerCertsAsync virtual to support the test seam (mirrors existing virtual WriteTagAsync).

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 05:29:57 -04:00
parent 722063638b
commit 5415e6566f
4 changed files with 154 additions and 5 deletions
@@ -212,7 +212,12 @@ public class ManagementActor : ReceiveActor
// gate must match — otherwise a Designer blocked in the UI could still
// rotate a production credential via the CLI/Management API.
or UpdateSmtpConfigCommand
or UpdateSmsConfigCommand => AdminOnly,
or UpdateSmsConfigCommand
// Server-certificate trust (arch-review C4). These mutate the site's
// trusted-peer PKI store — Admin, matching the Admin-gated UI cert
// page. Site-side reconcile semantics are owned by the site
// CertStoreActor (plan 03); this handler only relays.
or TrustServerCertCommand or RemoveServerCertCommand or ListServerCertsCommand => AdminOnly,
// Area management — any-of [Designer, Deployer] (arch-review C6).
// Exposed both in the Designer tooling and inside the Central UI
@@ -443,6 +448,9 @@ public class ManagementActor : ReceiveActor
BrowseNodeCommand cmd => await HandleBrowseNode(sp, cmd, user),
SearchAddressSpaceCommand cmd => await HandleSearchAddressSpace(sp, cmd, user),
VerifyEndpointCommand cmd => await HandleVerifyEndpoint(sp, cmd, user),
TrustServerCertCommand cmd => await HandleTrustServerCert(sp, cmd, user),
RemoveServerCertCommand cmd => await HandleRemoveServerCert(sp, cmd, user),
ListServerCertsCommand cmd => await HandleListServerCerts(sp, cmd, user),
QueryParkedMessagesCommand cmd => await HandleQueryParkedMessages(sp, cmd, user),
RetryParkedMessageCommand cmd => await HandleRetryParkedMessage(sp, cmd, user),
DiscardParkedMessageCommand cmd => await HandleDiscardParkedMessage(sp, cmd, user),
@@ -2955,6 +2963,39 @@ public class ManagementActor : ReceiveActor
return await commService.VerifyEndpointAsync(site, cmd);
}
// Server-certificate trust (arch-review C4). Admin-gated at the role check
// above. These mutate the site's trusted-peer PKI store — the handler only
// relays; the site-side broadcast/reconcile across both nodes is owned by
// the site CertStoreActor (plan 03). SiteIdentifier is REQUIRED so the actor
// knows which site to route to.
private static async Task<object?> HandleTrustServerCert(IServiceProvider sp, TrustServerCertCommand cmd, AuthenticatedUser user)
{
var site = cmd.SiteIdentifier
?? throw new ManagementCommandException("SiteIdentifier is required when trusting a server certificate via the management API.");
await EnforceSiteScopeForIdentifier(sp, user, site);
var commService = sp.GetRequiredService<CommunicationService>();
return await commService.TrustServerCertAsync(site, cmd);
}
private static async Task<object?> HandleRemoveServerCert(IServiceProvider sp, RemoveServerCertCommand cmd, AuthenticatedUser user)
{
var site = cmd.SiteIdentifier
?? throw new ManagementCommandException("SiteIdentifier is required when removing a server certificate via the management API.");
await EnforceSiteScopeForIdentifier(sp, user, site);
var commService = sp.GetRequiredService<CommunicationService>();
return await commService.RemoveServerCertAsync(site, cmd);
}
private static async Task<object?> HandleListServerCerts(IServiceProvider sp, ListServerCertsCommand cmd, AuthenticatedUser user)
{
var site = cmd.SiteIdentifier
?? throw new ManagementCommandException("SiteIdentifier is required when listing server certificates via the management API.");
await EnforceSiteScopeForIdentifier(sp, user, site);
var commService = sp.GetRequiredService<CommunicationService>();
return await commService.ListServerCertsAsync(site, cmd);
}
private static async Task<object?> HandleQueryParkedMessages(IServiceProvider sp, QueryParkedMessagesCommand cmd, AuthenticatedUser user)
{
await EnforceSiteScopeForIdentifier(sp, user, cmd.SiteIdentifier);