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:
@@ -314,6 +314,29 @@ public class RoleMapperTests : IDisposable
|
||||
Assert.Contains(site2.Id.ToString(), result.PermittedSiteIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapGroupsToRoles_UserInBothSystemWideAndScopedDeploymentGroup_IsSystemWide()
|
||||
{
|
||||
// Security-016: a user in BOTH an unscoped Deployment mapping
|
||||
// (SCADA-Deploy-All, Id=3) AND a scoped Deployment mapping
|
||||
// (SCADA-Deploy-SiteA, Id=4) used to be silently narrowed to the site-A
|
||||
// grant. The union semantics now preserve the broader grant: the
|
||||
// unscoped mapping wins, PermittedSiteIds is empty, system-wide.
|
||||
var siteA = new Site("SiteA", "S-A");
|
||||
_context.Sites.Add(siteA);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Mapping Id=4 (SCADA-Deploy-SiteA) is seeded; attach a scope rule for siteA.
|
||||
_context.SiteScopeRules.Add(new SiteScopeRule { LdapGroupMappingId = 4, SiteId = siteA.Id });
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var result = await _roleMapper.MapGroupsToRolesAsync(new[] { "SCADA-Deploy-All", "SCADA-Deploy-SiteA" });
|
||||
|
||||
Assert.Contains("Deployment", result.Roles);
|
||||
Assert.True(result.IsSystemWideDeployment);
|
||||
Assert.Empty(result.PermittedSiteIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapGroupsToRoles_SystemWideDeployment_NoScopeRules()
|
||||
{
|
||||
@@ -1030,6 +1053,20 @@ public class Security012GroupLookupFailureTests
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(new[] { "SCADA-Admins" }, result.Groups);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServiceAccountBindException_DoesNotInheritLdapException_SoCatchOrderIsCorrect()
|
||||
{
|
||||
// Security-019: the LdapAuthService catch chain matches
|
||||
// ServiceAccountBindException *before* the generic LdapException catch. That only
|
||||
// produces the distinct "Authentication service is misconfigured" message if the
|
||||
// exception type is NOT an LdapException subtype (otherwise it would be caught
|
||||
// by the broader handler first regardless of ordering).
|
||||
var ex = new ServiceAccountBindException(new InvalidOperationException("boom"));
|
||||
|
||||
Assert.IsNotType<Novell.Directory.Ldap.LdapException>(ex);
|
||||
Assert.IsType<InvalidOperationException>(ex.InnerException);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -1132,82 +1169,6 @@ public class AuthorizationPolicyTests
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.AuditExport, principal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SiteScope_SystemWideDeployer_Succeeds()
|
||||
{
|
||||
var handler = new SiteScopeAuthorizationHandler();
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(JwtTokenService.RoleClaimType, "Deployment")
|
||||
// No SiteId claims = system-wide
|
||||
};
|
||||
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
|
||||
|
||||
var requirement = new SiteScopeRequirement("42");
|
||||
var context = new AuthorizationHandlerContext(new[] { requirement }, principal, null);
|
||||
|
||||
await handler.HandleAsync(context);
|
||||
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SiteScope_PermittedSite_Succeeds()
|
||||
{
|
||||
var handler = new SiteScopeAuthorizationHandler();
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(JwtTokenService.RoleClaimType, "Deployment"),
|
||||
new(JwtTokenService.SiteIdClaimType, "1"),
|
||||
new(JwtTokenService.SiteIdClaimType, "2")
|
||||
};
|
||||
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
|
||||
|
||||
var requirement = new SiteScopeRequirement("1");
|
||||
var context = new AuthorizationHandlerContext(new[] { requirement }, principal, null);
|
||||
|
||||
await handler.HandleAsync(context);
|
||||
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SiteScope_UnpermittedSite_Fails()
|
||||
{
|
||||
var handler = new SiteScopeAuthorizationHandler();
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(JwtTokenService.RoleClaimType, "Deployment"),
|
||||
new(JwtTokenService.SiteIdClaimType, "1")
|
||||
};
|
||||
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
|
||||
|
||||
var requirement = new SiteScopeRequirement("99");
|
||||
var context = new AuthorizationHandlerContext(new[] { requirement }, principal, null);
|
||||
|
||||
await handler.HandleAsync(context);
|
||||
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SiteScope_NoDeploymentRole_Fails()
|
||||
{
|
||||
var handler = new SiteScopeAuthorizationHandler();
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(JwtTokenService.RoleClaimType, "Admin")
|
||||
};
|
||||
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
|
||||
|
||||
var requirement = new SiteScopeRequirement("1");
|
||||
var context = new AuthorizationHandlerContext(new[] { requirement }, principal, null);
|
||||
|
||||
await handler.HandleAsync(context);
|
||||
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
private static ClaimsPrincipal CreatePrincipal(string[] roles, string[]? siteIds = null)
|
||||
{
|
||||
var claims = new List<Claim>();
|
||||
|
||||
Reference in New Issue
Block a user