From 8c429c3131362e91e7e8d027469b3ce1792e0c57 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 18 Jun 2026 05:11:12 -0400 Subject: [PATCH] feat(adminui): trust/untrust/delete actions on /certificates (FleetAdmin) --- .../Components/Pages/Certificates.razor | 98 +++++++++++++++++-- .../EndpointRouteBuilderExtensions.cs | 3 + 2 files changed, 93 insertions(+), 8 deletions(-) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Certificates.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Certificates.razor index 0ac30099..601aaaa9 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Certificates.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Certificates.razor @@ -3,7 +3,11 @@ @rendermode RenderMode.InteractiveServer @using System.Security.Cryptography.X509Certificates @using Microsoft.Extensions.Configuration +@using Microsoft.AspNetCore.Components.Authorization +@using ZB.MOM.WW.OtOpcUa.AdminUI.Certificates @inject IConfiguration Config +@inject CertificateStoreManager CertManager +@implements IDisposable

OPC UA certificates

@@ -22,6 +26,18 @@ } else { + @if (_statusMsg is not null) + { +
@_statusMsg
+ } + @if (_pending is { } p) + { +
+ Confirm @p.Verb of @p.Subject? + + +
+ } @foreach (var store in _rows) {
@@ -51,6 +67,10 @@ else Thumbprint Not before Not after + @if (store.Kind is StoreKind.Trusted or StoreKind.Rejected) + { + Actions + } @@ -62,6 +82,24 @@ else @c.Thumbprint[..16]… @c.NotBefore.ToString("u") @c.NotAfter.ToString("u") + @if (store.Kind is StoreKind.Trusted or StoreKind.Rejected) + { + + + + @if (store.Kind == StoreKind.Rejected) + { + + } + else + { + + } + + + + + } } @@ -75,21 +113,42 @@ else @code { private List? _rows; - protected override void OnInitialized() + private enum StoreKind { Own, Trusted, Issuer, Rejected } + private sealed record StoreView(string Label, StoreKind Kind, string Path, List Certificates); + + private (StoreKind Kind, string Thumbprint, string Subject, string Verb)? _pending; + private string? _statusMsg; + private bool _statusError; + + protected override void OnInitialized() => LoadAll(); + + private void LoadAll() { + DisposeRows(); var pkiRoot = Config.GetValue("OpcUa:PkiStoreRoot") ?? "pki"; _rows = new() { - LoadStore("Own", Path.Combine(pkiRoot, "own", "certs")), - LoadStore("Trusted peers", Path.Combine(pkiRoot, "trusted", "certs")), - LoadStore("Trusted issuers", Path.Combine(pkiRoot, "issuer", "certs")), - LoadStore("Rejected", Path.Combine(pkiRoot, "rejected", "certs")), + LoadStore("Own", StoreKind.Own, Path.Combine(pkiRoot, "own", "certs")), + LoadStore("Trusted peers", StoreKind.Trusted, Path.Combine(pkiRoot, "trusted", "certs")), + LoadStore("Trusted issuers", StoreKind.Issuer, Path.Combine(pkiRoot, "issuer", "certs")), + LoadStore("Rejected", StoreKind.Rejected, Path.Combine(pkiRoot, "rejected", "certs")), }; + _pending = null; } - private static StoreView LoadStore(string label, string path) + private void DisposeRows() { - var view = new StoreView(label, path, new List()); + if (_rows is null) return; + foreach (var store in _rows) + foreach (var c in store.Certificates) + c.Dispose(); + } + + public void Dispose() => DisposeRows(); + + private static StoreView LoadStore(string label, StoreKind kind, string path) + { + var view = new StoreView(label, kind, path, new List()); if (!Directory.Exists(path)) return view; foreach (var file in Directory.EnumerateFiles(path).Where(IsCertFile)) { @@ -107,5 +166,28 @@ else || ext.Equals(".crt", StringComparison.OrdinalIgnoreCase); } - private sealed record StoreView(string Label, string Path, List Certificates); + private void RequestAction(StoreKind kind, X509Certificate2 cert, string verb) + { + _pending = (kind, cert.Thumbprint!, cert.Subject, verb); + _statusMsg = null; + } + + private void CancelAction() => _pending = null; + + private void ConfirmAction() + { + if (_pending is not { } p) return; + var result = p.Verb switch + { + "trust" => CertManager.Trust(p.Thumbprint), + "untrust" => CertManager.Untrust(p.Thumbprint), + "delete" => CertManager.Delete(p.Kind == StoreKind.Trusted ? "trusted" : "rejected", p.Thumbprint), + _ => CertActionResult.Fail("unknown action"), + }; + _statusError = !result.Success; + _statusMsg = result.Success + ? $"{char.ToUpper(p.Verb[0])}{p.Verb[1..]} of {p.Subject} succeeded." + : $"{p.Verb} failed: {result.Error}"; + LoadAll(); // clears _pending + } } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/EndpointRouteBuilderExtensions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/EndpointRouteBuilderExtensions.cs index ce7bef44..422a7dab 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/EndpointRouteBuilderExtensions.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/EndpointRouteBuilderExtensions.cs @@ -51,6 +51,9 @@ public static class EndpointRouteBuilderExtensions services.AddScoped(); services.AddScoped(); + // Certificate-store actions (trust/untrust/delete) for the /certificates page. + services.AddSingleton(); + return services; } }