using Microsoft.AspNetCore.Components.Authorization; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; using ZB.MOM.WW.ScadaBridge.Communication; using ZB.MOM.WW.ScadaBridge.Security; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services; /// /// Default implementation — a thin facade over /// the three cert-trust relay methods that enforces /// the CentralUI-side role trust boundary (Decision D7: Trust + Remove require /// Administrator, List requires Designer), and translates transport /// exceptions into a typed . /// /// /// Site-side actors (SiteCommunicationActor + DeploymentManagerActor) do /// not unwrap the central trust envelope, so the role check MUST run here — never on /// the site (mirrors and ). /// On an unauthorized caller the method returns a non-success /// with "Not authorized." rather than throwing; /// transport failures (timeouts, unreachable sites) collapse into a non-success result /// so the editor / page can render an inline outcome. /// public sealed class CertManagementService : ICertManagementService { private readonly CommunicationService _communication; private readonly AuthenticationStateProvider _auth; /// /// Initializes a new instance of the . /// /// Central-side cluster communication service. /// Authentication state provider used for the role guards. public CertManagementService(CommunicationService communication, AuthenticationStateProvider auth) { _communication = communication ?? throw new ArgumentNullException(nameof(communication)); _auth = auth ?? throw new ArgumentNullException(nameof(auth)); } /// public async Task TrustAsync( string siteIdentifier, string connectionName, string derBase64, string thumbprint, CancellationToken cancellationToken = default) { // D7: trusting a server certificate mutates every site node's PKI store, so // it is an Administrator-only action. The site does not enforce envelope-level // roles, so this check must happen here before any cross-cluster traffic. if (!await HasRoleAsync(Roles.Administrator)) { return new CertTrustResult(false, "Not authorized.", null); } try { return await _communication.TrustServerCertAsync( siteIdentifier, new TrustServerCertCommand(connectionName, derBase64, thumbprint), cancellationToken); } catch (TimeoutException ex) { return new CertTrustResult(false, ex.Message, null); } catch (OperationCanceledException) { // Caller-initiated cancel — propagate so Blazor can drop the response // cleanly. Distinct from Timeout (which the UI renders inline). throw; } catch (Exception ex) { return new CertTrustResult(false, ex.Message, null); } } /// public async Task ListAsync( string siteIdentifier, CancellationToken cancellationToken = default) { // D7: listing trusted certs is read-only, so the lower Designer bar applies // (an Administrator also satisfies this because admins hold every role claim // by convention). Same CentralUI-side guard rationale as TrustAsync. if (!await HasRoleAsync(Roles.Designer)) { return new CertTrustResult(false, "Not authorized.", null); } try { return await _communication.ListServerCertsAsync( siteIdentifier, new ListServerCertsCommand(), cancellationToken); } catch (TimeoutException ex) { return new CertTrustResult(false, ex.Message, null); } catch (OperationCanceledException) { throw; } catch (Exception ex) { return new CertTrustResult(false, ex.Message, null); } } /// public async Task RemoveAsync( string siteIdentifier, string thumbprint, CancellationToken cancellationToken = default) { // D7: removing trust mutates every site node's PKI store, so it is an // Administrator-only action — same gate as TrustAsync. if (!await HasRoleAsync(Roles.Administrator)) { return new CertTrustResult(false, "Not authorized.", null); } try { return await _communication.RemoveServerCertAsync( siteIdentifier, new RemoveServerCertCommand(thumbprint), cancellationToken); } catch (TimeoutException ex) { return new CertTrustResult(false, ex.Message, null); } catch (OperationCanceledException) { throw; } catch (Exception ex) { return new CertTrustResult(false, ex.Message, null); } } private async Task HasRoleAsync(string role) { var state = await _auth.GetAuthenticationStateAsync(); return state.User.HasClaim(JwtTokenService.RoleClaimType, role); } }