087a969ee6
Pulled forward from stage 7 because without it the feature is unusable and un-live-gateable: there was no way to create a key short of hand-inserting a row and computing an HMAC by hand. The minting logic is an IApiKeyAdminService in Core.Authorization, NOT logic in the Razor page — this repo has no bUnit, so anything only a page can reach is untestable, and minting credentials is not where that belongs. The page is a thin shell over it. Moved to Core.Authorization so both ends can reach them: ApiKeyOptions (was in Host) and OpcUaPrincipals (was in OpcUaServer.Security). The OPC UA server resolves principals; the AdminUI must refuse to mint a key claiming a reserved one. OpcUaServer already gained a Core reference in stage 1b, so no new edge — AdminUI gains one. ApiKeyOptions is now bound UNCONDITIONALLY rather than in the driver branch: a driver node needs the pepper to VERIFY keys and an admin node needs it to MINT them, and getting that wrong on either side is a silent authentication failure rather than a startup error. Validation rules, all covered: the reserved 'anonymous' principal is refused case-insensitively (so "ANONYMOUS" cannot slip past into a distinct principal that the evaluator would then match case-INsensitively against anonymous's grants); blank name/principal; a principal longer than the column, rejected rather than truncated, because silent truncation yields a key that authenticates and is granted nothing with no visible cause; a past expiry. Generation runs BEFORE the DbContext is opened so an unconfigured pepper surfaces as a clear error instead of a stored row whose hash nothing can verify. The page shows the plaintext ONCE and says so; it is held in component state and written nowhere else, because the server keeps only a hash. Revocation is flagged in the UI as taking effect only after the next deployment — driver-only nodes have no ConfigDb, so keys ride the artifact. FleetAdmin-only (Administrator), not ConfigEditor: this mints credentials. Core.Tests 68 green. Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
58 lines
2.7 KiB
Plaintext
58 lines
2.7 KiB
Plaintext
@inherits LayoutComponentBase
|
|
@using System.Security.Claims
|
|
|
|
@* Thin delegation to the shared ZB.MOM.WW.Theme side-rail chassis. ThemeShell owns
|
|
the brand bar, the CSS-only narrow-viewport hamburger, and the responsive collapse,
|
|
so MainLayout no longer carries its own .app-shell / hamburger wrapper. Nav sections
|
|
are static <details> (NavRailSection) whose expand state is persisted to localStorage
|
|
by the kit's <ThemeScripts/> (emitted in App.razor) — replacing the old interactive
|
|
NavSidebar island + cookie/URL auto-expand. *@
|
|
|
|
<ThemeShell Product="OtOpcUa" Accent="#2f5fd0">
|
|
<Nav>
|
|
<NavRailSection Title="Navigation" Key="nav">
|
|
<NavRailItem Href="/" Text="Overview" Match="NavLinkMatch.All" />
|
|
<NavRailItem Href="/fleet" Text="Fleet status" />
|
|
<NavRailItem Href="/hosts" Text="Host status" />
|
|
<NavRailItem Href="/clusters" Text="Clusters" />
|
|
<NavRailItem Href="/uns" Text="UNS" />
|
|
<NavRailItem Href="/raw" Text="Raw" />
|
|
<NavRailItem Href="/reservations" Text="Reservations" />
|
|
<NavRailItem Href="/certificates" Text="Certificates" />
|
|
<NavRailItem Href="/role-grants" Text="Role grants" />
|
|
<NavRailItem Href="/apikeys" Text="API keys" />
|
|
<NavRailItem Href="/admin/secrets" Text="Secrets" />
|
|
</NavRailSection>
|
|
<NavRailSection Title="Scripting" Key="scripting">
|
|
<NavRailItem Href="/scripts" Text="Scripts" />
|
|
<NavRailItem Href="/script-log" Text="Script log" />
|
|
</NavRailSection>
|
|
<NavRailSection Title="Live" Key="live">
|
|
<NavRailItem Href="/deployments" Text="Deployments" />
|
|
<NavRailItem Href="/alerts" Text="Alerts" />
|
|
<NavRailItem Href="/alarms-historian" Text="Alarms historian" />
|
|
</NavRailSection>
|
|
</Nav>
|
|
<RailFooter>
|
|
<AuthorizeView>
|
|
<Authorized>
|
|
<div class="rail-eyebrow">Session</div>
|
|
<a class="rail-user" href="/account">@context.User.Identity?.Name</a>
|
|
<div class="rail-roles">
|
|
@string.Join(", ", context.User.Claims
|
|
.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value))
|
|
</div>
|
|
<form method="post" action="/auth/logout">
|
|
<AntiforgeryToken />
|
|
<button class="rail-btn" type="submit">Sign out</button>
|
|
</form>
|
|
</Authorized>
|
|
<NotAuthorized>
|
|
<div class="rail-eyebrow">Session</div>
|
|
<a class="rail-btn" href="/login">Sign in</a>
|
|
</NotAuthorized>
|
|
</AuthorizeView>
|
|
</RailFooter>
|
|
<ChildContent>@Body</ChildContent>
|
|
</ThemeShell>
|