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:
@@ -1,7 +1,9 @@
|
||||
@page "/clusters/{ClusterId}"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.SignalR.Client
|
||||
@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.Entities
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
|
||||
@@ -11,10 +13,24 @@
|
||||
@inject GenerationService GenerationSvc
|
||||
@inject NavigationManager Nav
|
||||
|
||||
@if (_cluster is null)
|
||||
@if (!_loaded)
|
||||
{
|
||||
<p>Loading…</p>
|
||||
}
|
||||
else if (!_canView)
|
||||
{
|
||||
<section class="panel notice rise" style="animation-delay:.02s">
|
||||
You don't have access to cluster <span class="mono">@ClusterId</span>. A fleet-wide or
|
||||
cluster-scoped Admin role grant is required — ask a fleet admin to add one on the
|
||||
<a href="/role-grants">role grants</a> page.
|
||||
</section>
|
||||
}
|
||||
else if (_cluster is null)
|
||||
{
|
||||
<section class="panel notice rise" style="animation-delay:.02s">
|
||||
Cluster <span class="mono">@ClusterId</span> was not found.
|
||||
</section>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (_liveBanner is not null)
|
||||
@@ -31,7 +47,11 @@ else
|
||||
@if (!_cluster.Enabled) { <span class="chip chip-idle ms-2">Disabled</span> }
|
||||
</div>
|
||||
<div>
|
||||
@if (_currentDraft is not null)
|
||||
@if (!_canEdit)
|
||||
{
|
||||
<span class="chip chip-idle">Read-only access</span>
|
||||
}
|
||||
else if (_currentDraft is not null)
|
||||
{
|
||||
<a href="/clusters/@ClusterId/draft/@_currentDraft.GenerationId" class="btn btn-outline-primary">
|
||||
Edit current draft (gen @_currentDraft.GenerationId)
|
||||
@@ -119,11 +139,15 @@ else
|
||||
|
||||
@code {
|
||||
[Parameter] public string ClusterId { get; set; } = string.Empty;
|
||||
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
|
||||
private ServerCluster? _cluster;
|
||||
private ConfigGeneration? _currentDraft;
|
||||
private ConfigGeneration? _currentPublished;
|
||||
private string _tab = "overview";
|
||||
private bool _busy;
|
||||
private bool _loaded;
|
||||
private bool _canView;
|
||||
private bool _canEdit;
|
||||
private HubConnection? _hub;
|
||||
private string? _liveBanner;
|
||||
|
||||
@@ -131,6 +155,15 @@ else
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthState is not null)
|
||||
{
|
||||
var user = (await AuthState).User;
|
||||
_canView = user.HasClusterRole(ClusterId, AdminRole.ConfigViewer);
|
||||
_canEdit = user.HasClusterRole(ClusterId, AdminRole.ConfigEditor);
|
||||
}
|
||||
_loaded = true;
|
||||
if (!_canView) return;
|
||||
|
||||
await LoadAsync();
|
||||
await ConnectHubAsync();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
@page "/clusters/{ClusterId}/draft/{GenerationId:long}/diff"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
|
||||
@inject GenerationService GenerationSvc
|
||||
|
||||
<ClusterAuthorizeView ClusterId="@ClusterId" MinRole="AdminRole.ConfigViewer">
|
||||
<NotAuthorized>
|
||||
<section class="panel notice rise" style="animation-delay:.02s">
|
||||
Viewing cluster <span class="mono">@ClusterId</span> requires a fleet-wide or
|
||||
cluster-scoped Admin role grant.
|
||||
</section>
|
||||
</NotAuthorized>
|
||||
<Authorized>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h1 class="page-title mb-0">Draft diff</h1>
|
||||
@@ -41,6 +51,9 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
</Authorized>
|
||||
</ClusterAuthorizeView>
|
||||
|
||||
@code {
|
||||
[Parameter] public string ClusterId { get; set; } = string.Empty;
|
||||
[Parameter] public long GenerationId { get; set; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
@page "/clusters/{ClusterId}/draft/{GenerationId:long}/import-equipment"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
|
||||
@inject DriverInstanceService DriverSvc
|
||||
@inject UnsService UnsSvc
|
||||
@inject EquipmentImportBatchService BatchSvc
|
||||
@inject NavigationManager Nav
|
||||
@inject AuthenticationStateProvider AuthProvider
|
||||
|
||||
<ClusterAuthorizeView ClusterId="@ClusterId" MinRole="AdminRole.ConfigEditor">
|
||||
<NotAuthorized>
|
||||
<section class="panel notice rise" style="animation-delay:.02s">
|
||||
Importing equipment into 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">Equipment CSV import</h1>
|
||||
@@ -142,6 +153,9 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
</Authorized>
|
||||
</ClusterAuthorizeView>
|
||||
|
||||
@code {
|
||||
[Parameter] public string ClusterId { get; set; } = string.Empty;
|
||||
[Parameter] public long GenerationId { get; set; }
|
||||
|
||||
Reference in New Issue
Block a user