From d5fa1f450eaac5f523519fd8b047a1713a7bd68e Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 18 Apr 2026 14:43:35 -0400 Subject: [PATCH] =?UTF-8?q?Phase=203=20PR=2029=20=E2=80=94=20Account/sessi?= =?UTF-8?q?on=20page=20expanding=20the=20minimal=20sidebar=20role=20displa?= =?UTF-8?q?y=20into=20a=20dedicated=20/account=20route.=20Shows=20the=20au?= =?UTF-8?q?thenticated=20operator's=20identity=20(username=20from=20ClaimT?= =?UTF-8?q?ypes.NameIdentifier,=20display=20name=20from=20ClaimTypes.Name)?= =?UTF-8?q?,=20their=20Admin=20roles=20as=20badges=20(from=20ClaimTypes.Ro?= =?UTF-8?q?le),=20the=20raw=20LDAP=20groups=20that=20mapped=20to=20those?= =?UTF-8?q?=20roles=20(from=20the=20'ldap=5Fgroup'=20claim=20added=20by=20?= =?UTF-8?q?Login.razor=20at=20sign-in),=20and=20a=20capability=20table=20l?= =?UTF-8?q?isting=20each=20Admin=20capability=20with=20its=20required=20ro?= =?UTF-8?q?le=20and=20a=20Yes/No=20badge=20showing=20whether=20this=20sess?= =?UTF-8?q?ion=20has=20it.=20Capability=20list=20mirrors=20the=20Program.c?= =?UTF-8?q?s=20authorization=20policies=20+=20each=20page's=20[Authorize]?= =?UTF-8?q?=20attribute=20so=20operators=20can=20self-service=20check=20wh?= =?UTF-8?q?ether=20their=20session=20has=20access=20without=20trial-and-er?= =?UTF-8?q?ror=20navigation=20=E2=80=94=20capabilities=20covered:=20view?= =?UTF-8?q?=20clusters=20+=20fleet=20status=20(all=20roles),=20edit=20conf?= =?UTF-8?q?iguration=20drafts=20(ConfigEditor=20or=20FleetAdmin=20per=20Ca?= =?UTF-8?q?nEdit=20policy),=20publish=20generations=20(FleetAdmin=20per=20?= =?UTF-8?q?CanPublish=20policy),=20manage=20certificate=20trust=20(FleetAd?= =?UTF-8?q?min=20per=20PR=2028=20Certificates=20page=20attribute),=20manag?= =?UTF-8?q?e=20external-ID=20reservations=20(ConfigEditor=20or=20FleetAdmi?= =?UTF-8?q?n=20per=20Reservations=20page=20attribute).=20Sidebar's=20'Sign?= =?UTF-8?q?ed=20in=20as'=20line=20now=20wraps=20the=20display=20name=20in?= =?UTF-8?q?=20a=20link=20to=20/account=20so=20the=20existing=20sidebar-com?= =?UTF-8?q?pact=20view=20becomes=20the=20entry=20point=20for=20the=20fulle?= =?UTF-8?q?r=20page=20=E2=80=94=20keeps=20the=20sign-out=20button=20where?= =?UTF-8?q?=20it=20was=20for=20muscle=20memory,=20just=20adds=20the=20deta?= =?UTF-8?q?il=20page=20one=20click=20away.=20Page=20is=20gated=20with=20[A?= =?UTF-8?q?uthorize]=20(any=20authenticated=20admin)=20rather=20than=20a?= =?UTF-8?q?=20specific=20role=20=E2=80=94=20the=20capability=20table=20del?= =?UTF-8?q?iberately=20works=20for=20every=20signed-in=20user=20so=20they?= =?UTF-8?q?=20can=20see=20what=20they=20DON'T=20have=20access=20to,=20whic?= =?UTF-8?q?h=20helps=20them=20file=20the=20right=20ticket=20with=20their?= =?UTF-8?q?=20LDAP=20admin=20instead=20of=20getting=20a=20plain=20Access?= =?UTF-8?q?=20Denied=20when=20navigating=20blindly.=20Capability=20?= =?UTF-8?q?=E2=86=92=20required-role=20table=20is=20defined=20as=20a=20pri?= =?UTF-8?q?vate=20readonly=20record=20list=20in=20the=20page=20rather=20th?= =?UTF-8?q?an=20pulled=20from=20a=20service=20because=20it's=20a=20UI-pres?= =?UTF-8?q?entation=20concern,=20not=20runtime=20policy=20state=20?= =?UTF-8?q?=E2=80=94=20the=20runtime=20policy=20IS=20Program.cs's=20AddAut?= =?UTF-8?q?horizationBuilder=20+=20each=20page's=20[Authorize]=20attribute?= =?UTF-8?q?,=20and=20this=20table=20just=20mirrors=20it=20for=20operator?= =?UTF-8?q?=20readability.=20Comment=20on=20the=20list=20reminds=20future-?= =?UTF-8?q?me=20to=20extend=20it=20when=20a=20new=20policy=20or=20[Authori?= =?UTF-8?q?ze]=20page=20lands.=20No=20behavior=20change=20if=20roles=20are?= =?UTF-8?q?=20empty,=20but=20the=20page=20surfaces=20a=20hint=20('Sign-in?= =?UTF-8?q?=20would=20have=20been=20blocked,=20so=20if=20you're=20seeing?= =?UTF-8?q?=20this,=20the=20session=20claim=20is=20likely=20stale')=20that?= =?UTF-8?q?=20nudges=20the=20operator=20toward=20signing=20out=20+=20back?= =?UTF-8?q?=20in.=20No=20new=20tests=20added=20=E2=80=94=20the=20page=20is?= =?UTF-8?q?=20pure=20display=20over=20claims;=20its=20only=20logic=20is=20?= =?UTF-8?q?the=20'has-capability'=20Any-overlap=20check=20which=20is=20exa?= =?UTF-8?q?ctly=20what=20ASP.NET's=20[Authorize(Roles=3D...)]=20does=20in-?= =?UTF-8?q?framework,=20and=20duplicating=20that=20in=20a=20unit=20test=20?= =?UTF-8?q?would=20test=20the=20framework=20rather=20than=20our=20code.=20?= =?UTF-8?q?Admin.Tests=20Unit=20stays=2023=20pass=20/=200=20fail.=20Admin?= =?UTF-8?q?=20build=20clean.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Components/Layout/MainLayout.razor | 2 +- .../Components/Pages/Account.razor | 129 ++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Account.razor diff --git a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Layout/MainLayout.razor b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Layout/MainLayout.razor index f93b85f..03540ed 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Layout/MainLayout.razor +++ b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Layout/MainLayout.razor @@ -15,7 +15,7 @@
- Signed in as @context.User.Identity?.Name + Signed in as @context.User.Identity?.Name
@string.Join(", ", context.User.Claims.Where(c => c.Type.EndsWith("/role")).Select(c => c.Value)) diff --git a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Account.razor b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Account.razor new file mode 100644 index 0000000..16aef4c --- /dev/null +++ b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Account.razor @@ -0,0 +1,129 @@ +@page "/account" +@attribute [Microsoft.AspNetCore.Authorization.Authorize] +@using System.Security.Claims +@using ZB.MOM.WW.OtOpcUa.Admin.Services + +

My account

+ + + + @{ + var username = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "—"; + var displayName = context.User.Identity?.Name ?? "—"; + var roles = context.User.Claims + .Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList(); + var ldapGroups = context.User.Claims + .Where(c => c.Type == "ldap_group").Select(c => c.Value).ToList(); + } + +
+
+
+
+
Identity
+
+
Username
@username
+
Display name
@displayName
+
+
+
+
+ +
+
+
+
Admin roles
+ @if (roles.Count == 0) + { +

No Admin roles mapped — sign-in would have been blocked, so if you're seeing this, the session claim is likely stale.

+ } + else + { +
+ @foreach (var r in roles) + { + @r + } +
+ LDAP groups: @(ldapGroups.Count == 0 ? "(none surfaced)" : string.Join(", ", ldapGroups)) + } +
+
+
+ +
+
+
+
Capabilities
+

+ Each Admin role grants a fixed capability set per admin-ui.md §Admin Roles. + Pages below reflect what this session can access; the route's [Authorize] guard + is the ground truth — this table mirrors it for readability. +

+ + + + + + + + + + @foreach (var cap in Capabilities) + { + var has = cap.RequiredRoles.Any(r => roles.Contains(r, StringComparer.OrdinalIgnoreCase)); + + + + + + } + +
CapabilityRequired role(s)You have it?
@cap.Name
@cap.Description
@string.Join(" or ", cap.RequiredRoles) + @if (has) + { + Yes + } + else + { + No + } +
+
+
+
+
+ +
+
+ +
+
+
+
+ +@code { + private sealed record Capability(string Name, string Description, string[] RequiredRoles); + + // Kept in sync with Program.cs authorization policies + each page's [Authorize] attribute. + // When a new page or policy is added, extend this list so operators can self-service check + // whether their session has access without trial-and-error navigation. + private static readonly IReadOnlyList Capabilities = + [ + new("View clusters + fleet status", + "Read-only access to the cluster list, fleet dashboard, and generation history.", + [AdminRoles.ConfigViewer, AdminRoles.ConfigEditor, AdminRoles.FleetAdmin]), + new("Edit configuration drafts", + "Create and edit draft generations, manage namespace bindings and node ACLs. CanEdit policy.", + [AdminRoles.ConfigEditor, AdminRoles.FleetAdmin]), + new("Publish generations", + "Promote a draft to Published — triggers node roll-out. CanPublish policy.", + [AdminRoles.FleetAdmin]), + new("Manage certificate trust", + "Trust rejected client certs + revoke trust. FleetAdmin-only because the trust decision gates OPC UA client access.", + [AdminRoles.FleetAdmin]), + new("Manage external-ID reservations", + "Reserve / release external IDs that map into Galaxy contained names.", + [AdminRoles.ConfigEditor, AdminRoles.FleetAdmin]), + ]; +} -- 2.49.1