fix(admin): enforce authentication on all Admin UI routes (Admin-001/002)

Admin-001: Routes.razor used a plain RouteView, so the page-level
[Authorize] attributes on 11 pages were inert — every page, including
mutating ones, was reachable fully unauthenticated.
Admin-002: several pages (e.g. NewCluster, which writes config rows)
carried no auth attribute at all.

- Routes.razor: RouteView → AuthorizeRouteView with NotAuthorized /
  Authorizing slots; add RedirectToLogin component.
- Program.cs: SetFallbackPolicy(RequireAuthenticatedUser) — secure by
  default for new pages/endpoints.
- Login.razor: [AllowAnonymous] so login stays reachable; login page,
  /auth/* endpoints and static assets remain anonymous.
- Add [Authorize] to the previously un-gated pages; NewCluster gated to
  the CanPublish (FleetAdmin) policy.

Regression tests in PageAuthorizationTests pin that anonymous requests
to protected/mutating routes are rejected and that login + static
assets stay anonymously reachable. Admin test suite: 210/210 pass.

Resolves code-review findings Admin-001 and Admin-002 (Critical).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-22 05:53:58 -04:00
parent 571066130b
commit 973730d0eb
16 changed files with 208 additions and 7 deletions
@@ -1,4 +1,5 @@
@page "/alarms/historian"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@using Microsoft.AspNetCore.Components.Web
@using ZB.MOM.WW.OtOpcUa.Admin.Services
@using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian
@@ -1,4 +1,5 @@
@page "/clusters"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@using ZB.MOM.WW.OtOpcUa.Admin.Services
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
@inject ClusterService ClusterSvc
@@ -1,4 +1,8 @@
@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 Microsoft.AspNetCore.Components.Web
@using ZB.MOM.WW.OtOpcUa.Admin.Services
@@ -1,4 +1,5 @@
@page "/drivers/focas/{InstanceId}"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@using ZB.MOM.WW.OtOpcUa.Admin.Services
@inject FocasDriverDetailService DetailSvc
@@ -1,4 +1,5 @@
@page "/fleet"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.EntityFrameworkCore
@using ZB.MOM.WW.OtOpcUa.Configuration
@@ -1,4 +1,5 @@
@page "/"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@using Microsoft.AspNetCore.Components.Web
@using ZB.MOM.WW.OtOpcUa.Admin.Services
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
@@ -1,4 +1,5 @@
@page "/hosts"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.SignalR.Client
@using Microsoft.EntityFrameworkCore
@@ -1,4 +1,7 @@
@page "/login"
@* The login page must stay anonymously reachable — otherwise the fallback
authorization policy (Admin-001) would lock operators out of the only way in. *@
@attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous]
@using System.Security.Claims
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication.Cookies
@@ -1,4 +1,5 @@
@page "/modbus/address-preview"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@using Microsoft.AspNetCore.Components.Web
@using ZB.MOM.WW.OtOpcUa.Driver.Modbus
@rendermode RenderMode.InteractiveServer
@@ -1,4 +1,5 @@
@page "/modbus/diagnostics/{DriverInstanceId}"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@using Microsoft.AspNetCore.Components.Web
@using ZB.MOM.WW.OtOpcUa.Admin.Services
@rendermode RenderMode.InteractiveServer