fix(adminui): gate every page with named policies — ConfigEditor on the mutating surface (R2-05, 04/C-1)
This commit is contained in:
@@ -6,13 +6,13 @@
|
||||
{ "id": "T2", "subject": "Policy-semantics unit test AdminUiPoliciesTests (RED: ConfigEditor/AuthenticatedRead unregistered)", "status": "completed", "blockedBy": ["T1"] },
|
||||
{ "id": "T3", "subject": "Reflection PageAuthorizationGuardTests (RED: must list ~35 ungated/mis-idiom pages)", "status": "completed", "blockedBy": ["T1"] },
|
||||
{ "id": "T4", "subject": "Register ConfigEditor + AuthenticatedRead policies; switch existing AddPolicy literals to constants (GREEN for T2)", "status": "completed", "blockedBy": ["T2"] },
|
||||
{ "id": "T5", "subject": "_Imports.razor usings (Authorization + Security.Auth) + de-dupe Deployments/Alerts in-file @using", "status": "pending", "blockedBy": ["T1"] },
|
||||
{ "id": "T6", "subject": "Gate UNS pages with ConfigEditor (GlobalUns, EquipmentPage)", "status": "pending", "blockedBy": ["T4", "T5"] },
|
||||
{ "id": "T7", "subject": "Gate cluster editors with ConfigEditor (NewCluster, ClusterEdit, NodeEdit, NamespaceEdit, AclEdit)", "status": "pending", "blockedBy": ["T4", "T5"] },
|
||||
{ "id": "T8", "subject": "Gate driver pages + routers with ConfigEditor (8 driver pages, DriverTypePicker, DriverEditRouter)", "status": "pending", "blockedBy": ["T4", "T5"] },
|
||||
{ "id": "T9", "subject": "Converge Deployments/Scripts/ScriptEdit from Roles-string to ConfigEditor policy", "status": "pending", "blockedBy": ["T4", "T5"] },
|
||||
{ "id": "T10", "subject": "Read pages to AuthenticatedRead (16 incl. Home insert) + RoleGrants to FleetAdmin constant", "status": "pending", "blockedBy": ["T4", "T5"] },
|
||||
{ "id": "T11", "subject": "Guard GREEN checkpoint + full AdminUI.Tests; commit the gate", "status": "pending", "blockedBy": ["T3", "T6", "T7", "T8", "T9", "T10"] },
|
||||
{ "id": "T5", "subject": "_Imports.razor usings (Authorization + Security.Auth) + de-dupe Deployments/Alerts in-file @using", "status": "completed", "blockedBy": ["T1"] },
|
||||
{ "id": "T6", "subject": "Gate UNS pages with ConfigEditor (GlobalUns, EquipmentPage)", "status": "completed", "blockedBy": ["T4", "T5"] },
|
||||
{ "id": "T7", "subject": "Gate cluster editors with ConfigEditor (NewCluster, ClusterEdit, NodeEdit, NamespaceEdit, AclEdit)", "status": "completed", "blockedBy": ["T4", "T5"] },
|
||||
{ "id": "T8", "subject": "Gate driver pages + routers with ConfigEditor (8 driver pages, DriverTypePicker, DriverEditRouter)", "status": "completed", "blockedBy": ["T4", "T5"] },
|
||||
{ "id": "T9", "subject": "Converge Deployments/Scripts/ScriptEdit from Roles-string to ConfigEditor policy", "status": "completed", "blockedBy": ["T4", "T5"] },
|
||||
{ "id": "T10", "subject": "Read pages to AuthenticatedRead (16 incl. Home insert) + RoleGrants to FleetAdmin constant", "status": "completed", "blockedBy": ["T4", "T5"] },
|
||||
{ "id": "T11", "subject": "Guard GREEN checkpoint + full AdminUI.Tests; commit the gate", "status": "completed", "blockedBy": ["T3", "T6", "T7", "T8", "T9", "T10"] },
|
||||
{ "id": "T12", "subject": "Converge non-page literals to constants (ScriptAnalysisEndpoints, imperative DriverOperator x4, Certificates FleetAdmin x2)", "status": "pending", "blockedBy": ["T11"] },
|
||||
{ "id": "T13", "subject": "Docs: CLAUDE.md ScriptAnalysis gate sentence, docs/security.md, STATUS.md R2-05 row", "status": "pending", "blockedBy": ["T11"] },
|
||||
{ "id": "T14", "subject": "Full-solution test sweep (dotnet test ZB.MOM.WW.OtOpcUa.slnx)", "status": "pending", "blockedBy": ["T12", "T13"] },
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
grants in favour of fleet-wide LDAP-group → role mapping (Q4 of the AdminUI rebuild plan), so
|
||||
this version only shows identity + the resolved fleet roles + raw LDAP groups for
|
||||
troubleshooting. *@
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
|
||||
@using System.Security.Claims
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/alarms-historian"
|
||||
@* Live status of the local node's IAlarmHistorianSink (queue depth, drain state) via the
|
||||
HistorianAdapterActor.GetStatus query landed in F11. *@
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Akka.Actor
|
||||
@using Akka.Hosting
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
@* Live alarm tail via SignalR. Subscribes to /hubs/alerts and shows the most-recent
|
||||
AlarmTransitionEvent entries published by ScriptedAlarmActor (Runtime/ScriptedAlarms)
|
||||
and the AB CIP ALMD bridge. *@
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs
|
||||
@using ZB.MOM.WW.OtOpcUa.Commons.Interfaces
|
||||
@using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@page "/certificates"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using System.Security.Cryptography.X509Certificates
|
||||
@using Microsoft.Extensions.Configuration
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/clusters/{ClusterId}/acls/new"
|
||||
@page "/clusters/{ClusterId}/acls/{NodeAclId}"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@page "/clusters/{ClusterId}/acls"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@page "/clusters/{ClusterId}/audit"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@page "/clusters/{ClusterId}/drivers"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/clusters/{ClusterId}/edit"
|
||||
@* Edit page for an existing ServerCluster. The /clusters/new route lives in NewCluster.razor;
|
||||
this page handles only the update case so the form can disable ClusterId (immutable). *@
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@page "/clusters/{ClusterId}/namespaces"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@page "/clusters/{ClusterId}"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@page "/clusters/{ClusterId}/redundancy"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@page "/clusters"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@page "/clusters/{ClusterId}/drivers/new/abcip"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@page "/clusters/{ClusterId}/drivers/new/ablegacy"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
via <DynamicComponent> using _componentMap. Shows an error panel when the driver type has
|
||||
no registered typed page. *@
|
||||
@page "/clusters/{ClusterId}/drivers/{DriverInstanceId}"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
@* Driver type picker — presents a card grid of registered driver types and links to the
|
||||
per-type new-driver creation page (/clusters/{ClusterId}/drivers/new/{slug}). *@
|
||||
@page "/clusters/{ClusterId}/drivers/new"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0">New driver · <span class="mono">@ClusterId</span></h4>
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@page "/clusters/{ClusterId}/drivers/new/focas"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@page "/clusters/{ClusterId}/drivers/new/galaxy"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@page "/clusters/{ClusterId}/drivers/new/modbustcp"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@page "/clusters/{ClusterId}/drivers/new/opcuaclient"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@page "/clusters/{ClusterId}/drivers/new/s7"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@page "/clusters/{ClusterId}/drivers/new/twincat"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@* Live-edit form pattern — one page handles both create (NamespaceId is null) and update.
|
||||
RowVersion is preserved across post-back so EF Core enforces last-write-wins; concurrency
|
||||
conflicts surface as a toast and reload the current row. *@
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@page "/clusters/new"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@page "/clusters/{ClusterId}/nodes/{NodeId}"
|
||||
@* ClusterNode CRUD. ApplicationUri is fleet-wide unique — the EF unique index enforces this
|
||||
at SaveChanges. ServiceLevelBase defaults: 200 primary, 150 secondary. *@
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@page "/deployments"
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using ZB.MOM.WW.OtOpcUa.Commons.Interfaces
|
||||
@@ -9,7 +8,7 @@
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
|
||||
@using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations
|
||||
|
||||
@attribute [Authorize(Roles = "Administrator,Designer")]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
|
||||
@inject IDbContextFactory<OtOpcUaConfigDbContext> DbFactory
|
||||
@inject IAdminOperationsClient AdminOps
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
progress row owned by each DriverHostActor) and projects the most-recent row per node. The
|
||||
Akka cluster topology comes from IClusterRoleInfo so we can show nodes that haven't applied
|
||||
anything yet alongside nodes that have. *@
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using ZB.MOM.WW.OtOpcUa.Commons.Interfaces
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/"
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
|
||||
|
||||
<PageTitle>OtOpcUa</PageTitle>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
cluster. The health feed (DriverHealthChanged) carries no per-Akka-member identity, so the rows
|
||||
are cluster-scoped (keyed per driver instance across the cluster, not per member). The section
|
||||
reads the in-process driver-health snapshot store directly + reloads its config from the ConfigDB. *@
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Akka.Actor
|
||||
@using Akka.Cluster
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@page "/reservations"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@page "/role-grants"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize(Policy = "FleetAdmin")]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.FleetAdmin)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.Extensions.Options
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@page "/scripts/{ScriptId}"
|
||||
@* Script CRUD. SourceHash is computed automatically from SourceCode on save so the
|
||||
integrity check in v2's deployment pipeline doesn't require operator action. *@
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize(Roles = "Administrator,Designer")]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/script-log"
|
||||
@* Live script-log tail via SignalR. Subscribes to /hubs/script-log and shows entries from
|
||||
VirtualTagActor / ScriptedAlarmActor script execution. Engine emit lands with F8 + F9. *@
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs
|
||||
@using ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@page "/scripts"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize(Roles = "Administrator,Designer")]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
the shell + the Details tab (lifted from EquipmentModal's EditForm) + the create→edit redirect; the
|
||||
Tags / Virtual Tags / Alarms tabs render placeholders wired in later tasks. On a successful create the
|
||||
page redirects to /uns/equipment/{newId} so the other tabs (disabled while new) become available. *@
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@page "/uns"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@attribute [Authorize(Policy = AdminUiPolicies.ConfigEditor)]
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Uns
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@@ -7,4 +8,5 @@
|
||||
@using Microsoft.JSInterop
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Layout
|
||||
@using ZB.MOM.WW.OtOpcUa.Security.Auth
|
||||
@using ZB.MOM.WW.Theme
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Uns;
|
||||
using ZB.MOM.WW.OtOpcUa.Security.Auth;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// Reflection guard over every routable AdminUI page's class-level authorization metadata.
|
||||
/// This is the substitute for bUnit (the repo has no bUnit; razor <c>@attribute</c>/<c>@page</c>
|
||||
/// compile to class-level attributes, so a metadata scan is authoritative and needs no rendering).
|
||||
/// It is the anti-drift anchor: a new page fails until the author consciously classifies it.
|
||||
/// Guard universe = <c>typeof(Routes).Assembly</c> — the exact assembly Routes.razor hands
|
||||
/// <c><Router AppAssembly=…></c>, so the guard sees precisely the router's page set.
|
||||
/// </summary>
|
||||
public class PageAuthorizationGuardTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Target classification: every routable page → its required <see cref="AdminUiPolicies"/> policy.
|
||||
/// This is the 38-page census from the R2-05 plan. <see cref="AnonymousPages"/> holds the sole
|
||||
/// <c>[AllowAnonymous]</c> page. Together they must exactly cover the router's page set.
|
||||
/// </summary>
|
||||
private static readonly IReadOnlyDictionary<Type, string> ExpectedPolicy = new Dictionary<Type, string>
|
||||
{
|
||||
// ── ConfigEditor (20): the config-authoring surface (incl. live ResilienceConfig) ──
|
||||
[typeof(GlobalUns)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(EquipmentPage)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(NewCluster)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(ClusterEdit)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(NodeEdit)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(NamespaceEdit)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(AclEdit)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(DriverTypePicker)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(DriverEditRouter)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(ModbusDriverPage)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(S7DriverPage)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(AbCipDriverPage)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(AbLegacyDriverPage)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(TwinCATDriverPage)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(FocasDriverPage)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(OpcUaClientDriverPage)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(GalaxyDriverPage)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(Deployments)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(Scripts)] = AdminUiPolicies.ConfigEditor,
|
||||
[typeof(ScriptEdit)] = AdminUiPolicies.ConfigEditor,
|
||||
|
||||
// ── FleetAdmin (1) ──
|
||||
[typeof(RoleGrants)] = AdminUiPolicies.FleetAdmin,
|
||||
|
||||
// ── AuthenticatedRead (16): dashboards, lists, live tails (read-only) ──
|
||||
[typeof(Home)] = AdminUiPolicies.AuthenticatedRead,
|
||||
[typeof(Fleet)] = AdminUiPolicies.AuthenticatedRead,
|
||||
[typeof(global::ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Hosts)] = AdminUiPolicies.AuthenticatedRead,
|
||||
[typeof(Alerts)] = AdminUiPolicies.AuthenticatedRead,
|
||||
[typeof(ScriptLog)] = AdminUiPolicies.AuthenticatedRead,
|
||||
[typeof(AlarmsHistorian)] = AdminUiPolicies.AuthenticatedRead,
|
||||
[typeof(Account)] = AdminUiPolicies.AuthenticatedRead,
|
||||
[typeof(global::ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Certificates)] = AdminUiPolicies.AuthenticatedRead,
|
||||
[typeof(Reservations)] = AdminUiPolicies.AuthenticatedRead,
|
||||
[typeof(ClustersList)] = AdminUiPolicies.AuthenticatedRead,
|
||||
[typeof(ClusterOverview)] = AdminUiPolicies.AuthenticatedRead,
|
||||
[typeof(ClusterAudit)] = AdminUiPolicies.AuthenticatedRead,
|
||||
[typeof(ClusterAcls)] = AdminUiPolicies.AuthenticatedRead,
|
||||
[typeof(ClusterNamespaces)] = AdminUiPolicies.AuthenticatedRead,
|
||||
[typeof(ClusterDrivers)] = AdminUiPolicies.AuthenticatedRead,
|
||||
[typeof(ClusterRedundancy)] = AdminUiPolicies.AuthenticatedRead,
|
||||
};
|
||||
|
||||
/// <summary>The only page that must remain anonymously reachable (Admin-001 — the login form).</summary>
|
||||
private static readonly IReadOnlySet<Type> AnonymousPages = new HashSet<Type> { typeof(Login) };
|
||||
|
||||
private static readonly string[] KnownPolicyNames =
|
||||
[
|
||||
AdminUiPolicies.FleetAdmin,
|
||||
AdminUiPolicies.DriverOperator,
|
||||
AdminUiPolicies.ConfigEditor,
|
||||
AdminUiPolicies.AuthenticatedRead,
|
||||
];
|
||||
|
||||
private static IReadOnlyList<Type> RoutablePages() =>
|
||||
[.. typeof(Routes).Assembly.GetTypes()
|
||||
.Where(t => t.GetCustomAttributes<RouteAttribute>(inherit: false).Any())
|
||||
.OrderBy(t => t.FullName, StringComparer.Ordinal)];
|
||||
|
||||
private static string RouteOf(Type t) =>
|
||||
string.Join(", ", t.GetCustomAttributes<RouteAttribute>(inherit: false).Select(r => r.Template));
|
||||
|
||||
private static AuthorizeAttribute? Authorize(Type t) =>
|
||||
t.GetCustomAttributes<AuthorizeAttribute>(inherit: false).SingleOrDefault();
|
||||
|
||||
private static bool IsAnonymous(Type t) =>
|
||||
t.GetCustomAttributes<AllowAnonymousAttribute>(inherit: false).Any();
|
||||
|
||||
/// <summary>
|
||||
/// Rule 1 — no bare gates. Every routable page must carry either <c>[AllowAnonymous]</c> or an
|
||||
/// <c>[Authorize]</c> with a non-empty Policy and null Roles. Bans bare <c>[Authorize]</c>, the
|
||||
/// <c>Roles="…"</c> idiom, and attribute-less pages.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EveryRoutablePage_carriesAnExplicitNamedPolicyOrAllowAnonymous()
|
||||
{
|
||||
var offenders = new List<string>();
|
||||
foreach (var page in RoutablePages())
|
||||
{
|
||||
if (IsAnonymous(page))
|
||||
continue;
|
||||
|
||||
var auth = Authorize(page);
|
||||
if (auth is null)
|
||||
{
|
||||
offenders.Add($"{page.FullName} ({RouteOf(page)}): no [Authorize]/[AllowAnonymous] attribute");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(auth.Policy))
|
||||
offenders.Add($"{page.FullName} ({RouteOf(page)}): bare [Authorize] — no Policy");
|
||||
if (!string.IsNullOrEmpty(auth.Roles))
|
||||
offenders.Add($"{page.FullName} ({RouteOf(page)}): uses Roles=\"{auth.Roles}\" idiom — convert to a named policy");
|
||||
}
|
||||
|
||||
offenders.ShouldBeEmpty(
|
||||
"Pages must carry an explicit named policy (or [AllowAnonymous]). Offenders:\n" +
|
||||
string.Join("\n", offenders));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rule 2 — exhaustive classification. The router's page set must equal the expected map plus the
|
||||
/// anonymous allowlist, and every page's actual policy must match the expected one. A brand-new page
|
||||
/// fails here until the author classifies it.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EveryRoutablePage_isClassifiedAndMatchesExpectedPolicy()
|
||||
{
|
||||
var routable = RoutablePages().ToHashSet();
|
||||
var classified = new HashSet<Type>(ExpectedPolicy.Keys);
|
||||
classified.UnionWith(AnonymousPages);
|
||||
|
||||
var unclassified = routable.Except(classified)
|
||||
.Select(t => $"{t.FullName} ({RouteOf(t)})").OrderBy(s => s).ToList();
|
||||
unclassified.ShouldBeEmpty(
|
||||
"Routable pages missing from the guard's expected map — classify each in ExpectedPolicy or AnonymousPages:\n" +
|
||||
string.Join("\n", unclassified));
|
||||
|
||||
var stale = classified.Except(routable)
|
||||
.Select(t => t.FullName ?? t.Name).OrderBy(s => s).ToList();
|
||||
stale.ShouldBeEmpty(
|
||||
"Guard expected-map entries that are no longer routable pages — remove them:\n" +
|
||||
string.Join("\n", stale));
|
||||
|
||||
var mismatches = new List<string>();
|
||||
foreach (var (page, expected) in ExpectedPolicy)
|
||||
{
|
||||
var actual = Authorize(page)?.Policy;
|
||||
if (actual != expected)
|
||||
mismatches.Add($"{page.FullName} ({RouteOf(page)}): expected policy '{expected}', got '{actual ?? "<none>"}'");
|
||||
}
|
||||
|
||||
mismatches.ShouldBeEmpty(
|
||||
"Pages whose actual policy differs from the expected classification:\n" +
|
||||
string.Join("\n", mismatches));
|
||||
}
|
||||
|
||||
/// <summary>Rule 3 — every Policy value in use is one of the four known <see cref="AdminUiPolicies"/> constants.</summary>
|
||||
[Fact]
|
||||
public void EveryPolicyNameInUse_isAKnownAdminUiPolicy()
|
||||
{
|
||||
var unknown = new List<string>();
|
||||
foreach (var page in RoutablePages())
|
||||
{
|
||||
var policy = Authorize(page)?.Policy;
|
||||
if (!string.IsNullOrEmpty(policy) && !KnownPolicyNames.Contains(policy))
|
||||
unknown.Add($"{page.FullName} ({RouteOf(page)}): unknown policy '{policy}'");
|
||||
}
|
||||
|
||||
unknown.ShouldBeEmpty(
|
||||
"Pages referencing a policy name not defined in AdminUiPolicies:\n" +
|
||||
string.Join("\n", unknown));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user