From 6dea84cd283b0eced3ff617e41daf06a9fbe5018 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 20 May 2026 21:09:42 -0400 Subject: [PATCH] feat(security): OperationalAudit + AuditExport permissions for Audit Log surface (#23 M7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundle G (#23 M7-T15): replace the temporary Admin-only gate on the Audit Log surface with two new permission policies — OperationalAudit (read) and AuditExport (bulk-export) — so the read path and the forensic-export path can be delegated independently. ScadaLink.Security - AuthorizationPolicies: add OperationalAudit + AuditExport policy constants; register them via RequireClaim with an explicit role allow-list (OperationalAuditRoles, AuditExportRoles) so the role-to-permission mapping is documented in one place. - Default mapping: Admin and Audit roles grant both policies; AuditReadOnly grants OperationalAudit only (read access without bulk export); Design and Deployment grant neither. ScadaLink.CentralUI - AuditLogPage: switch the page-level [Authorize] to the OperationalAudit policy and wrap the Export-CSV button in an AuthorizeView gated on AuditExport so an OperationalAudit-only operator still sees the page + filters but cannot trigger the CSV pull. - ConfigurationAuditLog: switch from RequireAdmin to OperationalAudit so both pages under the Audit nav group share the same gate. - NavMenu: the Audit nav group now gates on OperationalAudit so the section header + both child links match the per-page policies. - AuditExportEndpoints: switch RequireAuthorization from RequireAdmin to AuditExport — this is the authoritative gate; the AuthorizeView on the button is just a UX affordance. Tests - New AuditLogPagePermissionTests covers the 5 brief-mandated cases plus defence-in-depth for Admin-alone and AuditReadOnly users on the endpoint. - SecurityTests: add policy-level coverage for the new role→permission matrix (Theory rows pin every role/policy combination). - AuditExportEndpointsTests: switch to AddScadaLinkAuthorization() so the test host exercises the real production wiring under the new gate. - AuditLogPageScaffoldTests: wrap the page render in a CascadingAuthenticationState so the new in-page AuthorizeView resolves the principal. --- .../Audit/AuditExportEndpoints.cs | 11 +- .../Components/Layout/NavMenu.razor | 14 +- .../Components/Pages/Audit/AuditLogPage.razor | 33 +- .../Pages/Audit/ConfigurationAuditLog.razor | 2 +- .../AuthorizationPolicies.cs | 99 ++++++ .../Audit/AuditExportEndpointsTests.cs | 22 +- .../Pages/AuditLogPagePermissionTests.cs | 323 ++++++++++++++++++ .../Pages/AuditLogPageScaffoldTests.cs | 14 +- .../ScadaLink.Security.Tests/SecurityTests.cs | 53 +++ 9 files changed, 538 insertions(+), 33 deletions(-) create mode 100644 tests/ScadaLink.CentralUI.Tests/Pages/AuditLogPagePermissionTests.cs diff --git a/src/ScadaLink.CentralUI/Audit/AuditExportEndpoints.cs b/src/ScadaLink.CentralUI/Audit/AuditExportEndpoints.cs index 66ed746..fdf34f6 100644 --- a/src/ScadaLink.CentralUI/Audit/AuditExportEndpoints.cs +++ b/src/ScadaLink.CentralUI/Audit/AuditExportEndpoints.cs @@ -23,9 +23,12 @@ namespace ScadaLink.CentralUI.Audit; /// /// /// -/// The route is admin-gated to mirror the NavMenu (RequireAdmin wraps -/// the Audit section). The query-string parser silently drops unrecognised -/// values to match the page-level parser in +/// The route is gated on the +/// policy (#23 M7-T15 / Bundle G) so only roles with the bulk-export +/// permission can pull a CSV — the page-level +/// gate is read-only +/// and intentionally narrower. The query-string parser silently drops +/// unrecognised values to match the page-level parser in /// AuditLogPage.ApplyQueryStringFilters — an unknown enum value yields /// the same "no constraint" outcome rather than a 400. /// @@ -43,7 +46,7 @@ public static class AuditExportEndpoints public static IEndpointRouteBuilder MapAuditExportEndpoints(this IEndpointRouteBuilder endpoints) { endpoints.MapGet("/api/centralui/audit/export", HandleExportAsync) - .RequireAuthorization(AuthorizationPolicies.RequireAdmin); + .RequireAuthorization(AuthorizationPolicies.AuditExport); return endpoints; } diff --git a/src/ScadaLink.CentralUI/Components/Layout/NavMenu.razor b/src/ScadaLink.CentralUI/Components/Layout/NavMenu.razor index 798d306..1c05b7e 100644 --- a/src/ScadaLink.CentralUI/Components/Layout/NavMenu.razor +++ b/src/ScadaLink.CentralUI/Components/Layout/NavMenu.razor @@ -108,11 +108,15 @@ - @* Audit — Admin only. Hosts the new Audit Log page (#23 M7) - and the renamed Configuration Audit Log (IAuditService - config-change viewer). Both items are Admin-gated, so - the section header sits inside the same policy block. *@ - + @* Audit — gated on the OperationalAudit policy (#23 M7-T15 + / Bundle G). Hosts the new Audit Log page (#23 M7) and + the renamed Configuration Audit Log (IAuditService + config-change viewer). Both items share the same gate, + so the section header sits inside the same policy block: + a non-audit user does not even see the heading. + OperationalAudit is satisfied by the Admin, Audit, and + AuditReadOnly roles. *@ +