Razor page layout: two tables (Rejected / Trusted) with Subject / Issuer / Thumbprint / Valid-window / Actions columns, status banner after each action with success or warning kind ('file missing' = another admin handled it), FleetAdmin-only via [Authorize(Roles=AdminRoles.FleetAdmin)]. Each action invokes LogActionAsync which Serilog-logs the authenticated admin user + thumbprint + action for an audit trail — DB-level ConfigAuditLog persistence is deferred because its schema is cluster-scoped and cert actions are cluster-agnostic; Serilog + CertTrustService's filesystem-op info logs give the forensic trail in the meantime. Sidebar link added to MainLayout between Reservations and the future Account page.
Tests — CertTrustServiceTests (9 new unit cases): ListRejected parses Subject + Thumbprint + store kind from a self-signed test cert written into rejected/certs/; rejected and trusted stores are kept separate; TrustRejected moves the file and the Rejected list is empty afterwards; TrustRejected with a thumbprint not in rejected returns false without touching trusted; DeleteRejected removes the file; UntrustCert removes from trusted only; thumbprint match is case-insensitive (operator UX); missing store directories produce empty lists instead of throwing DirectoryNotFoundException (pristine-install tolerance); a junk .der in the store is logged + skipped and the valid certs still surface (one bad file doesn't break the page). Full Admin.Tests Unit suite: 23 pass / 0 fail (14 prior + 9 new). Full Admin build clean — 0 errors, 0 warnings.
lmx-followups.md #3 marked DONE with a cross-reference to this PR and a note that flipping AutoAcceptUntrustedClientCertificates to false as the production default is a deployment-config follow-up, not a code gap — the Admin UI is now ready to be the trust gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
3.1 KiB
C#
87 lines
3.1 KiB
C#
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Serilog;
|
|
using ZB.MOM.WW.OtOpcUa.Admin.Components;
|
|
using ZB.MOM.WW.OtOpcUa.Admin.Hubs;
|
|
using ZB.MOM.WW.OtOpcUa.Admin.Security;
|
|
using ZB.MOM.WW.OtOpcUa.Admin.Services;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
builder.Host.UseSerilog((ctx, cfg) => cfg
|
|
.MinimumLevel.Information()
|
|
.WriteTo.Console()
|
|
.WriteTo.File("logs/otopcua-admin-.log", rollingInterval: RollingInterval.Day));
|
|
|
|
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
|
|
builder.Services.AddHttpContextAccessor();
|
|
builder.Services.AddSignalR();
|
|
|
|
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
|
.AddCookie(o =>
|
|
{
|
|
o.Cookie.Name = "OtOpcUa.Admin";
|
|
o.LoginPath = "/login";
|
|
o.ExpireTimeSpan = TimeSpan.FromHours(8);
|
|
});
|
|
|
|
builder.Services.AddAuthorizationBuilder()
|
|
.AddPolicy("CanEdit", p => p.RequireRole(AdminRoles.ConfigEditor, AdminRoles.FleetAdmin))
|
|
.AddPolicy("CanPublish", p => p.RequireRole(AdminRoles.FleetAdmin));
|
|
|
|
builder.Services.AddCascadingAuthenticationState();
|
|
|
|
builder.Services.AddDbContext<OtOpcUaConfigDbContext>(opt =>
|
|
opt.UseSqlServer(builder.Configuration.GetConnectionString("ConfigDb")
|
|
?? throw new InvalidOperationException("ConnectionStrings:ConfigDb not configured")));
|
|
|
|
builder.Services.AddScoped<ClusterService>();
|
|
builder.Services.AddScoped<GenerationService>();
|
|
builder.Services.AddScoped<EquipmentService>();
|
|
builder.Services.AddScoped<UnsService>();
|
|
builder.Services.AddScoped<NamespaceService>();
|
|
builder.Services.AddScoped<DriverInstanceService>();
|
|
builder.Services.AddScoped<NodeAclService>();
|
|
builder.Services.AddScoped<ReservationService>();
|
|
builder.Services.AddScoped<DraftValidationService>();
|
|
builder.Services.AddScoped<AuditLogService>();
|
|
|
|
// Cert-trust management — reads the OPC UA server's PKI store root so rejected client certs
|
|
// can be promoted to trusted via the Admin UI. Singleton: no per-request state, just
|
|
// filesystem operations.
|
|
builder.Services.Configure<CertTrustOptions>(builder.Configuration.GetSection(CertTrustOptions.SectionName));
|
|
builder.Services.AddSingleton<CertTrustService>();
|
|
|
|
// LDAP auth — parity with ScadaLink's LdapAuthService (decision #102).
|
|
builder.Services.Configure<LdapOptions>(
|
|
builder.Configuration.GetSection("Authentication:Ldap"));
|
|
builder.Services.AddScoped<ILdapAuthService, LdapAuthService>();
|
|
|
|
// SignalR real-time fleet status + alerts (admin-ui.md §"Real-Time Updates").
|
|
builder.Services.AddHostedService<FleetStatusPoller>();
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseSerilogRequestLogging();
|
|
app.UseStaticFiles();
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
app.UseAntiforgery();
|
|
|
|
app.MapPost("/auth/logout", async (HttpContext ctx) =>
|
|
{
|
|
await ctx.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
|
ctx.Response.Redirect("/");
|
|
});
|
|
|
|
app.MapHub<FleetStatusHub>("/hubs/fleet");
|
|
app.MapHub<AlertHub>("/hubs/alerts");
|
|
|
|
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
|
|
|
|
await app.RunAsync();
|
|
|
|
public partial class Program;
|