diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Admin/LdapMappingForm.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Admin/LdapMappingForm.razor index 47c9b76e..08f837ab 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Admin/LdapMappingForm.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Admin/LdapMappingForm.razor @@ -34,6 +34,8 @@ + +
Deployer role: configure site scope below after saving.
diff --git a/src/ZB.MOM.WW.ScadaBridge.Security/AuthorizationPolicies.cs b/src/ZB.MOM.WW.ScadaBridge.Security/AuthorizationPolicies.cs index 4e75034f..fc0dbcb0 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Security/AuthorizationPolicies.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Security/AuthorizationPolicies.cs @@ -74,6 +74,21 @@ public static class AuthorizationPolicies public const string RequireDesign = "RequireDesign"; public const string RequireDeployment = "RequireDeployment"; + /// + /// Permission to initiate a two-person Secured Write (M7-A3 / T14a). + /// Satisfied by the role claim. Single-role + /// policy mirroring . + /// + public const string RequireOperator = "RequireOperator"; + + /// + /// Permission to approve a two-person Secured Write (M7-A3 / T14a). + /// Satisfied by the role claim. Kept distinct + /// from so initiation and approval are + /// separable duties. + /// + public const string RequireVerifier = "RequireVerifier"; + /// /// Read access to the Audit Log #23 surface (Audit Log page, /// Configuration Audit Log page, Audit nav group). Granted to the @@ -135,6 +150,12 @@ public static class AuthorizationPolicies options.AddPolicy(RequireDeployment, policy => policy.RequireClaim(JwtTokenService.RoleClaimType, Roles.Deployer)); + options.AddPolicy(RequireOperator, policy => + policy.RequireClaim(JwtTokenService.RoleClaimType, Roles.Operator)); + + options.AddPolicy(RequireVerifier, policy => + policy.RequireClaim(JwtTokenService.RoleClaimType, Roles.Verifier)); + // Multi-role permission policies — the policy succeeds when the // principal holds ANY of the mapped roles. RequireClaim with // multiple allowed values is the right primitive: it checks diff --git a/src/ZB.MOM.WW.ScadaBridge.Security/Roles.cs b/src/ZB.MOM.WW.ScadaBridge.Security/Roles.cs index b3d90f45..1a2e9bf4 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Security/Roles.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Security/Roles.cs @@ -28,8 +28,16 @@ namespace ZB.MOM.WW.ScadaBridge.Security; /// AuditReadOnlyViewer (COLLAPSE — keeps /// audit-read + nav, loses bulk export, which it never had) /// -/// Operator and Engineer exist in the canonical vocabulary but are -/// unused by ScadaBridge, so they are intentionally not declared here. +/// Engineer exists in the canonical vocabulary but is unused by +/// ScadaBridge, so it is intentionally not declared here. Operator is +/// now declared (M7-A3 / T14a) for the two-person Secured Writes feature. +/// +/// +/// Secured Writes (M7-A3 / T14a): Operator initiates a secured write and +/// Verifier approves it — two distinct global roles so a single principal +/// cannot both initiate and approve (separation of duties). Both are coarse +/// global roles, matching the existing role model; site scoping (if any) is +/// layered on at the LDAP-mapping level like the other roles. /// /// public static class Roles @@ -39,7 +47,15 @@ public static class Roles public const string Deployer = "Deployer"; public const string Viewer = "Viewer"; + /// Initiates a two-person Secured Write (M7-A3 / T14a). Canonical + /// vocabulary role; pairs with who approves. + public const string Operator = "Operator"; + + /// Approves a two-person Secured Write (M7-A3 / T14a). Held by a + /// principal distinct from the initiating . + public const string Verifier = "Verifier"; + /// All declared ScadaBridge roles — the single source of truth for "all /// permissions" (e.g. the dev auto-login principal). Stays in sync if a role is added. - public static readonly string[] All = [Administrator, Designer, Deployer, Viewer]; + public static readonly string[] All = [Administrator, Designer, Deployer, Viewer, Operator, Verifier]; } diff --git a/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/RolesAllTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/RolesAllTests.cs index 1488d6c7..523482bb 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/RolesAllTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/RolesAllTests.cs @@ -9,7 +9,7 @@ public class RolesAllTests public void All_ContainsEveryDeclaredRole() { Assert.Equal( - new[] { Roles.Administrator, Roles.Designer, Roles.Deployer, Roles.Viewer }, + new[] { Roles.Administrator, Roles.Designer, Roles.Deployer, Roles.Viewer, Roles.Operator, Roles.Verifier }, Roles.All); } } diff --git a/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/RolesTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/RolesTests.cs new file mode 100644 index 00000000..28cd4b1c --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/RolesTests.cs @@ -0,0 +1,47 @@ +using ZB.MOM.WW.ScadaBridge.Security; +using Xunit; + +namespace ZB.MOM.WW.ScadaBridge.Security.Tests; + +/// +/// Pins the role-name string literals and the contents of . +/// Added for M7-A3 (T14a): the two-person Secured Writes feature introduces the +/// Operator (initiates) and Verifier (approves) global roles. +/// +public class RolesTests +{ + [Fact] + public void OperatorConst_HasCanonicalValue() + { + Assert.Equal("Operator", Roles.Operator); + } + + [Fact] + public void VerifierConst_HasCanonicalValue() + { + Assert.Equal("Verifier", Roles.Verifier); + } + + [Fact] + public void All_StillContainsOriginalFourRoles() + { + Assert.Contains(Roles.Administrator, Roles.All); + Assert.Contains(Roles.Designer, Roles.All); + Assert.Contains(Roles.Deployer, Roles.All); + Assert.Contains(Roles.Viewer, Roles.All); + } + + [Fact] + public void All_ContainsOperatorAndVerifier() + { + Assert.Contains("Operator", Roles.All); + Assert.Contains("Verifier", Roles.All); + } + + [Fact] + public void AuthorizationPolicies_DeclareOperatorAndVerifierPolicyNames() + { + Assert.Equal("RequireOperator", AuthorizationPolicies.RequireOperator); + Assert.Equal("RequireVerifier", AuthorizationPolicies.RequireVerifier); + } +}