513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
266 lines
12 KiB
C#
266 lines
12 KiB
C#
using System.Security.Claims;
|
|
using Microsoft.AspNetCore.Components.Authorization;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
|
using ZB.MOM.WW.ScadaBridge.Security;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Auth;
|
|
|
|
/// <summary>
|
|
/// Regression tests for site-scoped permissions. Site-scoped Deployment permissions are
|
|
/// written as <c>SiteId</c> claims at login but were never read — Deployment
|
|
/// pages listed and acted on every site. <see cref="SiteScopeService"/> is the
|
|
/// shared helper that reads those claims; these tests pin its behaviour.
|
|
/// </summary>
|
|
public class SiteScopeServiceTests
|
|
{
|
|
private sealed class StubAuthStateProvider : AuthenticationStateProvider
|
|
{
|
|
private readonly ClaimsPrincipal _user;
|
|
/// <summary>Initializes a new instance of the <c>StubAuthStateProvider</c> class.</summary>
|
|
/// <param name="user">The user.</param>
|
|
public StubAuthStateProvider(ClaimsPrincipal user) => _user = user;
|
|
/// <inheritdoc />
|
|
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
|
=> Task.FromResult(new AuthenticationState(_user));
|
|
}
|
|
|
|
private static SiteScopeService ForUser(params Claim[] claims)
|
|
{
|
|
var identity = new ClaimsIdentity(claims, authenticationType: "TestCookie");
|
|
return new SiteScopeService(new StubAuthStateProvider(new ClaimsPrincipal(identity)));
|
|
}
|
|
|
|
private static Claim Role(string role) => new(JwtTokenService.RoleClaimType, role);
|
|
private static Claim SiteClaim(int id) => new(JwtTokenService.SiteIdClaimType, id.ToString());
|
|
|
|
private static List<Site> Sites(params int[] ids)
|
|
=> ids.Select(id => new Site($"Site{id}", $"SITE-{id}") { Id = id }).ToList();
|
|
|
|
/// <summary>Verifies that a deployer with no site claims is treated as system-wide.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DeploymentUserWithNoSiteClaims_IsSystemWide()
|
|
{
|
|
var svc = ForUser(Role("Deployer"));
|
|
|
|
Assert.True(await svc.IsSystemWideAsync());
|
|
Assert.Empty(await svc.PermittedSiteIdsAsync());
|
|
}
|
|
|
|
/// <summary>Verifies that filtering sites for a system-wide user returns every site.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task SystemWideUser_FilterSites_ReturnsAllSites()
|
|
{
|
|
var svc = ForUser(Role("Deployer"));
|
|
|
|
var filtered = await svc.FilterSitesAsync(Sites(1, 2, 3));
|
|
|
|
Assert.Equal(new[] { 1, 2, 3 }, filtered.Select(s => s.Id));
|
|
}
|
|
|
|
/// <summary>Verifies that filtering sites for a scoped user returns only the permitted sites.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ScopedUser_FilterSites_ReturnsOnlyPermittedSites()
|
|
{
|
|
// Regression: a Deployment user scoped to sites 1 and 3 must NOT see site 2.
|
|
var svc = ForUser(Role("Deployer"), SiteClaim(1), SiteClaim(3));
|
|
|
|
var filtered = await svc.FilterSitesAsync(Sites(1, 2, 3, 4));
|
|
|
|
Assert.Equal(new[] { 1, 3 }, filtered.Select(s => s.Id).OrderBy(x => x));
|
|
}
|
|
|
|
/// <summary>Verifies that a scoped user is allowed access only to the sites they were granted.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ScopedUser_IsSiteAllowed_OnlyForGrantedSites()
|
|
{
|
|
var svc = ForUser(Role("Deployer"), SiteClaim(5));
|
|
|
|
Assert.True(await svc.IsSiteAllowedAsync(5));
|
|
Assert.False(await svc.IsSiteAllowedAsync(6));
|
|
}
|
|
|
|
/// <summary>Verifies that a scoped user is not system-wide and reports its permitted site ids.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ScopedUser_IsNotSystemWide_AndReportsItsPermittedIds()
|
|
{
|
|
var svc = ForUser(Role("Deployer"), SiteClaim(7), SiteClaim(9));
|
|
|
|
Assert.False(await svc.IsSystemWideAsync());
|
|
Assert.Equal(new[] { 7, 9 }, (await svc.PermittedSiteIdsAsync()).OrderBy(x => x));
|
|
}
|
|
|
|
/// <summary>Verifies that a system-wide user is allowed access to any site.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task SystemWideUser_IsSiteAllowed_ForAnySite()
|
|
{
|
|
var svc = ForUser(Role("Deployer"));
|
|
|
|
Assert.True(await svc.IsSiteAllowedAsync(1));
|
|
Assert.True(await svc.IsSiteAllowedAsync(999));
|
|
}
|
|
|
|
// ── Administrator is system-wide regardless of SiteId claims (register 86) ──
|
|
//
|
|
// ManagementActor has always exempted Administrator from site scoping
|
|
// (EnforceSiteScope and every handler that calls it). This service was
|
|
// claims-only, so an Administrator who ALSO carried a scoped Deployer grant
|
|
// could act fleet-wide through the CLI/HTTP surface while the UI hid the same
|
|
// sites. The UI was the side that disagreed with the documented rule.
|
|
|
|
/// <summary>Verifies that an administrator carrying site claims is still system-wide.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task AdministratorWithSiteClaims_IsSystemWide()
|
|
{
|
|
var svc = ForUser(Role("Administrator"), Role("Deployer"), SiteClaim(1), SiteClaim(3));
|
|
|
|
Assert.True(await svc.IsSystemWideAsync());
|
|
}
|
|
|
|
/// <summary>Verifies that filtering sites for an administrator with site claims returns every site.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task AdministratorWithSiteClaims_FilterSites_ReturnsAllSites()
|
|
{
|
|
var svc = ForUser(Role("Administrator"), Role("Deployer"), SiteClaim(1));
|
|
|
|
var filtered = await svc.FilterSitesAsync(Sites(1, 2, 3));
|
|
|
|
Assert.Equal(new[] { 1, 2, 3 }, filtered.Select(s => s.Id));
|
|
}
|
|
|
|
/// <summary>Verifies that an administrator with site claims is allowed access to sites outside those claims.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task AdministratorWithSiteClaims_IsSiteAllowed_ForSitesOutsideTheClaims()
|
|
{
|
|
var svc = ForUser(Role("Administrator"), SiteClaim(1));
|
|
|
|
Assert.True(await svc.IsSiteAllowedAsync(2));
|
|
}
|
|
|
|
/// <summary>Verifies that an administrator with site claims reports no permitted site ids.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task AdministratorWithSiteClaims_ReportsNoPermittedIds()
|
|
{
|
|
// The invariant "system-wide => empty id set" must hold for every caller, so
|
|
// a consumer that reads the ids without consulting IsSystemWide cannot
|
|
// silently narrow an Administrator to a subset of sites.
|
|
var svc = ForUser(Role("Administrator"), SiteClaim(1), SiteClaim(3));
|
|
|
|
Assert.Empty(await svc.PermittedSiteIdsAsync());
|
|
}
|
|
|
|
/// <summary>Verifies that the administrator role match ignores case.</summary>
|
|
/// <param name="role">The role.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Theory]
|
|
[InlineData("administrator")]
|
|
[InlineData("ADMINISTRATOR")]
|
|
public async Task AdministratorRoleMatch_IsCaseInsensitive(string role)
|
|
{
|
|
// ManagementActor compares roles with OrdinalIgnoreCase. ClaimsPrincipal
|
|
// .IsInRole compares claim VALUES ordinally, so matching the actor requires
|
|
// reading the role claims explicitly — otherwise the two sides would
|
|
// disagree on a differently-cased role name.
|
|
var svc = ForUser(Role(role), SiteClaim(1));
|
|
|
|
Assert.True(await svc.IsSystemWideAsync());
|
|
}
|
|
|
|
/// <summary>Verifies that a designer with site claims remains scoped to those sites.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DesignerWithSiteClaims_IsStillScoped()
|
|
{
|
|
// The class doc used to claim "any Admin/Design user" is system-wide. Only
|
|
// Administrator is — on either side of the rule — and the doc was corrected
|
|
// rather than the bypass widened.
|
|
var svc = ForUser(Role("Designer"), SiteClaim(1));
|
|
|
|
Assert.False(await svc.IsSystemWideAsync());
|
|
Assert.False(await svc.IsSiteAllowedAsync(2));
|
|
}
|
|
|
|
// ── Resolve(ClaimsPrincipal): the same rule as a pure function ───────────
|
|
// Display-gating callers (the rail pending-updates indicator) hold a principal
|
|
// already and must NOT pay for the instance methods' authoritative, fail-closed
|
|
// state refresh — which for a polling component would be a forced session
|
|
// revalidation per tick per circuit. These pin that the pure overload answers
|
|
// identically to the async path.
|
|
|
|
private static ClaimsPrincipal Principal(params Claim[] claims)
|
|
=> new(new ClaimsIdentity(claims, authenticationType: "TestCookie"));
|
|
|
|
/// <summary>Verifies that resolving an administrator yields a system-wide scope with an empty site set.</summary>
|
|
/// <returns>The result.</returns>
|
|
[Fact]
|
|
public void Resolve_Administrator_IsSystemWideWithAnEmptySet()
|
|
{
|
|
// Even carrying SiteId claims: an Administrator's ids are not collected at
|
|
// all, so "system-wide => empty set" holds for every caller.
|
|
var (isSystemWide, sites) = SiteScopeService.Resolve(
|
|
Principal(Role("Administrator"), SiteClaim(1), SiteClaim(2)));
|
|
|
|
Assert.True(isSystemWide);
|
|
Assert.Empty(sites);
|
|
}
|
|
|
|
/// <summary>Verifies that resolving a principal with site claims yields the scoped site set.</summary>
|
|
/// <returns>The result.</returns>
|
|
[Fact]
|
|
public void Resolve_SiteClaims_YieldTheScopedSet()
|
|
{
|
|
var (isSystemWide, sites) = SiteScopeService.Resolve(
|
|
Principal(Role("Deployer"), SiteClaim(1), SiteClaim(3)));
|
|
|
|
Assert.False(isSystemWide);
|
|
Assert.Equal([1, 3], sites.OrderBy(id => id));
|
|
}
|
|
|
|
/// <summary>Verifies that resolving a principal with no site claims yields a system-wide scope.</summary>
|
|
/// <returns>The result.</returns>
|
|
[Fact]
|
|
public void Resolve_NoSiteClaims_IsSystemWide()
|
|
{
|
|
var (isSystemWide, sites) = SiteScopeService.Resolve(Principal(Role("Deployer")));
|
|
|
|
Assert.True(isSystemWide);
|
|
Assert.Empty(sites);
|
|
}
|
|
|
|
/// <summary>Verifies that resolving an unauthenticated principal yields a scope with no permitted sites.</summary>
|
|
/// <returns>The result.</returns>
|
|
[Fact]
|
|
public void Resolve_UnauthenticatedPrincipal_IsScopedToNothing()
|
|
{
|
|
// Fail closed: not system-wide AND no permitted sites.
|
|
var (isSystemWide, sites) = SiteScopeService.Resolve(new ClaimsPrincipal(new ClaimsIdentity()));
|
|
|
|
Assert.False(isSystemWide);
|
|
Assert.Empty(sites);
|
|
}
|
|
|
|
/// <summary>Verifies that the synchronous resolve produces the same result as the async path.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task Resolve_AgreesWithTheAsyncPath()
|
|
{
|
|
var claims = new[] { Role("Deployer"), SiteClaim(2), SiteClaim(5) };
|
|
var svc = ForUser(claims);
|
|
|
|
var (isSystemWide, sites) = SiteScopeService.Resolve(Principal(claims));
|
|
|
|
Assert.Equal(await svc.IsSystemWideAsync(), isSystemWide);
|
|
Assert.Equal((await svc.PermittedSiteIdsAsync()).OrderBy(id => id), sites.OrderBy(id => id));
|
|
}
|
|
}
|