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:
@@ -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
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
@* Server-side redirect to the login page for an unauthenticated request.
|
||||
Used by AuthorizeRouteView's NotAuthorized slot (Admin-001). The current
|
||||
path is carried through as returnUrl so the operator lands back where
|
||||
they aimed after signing in. *@
|
||||
@inject NavigationManager Nav
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var returnUrl = Nav.ToBaseRelativePath(Nav.Uri);
|
||||
var target = string.IsNullOrEmpty(returnUrl) || returnUrl == "login"
|
||||
? "login"
|
||||
: $"login?returnUrl={Uri.EscapeDataString("/" + returnUrl)}";
|
||||
Nav.NavigateTo(target, forceLoad: true);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,30 @@
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using ZB.MOM.WW.OtOpcUa.Admin.Components.Layout
|
||||
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
|
||||
@* AuthorizeRouteView (not a plain RouteView) is what makes a page-level
|
||||
[Authorize] attribute actually enforced — with RouteView the attribute
|
||||
is inert (Admin-001). Unauthenticated users hit the NotAuthorized slot
|
||||
and are bounced to /login; the route is preserved as returnUrl. *@
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
||||
<NotAuthorized>
|
||||
@if (context.User.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
<RedirectToLogin/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p class="text-danger">You do not have permission to view this page.</p>
|
||||
</LayoutView>
|
||||
}
|
||||
</NotAuthorized>
|
||||
<Authorizing>
|
||||
<LayoutView Layout="@typeof(MainLayout)"><p>Authorizing…</p></LayoutView>
|
||||
</Authorizing>
|
||||
</AuthorizeRouteView>
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(MainLayout)"><p>Not found.</p></LayoutView>
|
||||
|
||||
@@ -28,9 +28,17 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
|
||||
o.ExpireTimeSpan = TimeSpan.FromHours(8);
|
||||
});
|
||||
|
||||
// Secure-by-default: the fallback policy requires an authenticated user for any
|
||||
// endpoint (and any routable page) that carries no explicit authorization metadata,
|
||||
// so a newly added page cannot accidentally ship anonymously reachable (Admin-001/002).
|
||||
// Pages/endpoints that must stay anonymous opt out with [AllowAnonymous] — the login
|
||||
// page, the /auth/* endpoints and static files all do.
|
||||
builder.Services.AddAuthorizationBuilder()
|
||||
.AddPolicy("CanEdit", p => p.RequireRole(AdminRoles.ConfigEditor, AdminRoles.FleetAdmin))
|
||||
.AddPolicy("CanPublish", p => p.RequireRole(AdminRoles.FleetAdmin));
|
||||
.AddPolicy("CanPublish", p => p.RequireRole(AdminRoles.FleetAdmin))
|
||||
.SetFallbackPolicy(new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder()
|
||||
.RequireAuthenticatedUser()
|
||||
.Build());
|
||||
|
||||
builder.Services.AddCascadingAuthenticationState();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user