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;
/// AuditReadOnly → Viewer (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);
+ }
+}