fix(admin): resolve Medium code-review finding (Admin-007)

NewCluster.razor and ClusterDetail.razor now resolve ClaimTypes.Name /
NameIdentifier from the cascaded AuthenticationState instead of hardcoding
"admin-ui" as the createdBy audit field. The operator principal is now
attributed correctly on every cluster-create and draft-create write path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-22 07:27:40 -04:00
parent a9cede8ed4
commit 08f000069c
3 changed files with 26 additions and 6 deletions

View File

@@ -1,5 +1,6 @@
@page "/clusters/{ClusterId}"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@using System.Security.Claims
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.SignalR.Client
@using ZB.MOM.WW.OtOpcUa.Admin.Hubs
@@ -200,7 +201,12 @@ else
_busy = true;
try
{
var draft = await GenerationSvc.CreateDraftAsync(ClusterId, createdBy: "admin-ui", CancellationToken.None);
// Admin-007: record the authenticated operator's name, not a static literal.
var user = AuthState is not null ? (await AuthState).User : null;
var operatorName = user?.FindFirstValue(ClaimTypes.Name)
?? user?.FindFirstValue(ClaimTypes.NameIdentifier)
?? "unknown";
var draft = await GenerationSvc.CreateDraftAsync(ClusterId, createdBy: operatorName, CancellationToken.None);
Nav.NavigateTo($"/clusters/{ClusterId}/draft/{draft.GenerationId}");
}
finally { _busy = false; }

View File

@@ -4,6 +4,8 @@
and its CreateAsync write path exploitable by any caller. *@
@attribute [Microsoft.AspNetCore.Authorization.Authorize(Policy = "CanPublish")]
@using System.ComponentModel.DataAnnotations
@using System.Security.Claims
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Web
@using ZB.MOM.WW.OtOpcUa.Admin.Services
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
@@ -73,6 +75,10 @@
public RedundancyMode RedundancyMode { get; set; } = RedundancyMode.None;
}
// Admin-007: record the authenticated operator's identity on every write path, not
// the static literal "admin-ui" which produced an unattributable audit trail.
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
private Input _input = new();
private bool _submitting;
private string? _error;
@@ -84,6 +90,14 @@
try
{
// Resolve the signed-in principal name. The page is [Authorize(Policy="CanPublish")]
// so AuthState will always be available with an authenticated user here; fall back to
// "unknown" only as a defensive last resort (should never happen in practice).
var user = AuthState is not null ? (await AuthState).User : null;
var operatorName = user?.FindFirstValue(ClaimTypes.Name)
?? user?.FindFirstValue(ClaimTypes.NameIdentifier)
?? "unknown";
var cluster = new ServerCluster
{
ClusterId = _input.ClusterId,
@@ -93,11 +107,11 @@
RedundancyMode = _input.RedundancyMode,
NodeCount = _input.RedundancyMode == RedundancyMode.None ? (byte)1 : (byte)2,
Enabled = true,
CreatedBy = "admin-ui",
CreatedBy = operatorName,
};
await ClusterSvc.CreateAsync(cluster, createdBy: "admin-ui", CancellationToken.None);
await GenerationSvc.CreateDraftAsync(cluster.ClusterId, createdBy: "admin-ui", CancellationToken.None);
await ClusterSvc.CreateAsync(cluster, createdBy: operatorName, CancellationToken.None);
await GenerationSvc.CreateDraftAsync(cluster.ClusterId, createdBy: operatorName, CancellationToken.None);
Nav.NavigateTo($"/clusters/{cluster.ClusterId}");
}