fix(security): close auth & site-scoping gaps across 8 findings
Resolves the auth-theme batch from the 2026-05-28 baseline review (8 findings across Security/CentralUI/ManagementService/CLI). The most consequential gaps: NotificationReport + SiteCallsReport now route through SiteScopeService so a site-scoped Deployment user cannot see or act on other sites' rows (CUI-028); QueryAuditLogCommand is no longer "any authenticated user" — gated Admin-only to match /api/audit/query's strictness (MS-018); RoleMapper preserves the broader grant when a user is in both an unscoped and scoped Deployment LDAP group, instead of silently narrowing to the scoped set (Sec-016); and the dead SiteScopeRequirement/Handler are deleted so SiteScopeService is unambiguously the sole site-scoping mechanism (Sec-017). Pending findings: 172 → 164.
This commit is contained in:
@@ -530,4 +530,81 @@ public class AuditEndpointsTests
|
||||
Assert.Null(paging.AfterEventId);
|
||||
Assert.Null(paging.AfterOccurredAtUtc);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// ApplySiteScope (Management-019)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ApplySiteScope_SystemWideUser_ReturnsFilterUnchanged()
|
||||
{
|
||||
// Empty PermittedSiteIds is the system-wide signal (Admin, system-wide
|
||||
// Deployment, audit roles with no scope rules attached). The filter
|
||||
// should pass through with no restriction added.
|
||||
var user = new ScadaLink.Commons.Messages.Management.AuthenticatedUser(
|
||||
"alice", "Alice", new[] { "Admin" }, Array.Empty<string>());
|
||||
var filter = new AuditLogQueryFilter(SourceSiteIds: new[] { "plant-a" });
|
||||
|
||||
var result = AuditEndpoints.ApplySiteScope(filter, user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Same(filter, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySiteScope_ScopedUser_EmptyCallerFilter_RestrictedToPermittedSet()
|
||||
{
|
||||
// No explicit sourceSiteId from the caller — the helper must restrict
|
||||
// the query to the user's permitted set, otherwise a site-scoped audit
|
||||
// user could read every site's rows.
|
||||
var user = new ScadaLink.Commons.Messages.Management.AuthenticatedUser(
|
||||
"alice", "Alice", new[] { "AuditReadOnly" }, new[] { "plant-a", "plant-b" });
|
||||
var filter = new AuditLogQueryFilter();
|
||||
|
||||
var result = AuditEndpoints.ApplySiteScope(filter, user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result!.SourceSiteIds);
|
||||
Assert.Equal(new[] { "plant-a", "plant-b" }, result.SourceSiteIds!.OrderBy(s => s).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySiteScope_ScopedUser_ExplicitInScopeFilter_KeptVerbatim()
|
||||
{
|
||||
var user = new ScadaLink.Commons.Messages.Management.AuthenticatedUser(
|
||||
"alice", "Alice", new[] { "AuditReadOnly" }, new[] { "plant-a", "plant-b" });
|
||||
var filter = new AuditLogQueryFilter(SourceSiteIds: new[] { "plant-a" });
|
||||
|
||||
var result = AuditEndpoints.ApplySiteScope(filter, user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(new[] { "plant-a" }, result!.SourceSiteIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySiteScope_ScopedUser_ExplicitOutOfScopeFilter_ReturnsNull()
|
||||
{
|
||||
// Caller explicitly asked for a site they cannot see — the helper signals
|
||||
// "403" by returning null rather than silently producing an empty page.
|
||||
var user = new ScadaLink.Commons.Messages.Management.AuthenticatedUser(
|
||||
"alice", "Alice", new[] { "AuditReadOnly" }, new[] { "plant-a" });
|
||||
var filter = new AuditLogQueryFilter(SourceSiteIds: new[] { "plant-b" });
|
||||
|
||||
var result = AuditEndpoints.ApplySiteScope(filter, user);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySiteScope_ScopedUser_MixedInAndOutOfScopeFilter_IntersectedToInScopeOnly()
|
||||
{
|
||||
var user = new ScadaLink.Commons.Messages.Management.AuthenticatedUser(
|
||||
"alice", "Alice", new[] { "AuditReadOnly" }, new[] { "plant-a" });
|
||||
var filter = new AuditLogQueryFilter(SourceSiteIds: new[] { "plant-a", "plant-b" });
|
||||
|
||||
var result = AuditEndpoints.ApplySiteScope(filter, user);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(new[] { "plant-a" }, result!.SourceSiteIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,36 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
Assert.Contains("Deployment", response.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QueryAuditLogCommand_WithNoRoles_ReturnsUnauthorized()
|
||||
{
|
||||
// ManagementService-018: QueryAuditLogCommand used to fall through to the
|
||||
// default "any authenticated user" case, allowing a Deployment-only or
|
||||
// no-role caller to read the configuration audit log via /management
|
||||
// even though /api/audit/query enforces OperationalAuditRoles. The fix
|
||||
// gates this legacy command to Admin so the older route is never looser
|
||||
// than the new REST endpoint.
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new QueryAuditLogCommand(null, null, null, null, null, 1, 25));
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Admin", response.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QueryAuditLogCommand_WithDeploymentRole_ReturnsUnauthorized()
|
||||
{
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new QueryAuditLogCommand(null, null, null, null, null, 1, 25), "Deployment");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Admin", response.Message);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 2. Read-only query passes without special role
|
||||
// ========================================================================
|
||||
|
||||
Reference in New Issue
Block a user