Merge pull request (#145) - ACL + role-grant SignalR invalidation
This commit was merged in pull request #145.
This commit is contained in:
@@ -1,9 +1,13 @@
|
|||||||
|
@using Microsoft.AspNetCore.SignalR.Client
|
||||||
|
@using ZB.MOM.WW.OtOpcUa.Admin.Hubs
|
||||||
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
||||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
||||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
|
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
|
||||||
@using ZB.MOM.WW.OtOpcUa.Core.Authorization
|
@using ZB.MOM.WW.OtOpcUa.Core.Authorization
|
||||||
@inject NodeAclService AclSvc
|
@inject NodeAclService AclSvc
|
||||||
@inject PermissionProbeService ProbeSvc
|
@inject PermissionProbeService ProbeSvc
|
||||||
|
@inject NavigationManager Nav
|
||||||
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
<div class="d-flex justify-content-between mb-3">
|
<div class="d-flex justify-content-between mb-3">
|
||||||
<h4>Access-control grants</h4>
|
<h4>Access-control grants</h4>
|
||||||
@@ -205,6 +209,30 @@ else
|
|||||||
|
|
||||||
private static string? NullIfBlank(string s) => string.IsNullOrWhiteSpace(s) ? null : s;
|
private static string? NullIfBlank(string s) => string.IsNullOrWhiteSpace(s) ? null : s;
|
||||||
|
|
||||||
|
private HubConnection? _hub;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!firstRender || _hub is not null) return;
|
||||||
|
_hub = new HubConnectionBuilder()
|
||||||
|
.WithUrl(Nav.ToAbsoluteUri("/hubs/fleet-status"))
|
||||||
|
.WithAutomaticReconnect()
|
||||||
|
.Build();
|
||||||
|
_hub.On<NodeAclChangedMessage>("NodeAclChanged", async msg =>
|
||||||
|
{
|
||||||
|
if (msg.ClusterId != ClusterId || msg.GenerationId != GenerationId) return;
|
||||||
|
_acls = await AclSvc.ListAsync(GenerationId, CancellationToken.None);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
});
|
||||||
|
await _hub.StartAsync();
|
||||||
|
await _hub.SendAsync("SubscribeCluster", ClusterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_hub is not null) { await _hub.DisposeAsync(); _hub = null; }
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync() =>
|
protected override async Task OnParametersSetAsync() =>
|
||||||
_acls = await AclSvc.ListAsync(GenerationId, CancellationToken.None);
|
_acls = await AclSvc.ListAsync(GenerationId, CancellationToken.None);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
@page "/role-grants"
|
@page "/role-grants"
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using Microsoft.AspNetCore.SignalR.Client
|
||||||
|
@using ZB.MOM.WW.OtOpcUa.Admin.Hubs
|
||||||
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
||||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
||||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
|
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
|
||||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Services
|
@using ZB.MOM.WW.OtOpcUa.Configuration.Services
|
||||||
@inject ILdapGroupRoleMappingService RoleSvc
|
@inject ILdapGroupRoleMappingService RoleSvc
|
||||||
@inject ClusterService ClusterSvc
|
@inject ClusterService ClusterSvc
|
||||||
|
@inject AclChangeNotifier Notifier
|
||||||
|
@inject NavigationManager Nav
|
||||||
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
<h1 class="mb-4">LDAP group → Admin role grants</h1>
|
<h1 class="mb-4">LDAP group → Admin role grants</h1>
|
||||||
|
|
||||||
@@ -147,6 +153,7 @@ else
|
|||||||
Notes = string.IsNullOrWhiteSpace(_notes) ? null : _notes,
|
Notes = string.IsNullOrWhiteSpace(_notes) ? null : _notes,
|
||||||
};
|
};
|
||||||
await RoleSvc.CreateAsync(row, CancellationToken.None);
|
await RoleSvc.CreateAsync(row, CancellationToken.None);
|
||||||
|
await Notifier.NotifyRoleGrantsChangedAsync(CancellationToken.None);
|
||||||
_showForm = false;
|
_showForm = false;
|
||||||
await ReloadAsync();
|
await ReloadAsync();
|
||||||
}
|
}
|
||||||
@@ -156,6 +163,30 @@ else
|
|||||||
private async Task DeleteAsync(Guid id)
|
private async Task DeleteAsync(Guid id)
|
||||||
{
|
{
|
||||||
await RoleSvc.DeleteAsync(id, CancellationToken.None);
|
await RoleSvc.DeleteAsync(id, CancellationToken.None);
|
||||||
|
await Notifier.NotifyRoleGrantsChangedAsync(CancellationToken.None);
|
||||||
await ReloadAsync();
|
await ReloadAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HubConnection? _hub;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!firstRender || _hub is not null) return;
|
||||||
|
_hub = new HubConnectionBuilder()
|
||||||
|
.WithUrl(Nav.ToAbsoluteUri("/hubs/fleet-status"))
|
||||||
|
.WithAutomaticReconnect()
|
||||||
|
.Build();
|
||||||
|
_hub.On<RoleGrantsChangedMessage>("RoleGrantsChanged", async _ =>
|
||||||
|
{
|
||||||
|
await ReloadAsync();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
});
|
||||||
|
await _hub.StartAsync();
|
||||||
|
await _hub.SendAsync("SubscribeFleet");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_hub is not null) { await _hub.DisposeAsync(); _hub = null; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ builder.Services.AddScoped<NamespaceService>();
|
|||||||
builder.Services.AddScoped<DriverInstanceService>();
|
builder.Services.AddScoped<DriverInstanceService>();
|
||||||
builder.Services.AddScoped<NodeAclService>();
|
builder.Services.AddScoped<NodeAclService>();
|
||||||
builder.Services.AddScoped<PermissionProbeService>();
|
builder.Services.AddScoped<PermissionProbeService>();
|
||||||
|
builder.Services.AddScoped<AclChangeNotifier>();
|
||||||
builder.Services.AddScoped<ReservationService>();
|
builder.Services.AddScoped<ReservationService>();
|
||||||
builder.Services.AddScoped<DraftValidationService>();
|
builder.Services.AddScoped<DraftValidationService>();
|
||||||
builder.Services.AddScoped<AuditLogService>();
|
builder.Services.AddScoped<AuditLogService>();
|
||||||
|
|||||||
49
src/ZB.MOM.WW.OtOpcUa.Admin/Services/AclChangeNotifier.cs
Normal file
49
src/ZB.MOM.WW.OtOpcUa.Admin/Services/AclChangeNotifier.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Admin.Hubs;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Admin.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thin SignalR push helper for ACL + role-grant invalidation — slice 2 of task #196.
|
||||||
|
/// Lets the Admin services + razor pages invalidate connected peers' views without each
|
||||||
|
/// one having to know the hub wiring. Two message kinds: <c>NodeAclChanged</c> (cluster-scoped)
|
||||||
|
/// and <c>RoleGrantsChanged</c> (fleet-wide — role mappings cross cluster boundaries).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Intentionally fire-and-forget — a failed hub send doesn't rollback the DB write that
|
||||||
|
/// triggered it. Worst-case an operator sees stale data until their next poll or manual
|
||||||
|
/// refresh; better than a transient hub blip blocking the authoritative write path.
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class AclChangeNotifier(IHubContext<FleetStatusHub> fleetHub, ILogger<AclChangeNotifier> logger)
|
||||||
|
{
|
||||||
|
public async Task NotifyNodeAclChangedAsync(string clusterId, long generationId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var msg = new NodeAclChangedMessage(ClusterId: clusterId, GenerationId: generationId, ObservedAtUtc: DateTime.UtcNow);
|
||||||
|
await fleetHub.Clients.Group(FleetStatusHub.GroupName(clusterId))
|
||||||
|
.SendAsync("NodeAclChanged", msg, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "NodeAclChanged push failed for cluster {ClusterId} gen {GenerationId}", clusterId, generationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task NotifyRoleGrantsChangedAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var msg = new RoleGrantsChangedMessage(ObservedAtUtc: DateTime.UtcNow);
|
||||||
|
await fleetHub.Clients.Group(FleetStatusHub.FleetGroup)
|
||||||
|
.SendAsync("RoleGrantsChanged", msg, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "RoleGrantsChanged push failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record NodeAclChangedMessage(string ClusterId, long GenerationId, DateTime ObservedAtUtc);
|
||||||
|
public sealed record RoleGrantsChangedMessage(DateTime ObservedAtUtc);
|
||||||
@@ -5,7 +5,7 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|||||||
|
|
||||||
namespace ZB.MOM.WW.OtOpcUa.Admin.Services;
|
namespace ZB.MOM.WW.OtOpcUa.Admin.Services;
|
||||||
|
|
||||||
public sealed class NodeAclService(OtOpcUaConfigDbContext db)
|
public sealed class NodeAclService(OtOpcUaConfigDbContext db, AclChangeNotifier? notifier = null)
|
||||||
{
|
{
|
||||||
public Task<List<NodeAcl>> ListAsync(long generationId, CancellationToken ct) =>
|
public Task<List<NodeAcl>> ListAsync(long generationId, CancellationToken ct) =>
|
||||||
db.NodeAcls.AsNoTracking()
|
db.NodeAcls.AsNoTracking()
|
||||||
@@ -31,6 +31,10 @@ public sealed class NodeAclService(OtOpcUaConfigDbContext db)
|
|||||||
};
|
};
|
||||||
db.NodeAcls.Add(acl);
|
db.NodeAcls.Add(acl);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
if (notifier is not null)
|
||||||
|
await notifier.NotifyNodeAclChangedAsync(clusterId, draftId, ct);
|
||||||
|
|
||||||
return acl;
|
return acl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,5 +44,8 @@ public sealed class NodeAclService(OtOpcUaConfigDbContext db)
|
|||||||
if (row is null) return;
|
if (row is null) return;
|
||||||
db.NodeAcls.Remove(row);
|
db.NodeAcls.Remove(row);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
if (notifier is not null)
|
||||||
|
await notifier.NotifyNodeAclChangedAsync(row.ClusterId, row.GenerationId, ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user