feat(admin): consume LDAP role grants at sign-in, incl. cluster scoping

The role-grants page authored LdapGroupRoleMapping rows but nothing
consumed them — sign-in only read the static appsettings GroupToRole
dictionary. Wire the DB-backed grants into the auth path.

- AdminRoleGrantResolver merges the static bootstrap dictionary (always
  fleet-wide, lock-out-proof) with DB grants; system-wide rows fold into
  fleet roles, cluster-scoped rows become (cluster, role) grants.
- Login emits a ClaimTypes.Role claim per fleet role and a cluster_role
  claim per cluster-scoped grant; lock-out check spans both scopes.
- ClusterRoleClaims + ClaimsPrincipal extensions resolve the effective
  role for a cluster (highest of fleet-wide and cluster-scoped).
- ClusterAuthorizeView gates cluster pages: ClusterDetail (view +
  ConfigEditor draft actions), DraftEditor (ConfigEditor / FleetAdmin
  publish), DiffViewer (ConfigViewer), ImportEquipment (ConfigEditor).
- RoleGrants page is now FleetAdmin-only; Account surfaces fleet-wide
  and cluster-scoped grants separately.

Control-plane only — decision #150 holds, NodeAcl is untouched.

Tests: AdminRoleGrantResolverTests + ClusterRoleClaimsTests (22).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-18 03:08:39 -04:00
parent 1e04796953
commit 8adb83afee
14 changed files with 567 additions and 10 deletions

View File

@@ -1,10 +1,21 @@
@page "/clusters/{ClusterId}/draft/{GenerationId:long}"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@using ZB.MOM.WW.OtOpcUa.Admin.Services
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
@using ZB.MOM.WW.OtOpcUa.Configuration.Validation
@inject GenerationService GenerationSvc
@inject DraftValidationService ValidationSvc
@inject NavigationManager Nav
<ClusterAuthorizeView ClusterId="@ClusterId" MinRole="AdminRole.ConfigEditor">
<NotAuthorized>
<section class="panel notice rise" style="animation-delay:.02s">
Editing cluster <span class="mono">@ClusterId</span> requires the
<span class="mono">ConfigEditor</span> role for this cluster.
</section>
</NotAuthorized>
<Authorized>
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h1 class="page-title mb-0">Draft editor</h1>
@@ -13,7 +24,9 @@
<div>
<a class="btn btn-outline-secondary" href="/clusters/@ClusterId">Back to cluster</a>
<a class="btn btn-outline-primary ms-2" href="/clusters/@ClusterId/draft/@GenerationId/diff">View diff</a>
<button class="btn btn-primary ms-2" disabled="@(_errors.Count != 0 || _busy)" @onclick="PublishAsync">Publish</button>
<ClusterAuthorizeView ClusterId="@ClusterId" MinRole="AdminRole.FleetAdmin">
<button class="btn btn-primary ms-2" disabled="@(_errors.Count != 0 || _busy)" @onclick="PublishAsync">Publish</button>
</ClusterAuthorizeView>
</div>
</div>
@@ -65,6 +78,9 @@
</div>
</div>
</Authorized>
</ClusterAuthorizeView>
@code {
[Parameter] public string ClusterId { get; set; } = string.Empty;
[Parameter] public long GenerationId { get; set; }