feat(adminui): trust/untrust/delete actions on /certificates (FleetAdmin)

This commit is contained in:
Joseph Doherty
2026-06-18 05:11:12 -04:00
parent e8769fd8a8
commit 8c429c3131
2 changed files with 93 additions and 8 deletions
@@ -3,7 +3,11 @@
@rendermode RenderMode.InteractiveServer @rendermode RenderMode.InteractiveServer
@using System.Security.Cryptography.X509Certificates @using System.Security.Cryptography.X509Certificates
@using Microsoft.Extensions.Configuration @using Microsoft.Extensions.Configuration
@using Microsoft.AspNetCore.Components.Authorization
@using ZB.MOM.WW.OtOpcUa.AdminUI.Certificates
@inject IConfiguration Config @inject IConfiguration Config
@inject CertificateStoreManager CertManager
@implements IDisposable
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">OPC UA certificates</h4> <h4 class="mb-0">OPC UA certificates</h4>
@@ -22,6 +26,18 @@
} }
else else
{ {
@if (_statusMsg is not null)
{
<section class="panel @(_statusError ? "error" : "notice") rise mt-3">@_statusMsg</section>
}
@if (_pending is { } p)
{
<section class="panel notice rise mt-3">
Confirm <strong>@p.Verb</strong> of <span class="mono small">@p.Subject</span>?
<button class="btn btn-sm btn-primary" @onclick="ConfirmAction">Confirm</button>
<button class="btn btn-sm btn-outline-secondary" @onclick="CancelAction">Cancel</button>
</section>
}
@foreach (var store in _rows) @foreach (var store in _rows)
{ {
<section class="panel rise mt-3" style="animation-delay:.08s"> <section class="panel rise mt-3" style="animation-delay:.08s">
@@ -51,6 +67,10 @@ else
<th>Thumbprint</th> <th>Thumbprint</th>
<th>Not before</th> <th>Not before</th>
<th>Not after</th> <th>Not after</th>
@if (store.Kind is StoreKind.Trusted or StoreKind.Rejected)
{
<th>Actions</th>
}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -62,6 +82,24 @@ else
<td><span class="mono small">@c.Thumbprint[..16]…</span></td> <td><span class="mono small">@c.Thumbprint[..16]…</span></td>
<td>@c.NotBefore.ToString("u")</td> <td>@c.NotBefore.ToString("u")</td>
<td>@c.NotAfter.ToString("u")</td> <td>@c.NotAfter.ToString("u")</td>
@if (store.Kind is StoreKind.Trusted or StoreKind.Rejected)
{
<td>
<AuthorizeView Policy="FleetAdmin">
<Authorized>
@if (store.Kind == StoreKind.Rejected)
{
<button class="btn btn-sm btn-outline-primary" @onclick='() => RequestAction(store.Kind, c, "trust")'>Trust</button>
}
else
{
<button class="btn btn-sm btn-outline-secondary" @onclick='() => RequestAction(store.Kind, c, "untrust")'>Untrust</button>
}
<button class="btn btn-sm btn-outline-danger" @onclick='() => RequestAction(store.Kind, c, "delete")'>Delete</button>
</Authorized>
</AuthorizeView>
</td>
}
</tr> </tr>
} }
</tbody> </tbody>
@@ -75,21 +113,42 @@ else
@code { @code {
private List<StoreView>? _rows; private List<StoreView>? _rows;
protected override void OnInitialized() private enum StoreKind { Own, Trusted, Issuer, Rejected }
private sealed record StoreView(string Label, StoreKind Kind, string Path, List<X509Certificate2> 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<string?>("OpcUa:PkiStoreRoot") ?? "pki"; var pkiRoot = Config.GetValue<string?>("OpcUa:PkiStoreRoot") ?? "pki";
_rows = new() _rows = new()
{ {
LoadStore("Own", Path.Combine(pkiRoot, "own", "certs")), LoadStore("Own", StoreKind.Own, Path.Combine(pkiRoot, "own", "certs")),
LoadStore("Trusted peers", Path.Combine(pkiRoot, "trusted", "certs")), LoadStore("Trusted peers", StoreKind.Trusted, Path.Combine(pkiRoot, "trusted", "certs")),
LoadStore("Trusted issuers", Path.Combine(pkiRoot, "issuer", "certs")), LoadStore("Trusted issuers", StoreKind.Issuer, Path.Combine(pkiRoot, "issuer", "certs")),
LoadStore("Rejected", Path.Combine(pkiRoot, "rejected", "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<X509Certificate2>()); 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<X509Certificate2>());
if (!Directory.Exists(path)) return view; if (!Directory.Exists(path)) return view;
foreach (var file in Directory.EnumerateFiles(path).Where(IsCertFile)) foreach (var file in Directory.EnumerateFiles(path).Where(IsCertFile))
{ {
@@ -107,5 +166,28 @@ else
|| ext.Equals(".crt", StringComparison.OrdinalIgnoreCase); || ext.Equals(".crt", StringComparison.OrdinalIgnoreCase);
} }
private sealed record StoreView(string Label, string Path, List<X509Certificate2> 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
}
} }
@@ -51,6 +51,9 @@ public static class EndpointRouteBuilderExtensions
services.AddScoped<ScriptAnalysis.ScriptAnalysisService>(); services.AddScoped<ScriptAnalysis.ScriptAnalysisService>();
services.AddScoped<ScriptAnalysis.IScriptTagCatalog, ScriptAnalysis.ScriptTagCatalog>(); services.AddScoped<ScriptAnalysis.IScriptTagCatalog, ScriptAnalysis.ScriptTagCatalog>();
// Certificate-store actions (trust/untrust/delete) for the /certificates page.
services.AddSingleton<Certificates.CertificateStoreManager>();
return services; return services;
} }
} }