@page "/clusters/new" @* Cluster creation is a FleetAdmin operation per admin-ui.md "Add a new cluster" — CanPublish gates it (Admin-002). Without this attribute the page was reachable 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 @using ZB.MOM.WW.OtOpcUa.Configuration.Enums @rendermode RenderMode.InteractiveServer @inject ClusterService ClusterSvc @inject GenerationService GenerationSvc @inject NavigationManager Nav

New cluster

Stable internal ID. Lowercase alphanumeric + hyphens; ≤ 64 chars.
@if (!string.IsNullOrEmpty(_error)) {
@_error
}
Cancel
@code { private sealed class Input { [Required, RegularExpression("^[a-z0-9-]{1,64}$", ErrorMessage = "Lowercase alphanumeric + hyphens only")] public string ClusterId { get; set; } = string.Empty; [Required, StringLength(128)] public string Name { get; set; } = string.Empty; [StringLength(32)] public string Enterprise { get; set; } = "zb"; [StringLength(32)] public string Site { get; set; } = "dev"; 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? AuthState { get; set; } private Input _input = new(); private bool _submitting; private string? _error; private async Task CreateAsync() { _submitting = true; _error = null; 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, Name = _input.Name, Enterprise = _input.Enterprise, Site = _input.Site, RedundancyMode = _input.RedundancyMode, NodeCount = _input.RedundancyMode == RedundancyMode.None ? (byte)1 : (byte)2, Enabled = true, CreatedBy = operatorName, }; await ClusterSvc.CreateAsync(cluster, createdBy: operatorName, CancellationToken.None); await GenerationSvc.CreateDraftAsync(cluster.ClusterId, createdBy: operatorName, CancellationToken.None); Nav.NavigateTo($"/clusters/{cluster.ClusterId}"); } catch (Exception ex) { _error = ex.Message; } finally { _submitting = false; } } }