feat(security): OperationalAudit + AuditExport permissions for Audit Log surface (#23 M7)
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.
This commit is contained in:
@@ -1077,6 +1077,59 @@ public class AuthorizationPolicyTests
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.RequireAdmin, principal));
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.RequireDesign, principal));
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.RequireDeployment, principal));
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.OperationalAudit, principal));
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.AuditExport, principal));
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Audit Log #23 — OperationalAudit + AuditExport policies (M7-T15).
|
||||
// Default mapping (see AuthorizationPolicies XML doc):
|
||||
// Admin → OperationalAudit + AuditExport
|
||||
// Audit → OperationalAudit + AuditExport
|
||||
// AuditReadOnly → OperationalAudit only
|
||||
// Design → neither
|
||||
// Deployment → neither
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
[InlineData("Admin")]
|
||||
[InlineData("Audit")]
|
||||
[InlineData("AuditReadOnly")]
|
||||
public async Task OperationalAuditPolicy_GrantedRoles_Succeed(string role)
|
||||
{
|
||||
var principal = CreatePrincipal(new[] { role });
|
||||
Assert.True(await EvaluatePolicy(AuthorizationPolicies.OperationalAudit, principal));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Design")]
|
||||
[InlineData("Deployment")]
|
||||
public async Task OperationalAuditPolicy_UngrantedRoles_Fail(string role)
|
||||
{
|
||||
var principal = CreatePrincipal(new[] { role });
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.OperationalAudit, principal));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Admin")]
|
||||
[InlineData("Audit")]
|
||||
public async Task AuditExportPolicy_GrantedRoles_Succeed(string role)
|
||||
{
|
||||
var principal = CreatePrincipal(new[] { role });
|
||||
Assert.True(await EvaluatePolicy(AuthorizationPolicies.AuditExport, principal));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("AuditReadOnly")]
|
||||
[InlineData("Design")]
|
||||
[InlineData("Deployment")]
|
||||
public async Task AuditExportPolicy_UngrantedRoles_Fail(string role)
|
||||
{
|
||||
// AuditReadOnly is the load-bearing case: it grants OperationalAudit
|
||||
// (read) but NOT AuditExport (bulk export) — the split that lets a
|
||||
// triage operator drill in without exfiltrating the table.
|
||||
var principal = CreatePrincipal(new[] { role });
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.AuditExport, principal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user