feat(auth): ScadaBridge full canonical claims (ZbClaimTypes role/scope) + ZbCookieDefaults, keep cookie name (Task 1.5)
This commit is contained in:
@@ -36,7 +36,7 @@ public class ApiKeyFormAuditDrillinTests : BunitContext
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "admin"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "admin"),
|
||||
new Claim(JwtTokenService.RoleClaimType, "Admin"),
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
|
||||
@@ -26,7 +26,7 @@ public class ApiKeysListPageTests : BunitContext
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "admin"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "admin"),
|
||||
new Claim(JwtTokenService.RoleClaimType, "Admin"),
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
|
||||
@@ -35,7 +35,7 @@ public class SiteFormAuditDrillinTests : BunitContext
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "admin"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "admin"),
|
||||
new Claim(JwtTokenService.RoleClaimType, "Admin"),
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
using System.Text.Json;
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
@@ -28,7 +29,7 @@ public class DataConnectionFormTests : BunitContext
|
||||
{
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "tester"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "tester"),
|
||||
new Claim(ClaimTypes.Role, "Admin")
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -35,7 +36,7 @@ public class DataConnectionsPageTests : BunitContext
|
||||
{
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "tester"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "tester"),
|
||||
new Claim(ClaimTypes.Role, "Admin")
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ public class InstanceConfigureAuditDrillinTests : BunitContext
|
||||
// Auth: a system-wide Deployment user so SiteScope grants everything.
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "deployer"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "deployer"),
|
||||
new Claim(JwtTokenService.RoleClaimType, "Deployment"),
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ public class ExternalSystemFormAuditDrillinTests : BunitContext
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "tester"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "tester"),
|
||||
new Claim(JwtTokenService.RoleClaimType, "Design"),
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
|
||||
@@ -16,8 +16,9 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Layout;
|
||||
/// reveals a section's items and persists state to a cookie) and that the
|
||||
/// Notifications section's items are gated per-policy. The
|
||||
/// <c>AuthorizeView Policy=...</c> blocks evaluate the real policies, which
|
||||
/// require a claim of type <see cref="JwtTokenService.RoleClaimType"/> ("Role"),
|
||||
/// so the test principal carries claims of that exact type.
|
||||
/// require a claim of type <see cref="JwtTokenService.RoleClaimType"/> (the
|
||||
/// canonical <c>ZbClaimTypes.Role</c> framework URI), so the test principal
|
||||
/// carries claims of that exact type.
|
||||
/// </summary>
|
||||
public class NavMenuTests : BunitContext
|
||||
{
|
||||
@@ -40,7 +41,7 @@ public class NavMenuTests : BunitContext
|
||||
/// </summary>
|
||||
private IRenderedComponent<NavMenu> RenderWithRoles(params string[] roles)
|
||||
{
|
||||
var claims = new List<Claim> { new("Username", "tester") };
|
||||
var claims = new List<Claim> { new(JwtTokenService.UsernameClaimType, "tester") };
|
||||
claims.AddRange(roles.Select(r => new Claim(JwtTokenService.RoleClaimType, r)));
|
||||
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
|
||||
@@ -57,7 +57,7 @@ public class AuditLogPagePermissionTests : BunitContext
|
||||
|
||||
private static ClaimsPrincipal BuildPrincipal(params string[] roles)
|
||||
{
|
||||
var claims = new List<Claim> { new("Username", "tester") };
|
||||
var claims = new List<Claim> { new(JwtTokenService.UsernameClaimType, "tester") };
|
||||
claims.AddRange(roles.Select(r => new Claim(JwtTokenService.RoleClaimType, r)));
|
||||
return new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ public class AuditLogPageScaffoldTests : BunitContext
|
||||
|
||||
private static ClaimsPrincipal BuildPrincipal(params string[] roles)
|
||||
{
|
||||
var claims = new List<Claim> { new("Username", "tester") };
|
||||
var claims = new List<Claim> { new(JwtTokenService.UsernameClaimType, "tester") };
|
||||
claims.AddRange(roles.Select(r => new Claim(JwtTokenService.RoleClaimType, r)));
|
||||
return new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public class ExecutionTreePageTests : BunitContext
|
||||
|
||||
private static ClaimsPrincipal BuildPrincipal(params string[] roles)
|
||||
{
|
||||
var claims = new List<Claim> { new("Username", "tester") };
|
||||
var claims = new List<Claim> { new(JwtTokenService.UsernameClaimType, "tester") };
|
||||
claims.AddRange(roles.Select(r => new Claim(JwtTokenService.RoleClaimType, r)));
|
||||
return new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
using Akka.Actor;
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
@@ -80,7 +81,7 @@ public class HealthPageTests : BunitContext
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "tester"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "tester"),
|
||||
new Claim(ClaimTypes.Role, "Admin"),
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
|
||||
@@ -68,7 +68,7 @@ public class NotificationKpisPageTests : BunitContext
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "tester"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "tester"),
|
||||
new Claim(ClaimTypes.Role, "Deployment"),
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -21,7 +22,7 @@ public class NotificationListsPageTests : BunitContext
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "tester"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "tester"),
|
||||
new Claim(ClaimTypes.Role, "Design"),
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
using Akka.Actor;
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
@@ -87,7 +88,7 @@ public class NotificationReportDetailModalTests : BunitContext
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "tester"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "tester"),
|
||||
new Claim(ClaimTypes.Role, "Deployment"),
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
|
||||
@@ -73,7 +73,7 @@ public class NotificationReportPageTests : BunitContext
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "tester"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "tester"),
|
||||
new Claim(ClaimTypes.Role, "Deployment"),
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
using Akka.Actor;
|
||||
using Bunit;
|
||||
using Bunit.TestDoubles;
|
||||
@@ -171,7 +172,7 @@ public sealed class QueryStringDrillInTests
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "tester"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "tester"),
|
||||
new Claim(ClaimTypes.Role, "Deployment"),
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
|
||||
@@ -88,7 +88,7 @@ public class SiteCallsReportPageTests : BunitContext
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "tester"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "tester"),
|
||||
new Claim(ClaimTypes.Role, "Deployment"),
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
@@ -493,7 +493,7 @@ public class SiteCallsReportPageTests : BunitContext
|
||||
// Last AuthenticationStateProvider registration wins on resolution.
|
||||
var scopedUser = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim("Username", "scoped"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "scoped"),
|
||||
new Claim(ClaimTypes.Role, "Deployment"),
|
||||
new Claim(JwtTokenService.SiteIdClaimType, "1"), // Plant A only
|
||||
}, "TestAuth"));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -19,7 +20,7 @@ public class SmtpConfigurationPageTests : BunitContext
|
||||
{
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "tester"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "tester"),
|
||||
new Claim(ClaimTypes.Role, "Admin"),
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -50,7 +51,7 @@ public class TemplatesPageTests : BunitContext
|
||||
// GetCurrentUserAsync(); supply a stub so OnInitializedAsync doesn't crash.
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "tester"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "tester"),
|
||||
new Claim(ClaimTypes.Role, "Design")
|
||||
};
|
||||
var identity = new ClaimsIdentity(claims, "TestAuth");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -86,7 +87,7 @@ public class TopologyPageTests : BunitContext
|
||||
{
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim("Username", "tester"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "tester"),
|
||||
new Claim(ClaimTypes.Role, "Deployment")
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
@@ -215,7 +216,7 @@ public class TopologyPageTests : BunitContext
|
||||
// permitted sites via SiteScopeService.
|
||||
var scopedUser = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim("Username", "scoped-tester"),
|
||||
new Claim(JwtTokenService.UsernameClaimType, "scoped-tester"),
|
||||
new Claim(ZB.MOM.WW.ScadaBridge.Security.JwtTokenService.RoleClaimType, "Deployment"),
|
||||
// Permitted on site 1 only.
|
||||
new Claim(ZB.MOM.WW.ScadaBridge.Security.JwtTokenService.SiteIdClaimType, "1"),
|
||||
|
||||
@@ -1205,3 +1205,136 @@ public class SecurityOptionsValidatorTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Task 1.5: Canonical claim vocabulary (ZbClaimTypes) — role/scope migration
|
||||
|
||||
/// <summary>
|
||||
/// Task 1.5 (full canonical adoption): the Security module's claim-type constants now
|
||||
/// alias the shared <see cref="ZbClaimTypes"/> vocabulary. These tests pin the contract
|
||||
/// that the MINTED claim type is exactly the type the policies + token validation CONSUME,
|
||||
/// across both the JWT bearer and the cookie principal, so a future drift fails loudly.
|
||||
/// </summary>
|
||||
public class CanonicalClaimVocabularyTests
|
||||
{
|
||||
private static SecurityOptions CreateOptions() => new()
|
||||
{
|
||||
JwtSigningKey = "this-is-a-test-signing-key-for-hmac-sha256-must-be-long-enough",
|
||||
JwtExpiryMinutes = 15,
|
||||
IdleTimeoutMinutes = 30,
|
||||
JwtRefreshThresholdMinutes = 5
|
||||
};
|
||||
|
||||
private static JwtTokenService CreateService() =>
|
||||
new(Options.Create(CreateOptions()), NullLogger<JwtTokenService>.Instance);
|
||||
|
||||
[Fact]
|
||||
public void ClaimTypeConstants_AreCanonicalZbClaimTypes()
|
||||
{
|
||||
// The constants must be the canonical shared types so every mint/consume site
|
||||
// inherits them centrally.
|
||||
Assert.Equal(ZbClaimTypes.Role, JwtTokenService.RoleClaimType);
|
||||
Assert.Equal(ZbClaimTypes.ScopeId, JwtTokenService.SiteIdClaimType);
|
||||
Assert.Equal(ZbClaimTypes.DisplayName, JwtTokenService.DisplayNameClaimType);
|
||||
Assert.Equal(ZbClaimTypes.Username, JwtTokenService.UsernameClaimType);
|
||||
|
||||
// Role aliases the framework URI; scope/display/username are the zb: strings.
|
||||
Assert.Equal(ClaimTypes.Role, JwtTokenService.RoleClaimType);
|
||||
Assert.Equal("zb:scopeid", JwtTokenService.SiteIdClaimType);
|
||||
Assert.Equal("zb:displayname", JwtTokenService.DisplayNameClaimType);
|
||||
Assert.Equal("zb:username", JwtTokenService.UsernameClaimType);
|
||||
|
||||
// LastActivity has no canonical equivalent and is unchanged.
|
||||
Assert.Equal("LastActivity", JwtTokenService.LastActivityClaimType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MintedJwt_RoundTrips_CanonicalClaimTypesVerbatim()
|
||||
{
|
||||
var service = CreateService();
|
||||
var token = service.GenerateToken(
|
||||
"Jane Roe", "janer",
|
||||
new[] { Roles.Audit },
|
||||
new[] { "7" });
|
||||
|
||||
var principal = service.ValidateToken(token);
|
||||
Assert.NotNull(principal);
|
||||
|
||||
// Claim TYPES must be the canonical strings, not the framework's short JWT names
|
||||
// (MapInboundClaims=false / cleared outbound map guarantee this).
|
||||
Assert.Equal("Jane Roe", principal!.FindFirst(ZbClaimTypes.DisplayName)?.Value);
|
||||
Assert.Equal("janer", principal.FindFirst(ZbClaimTypes.Username)?.Value);
|
||||
Assert.Equal(Roles.Audit, principal.FindFirst(ZbClaimTypes.Role)?.Value);
|
||||
Assert.Equal("7", principal.FindFirst(ZbClaimTypes.ScopeId)?.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MintedJwt_RoleClaim_SatisfiesOperationalAuditPolicy()
|
||||
{
|
||||
// The load-bearing round-trip: a JWT minted with RoleClaimType=Audit must satisfy
|
||||
// a RequireClaim(RoleClaimType, OperationalAuditRoles) policy after validation.
|
||||
var service = CreateService();
|
||||
var token = service.GenerateToken("Jane Roe", "janer", new[] { Roles.Audit }, null);
|
||||
|
||||
var principal = service.ValidateToken(token);
|
||||
Assert.NotNull(principal);
|
||||
|
||||
Assert.True(await EvaluatePolicy(AuthorizationPolicies.OperationalAudit, principal!));
|
||||
// Audit does NOT grant AuditExport via a different vocabulary by accident:
|
||||
Assert.True(await EvaluatePolicy(AuthorizationPolicies.AuditExport, principal!));
|
||||
// AuditReadOnly is read-only — separate assertion that the role VALUE semantics
|
||||
// are untouched by the type migration.
|
||||
var roPrincipal = service.ValidateToken(
|
||||
service.GenerateToken("RO", "ro", new[] { Roles.AuditReadOnly }, null));
|
||||
Assert.True(await EvaluatePolicy(AuthorizationPolicies.OperationalAudit, roPrincipal!));
|
||||
Assert.False(await EvaluatePolicy(AuthorizationPolicies.AuditExport, roPrincipal!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CookiePrincipal_BuiltLikeLogin_AuthorizesAndExposesCanonicalTypes()
|
||||
{
|
||||
// Reproduce AuthEndpoints' cookie principal exactly: canonical claim types via the
|
||||
// constants + a ClaimsIdentity whose roleType is RoleClaimType so IsInRole resolves.
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.Name, "janer"),
|
||||
new(JwtTokenService.DisplayNameClaimType, "Jane Roe"),
|
||||
new(JwtTokenService.UsernameClaimType, "janer"),
|
||||
new(JwtTokenService.RoleClaimType, Roles.Admin),
|
||||
new(JwtTokenService.SiteIdClaimType, "3"),
|
||||
};
|
||||
var identity = new ClaimsIdentity(
|
||||
claims,
|
||||
authenticationType: "TestCookie",
|
||||
nameType: ClaimTypes.Name,
|
||||
roleType: JwtTokenService.RoleClaimType);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
|
||||
// Claim types are canonical.
|
||||
Assert.Equal("zb:scopeid", JwtTokenService.SiteIdClaimType);
|
||||
Assert.Equal("3", principal.FindFirst(ZbClaimTypes.ScopeId)?.Value);
|
||||
Assert.Equal("janer", principal.Identity?.Name); // ClaimTypes.Name resolves Identity.Name
|
||||
|
||||
// roleType wiring => IsInRole resolves against the canonical role claim.
|
||||
Assert.True(principal.IsInRole(Roles.Admin));
|
||||
|
||||
// Admin holds every permission by convention.
|
||||
Assert.True(await EvaluatePolicy(AuthorizationPolicies.RequireAdmin, principal));
|
||||
Assert.True(await EvaluatePolicy(AuthorizationPolicies.OperationalAudit, principal));
|
||||
Assert.True(await EvaluatePolicy(AuthorizationPolicies.AuditExport, principal));
|
||||
}
|
||||
|
||||
private static async Task<bool> EvaluatePolicy(string policyName, ClaimsPrincipal principal)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddScadaBridgeAuthorization();
|
||||
services.AddLogging();
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var authService = provider.GetRequiredService<IAuthorizationService>();
|
||||
|
||||
var result = await authService.AuthorizeAsync(principal, null, policyName);
|
||||
return result.Succeeded;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
Reference in New Issue
Block a user