feat(auth)!: ScadaBridge canonical roles + SoD collapse (Audit→Administrator, AuditReadOnly→Viewer) + config-DB migration (Task 1.7)
Standardize role string VALUES on the canonical vocabulary
(Administrator/Designer/Deployer/Viewer; Operator/Engineer unused here):
Admin -> Administrator
Design -> Designer
Deployment -> Deployer
Audit -> Administrator (COLLAPSE; accepted privilege escalation)
AuditReadOnly-> Viewer (COLLAPSE; keeps audit-read, no export)
SoD: OperationalAuditRoles = { Administrator, Viewer },
AuditExportRoles = { Administrator }
so Viewer reads the audit log + nav but cannot bulk-export, while
Administrator does both + holds the full admin surface (the documented,
accepted auditor/admin SoD collapse).
Atomic move across every enforcement site:
- Roles constants; AuthorizationPolicies (RequireClaim values + SoD arrays +
honest XML-doc); RoleMapper Deployer check.
- ManagementActor.GetRequiredRole switch + the hard-coded site-scope
admin-bypass (now Roles.Administrator at all 6 sites). Site-scoping logic
is otherwise unchanged.
- DebugStreamHub Administrator/Deployer gates (Deployer kept case-sensitive).
- CentralUI BrowseService/BindingTester Designer guards; LdapMappingForm
dropdown now offers canonical values (incl. Viewer).
- Config-DB seed (LdapGroupMappings Id 1-4) + EF migration CanonicalizeRoles:
Id-keyed UpdateData for seed rows + idempotent raw catch-all UPDATEs for
operator-added rows. Down is lossy on the collapse (documented in-file).
No pending model changes.
Tests reworked to the collapsed model across Security/CentralUI/
ManagementService/ConfigurationDatabase/Integration suites, incl. explicit
Viewer-reads-not-exports and former-Audit-now-Administrator-escalation cases.
CHANGELOG: BREAKING security note documenting the canonicalization + SoD
collapse.
This commit is contained in:
+8
-5
@@ -25,12 +25,15 @@ public class LdapGroupMappingConfiguration : IEntityTypeConfiguration<LdapGroupM
|
||||
|
||||
builder.HasIndex(m => m.LdapGroupName).IsUnique();
|
||||
|
||||
// Seed default group mappings matching GLAuth test users
|
||||
// Seed default group mappings matching GLAuth test users.
|
||||
// Role VALUES are the canonical six (Task 1.7): Administrator/Designer/
|
||||
// Deployer. The LDAP group NAMES (SCADA-Admins etc.) are unchanged —
|
||||
// only the role each group maps to was canonicalized.
|
||||
builder.HasData(
|
||||
new LdapGroupMapping("SCADA-Admins", "Admin") { Id = 1 },
|
||||
new LdapGroupMapping("SCADA-Designers", "Design") { Id = 2 },
|
||||
new LdapGroupMapping("SCADA-Deploy-All", "Deployment") { Id = 3 },
|
||||
new LdapGroupMapping("SCADA-Deploy-SiteA", "Deployment") { Id = 4 });
|
||||
new LdapGroupMapping("SCADA-Admins", "Administrator") { Id = 1 },
|
||||
new LdapGroupMapping("SCADA-Designers", "Designer") { Id = 2 },
|
||||
new LdapGroupMapping("SCADA-Deploy-All", "Deployer") { Id = 3 },
|
||||
new LdapGroupMapping("SCADA-Deploy-SiteA", "Deployer") { Id = 4 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1740
File diff suppressed because it is too large
Load Diff
+133
@@ -0,0 +1,133 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Task 1.7 — canonicalizes ScadaBridge role VALUES onto the canonical six
|
||||
/// (only Administrator/Designer/Deployer/Viewer are used here) and collapses
|
||||
/// the former audit roles:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Admin → Administrator</description></item>
|
||||
/// <item><description>Design → Designer</description></item>
|
||||
/// <item><description>Deployment → Deployer</description></item>
|
||||
/// <item><description>Audit → Administrator (COLLAPSE — accepted SoD loss /
|
||||
/// privilege escalation)</description></item>
|
||||
/// <item><description>AuditReadOnly → Viewer (COLLAPSE — keeps audit-read,
|
||||
/// never had export)</description></item>
|
||||
/// </list>
|
||||
/// The <c>UpdateData</c> calls below canonicalize the four seeded rows
|
||||
/// (Id 1-4). The raw <c>Sql</c> catch-alls then canonicalize ANY
|
||||
/// operator-added (non-seed) rows whose <c>Role</c> still holds a legacy
|
||||
/// value — <c>LdapGroupMappings.Role</c> is free-text nvarchar, so operators
|
||||
/// may have created rows with Admin/Design/Deployment/Audit/AuditReadOnly.
|
||||
/// The catch-alls are value-keyed (WHERE Role IN (...)), so they are
|
||||
/// idempotent and safe whether or not the seed <c>UpdateData</c> already ran
|
||||
/// (after the seed update the seed rows no longer match a legacy value, so
|
||||
/// they are simply skipped).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// LOSSY DOWN: the Audit/AuditReadOnly → Administrator/Viewer collapse is not
|
||||
/// perfectly reversible. Down is a best-effort that maps Administrator→Admin
|
||||
/// and Viewer→AuditReadOnly. It CANNOT recover the original Audit vs Admin
|
||||
/// distinction (both became Administrator) nor distinguish a genuine Viewer
|
||||
/// from a former AuditReadOnly (both became Viewer). The four seeded rows are
|
||||
/// restored exactly by the Id-keyed <c>UpdateData</c> calls; operator rows
|
||||
/// are reverted by the value-keyed catch-alls under this caveat.
|
||||
/// </remarks>
|
||||
public partial class CanonicalizeRoles : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// 1) Canonicalize the four seeded rows by Id (EF-scaffolded).
|
||||
migrationBuilder.UpdateData(
|
||||
table: "LdapGroupMappings",
|
||||
keyColumn: "Id",
|
||||
keyValue: 1,
|
||||
column: "Role",
|
||||
value: "Administrator");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "LdapGroupMappings",
|
||||
keyColumn: "Id",
|
||||
keyValue: 2,
|
||||
column: "Role",
|
||||
value: "Designer");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "LdapGroupMappings",
|
||||
keyColumn: "Id",
|
||||
keyValue: 3,
|
||||
column: "Role",
|
||||
value: "Deployer");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "LdapGroupMappings",
|
||||
keyColumn: "Id",
|
||||
keyValue: 4,
|
||||
column: "Role",
|
||||
value: "Deployer");
|
||||
|
||||
// 2) Catch-all for operator-added (non-seed) rows. Value-keyed and
|
||||
// idempotent. Audit collapses INTO Administrator; AuditReadOnly
|
||||
// collapses INTO Viewer.
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE [LdapGroupMappings] SET [Role] = N'Administrator' WHERE [Role] IN (N'Admin', N'Audit');");
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE [LdapGroupMappings] SET [Role] = N'Designer' WHERE [Role] = N'Design';");
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE [LdapGroupMappings] SET [Role] = N'Deployer' WHERE [Role] = N'Deployment';");
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE [LdapGroupMappings] SET [Role] = N'Viewer' WHERE [Role] = N'AuditReadOnly';");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Restore the four seeded rows exactly (Id-keyed). Down is lossy for
|
||||
// the collapsed audit roles — see the class <remarks>.
|
||||
migrationBuilder.UpdateData(
|
||||
table: "LdapGroupMappings",
|
||||
keyColumn: "Id",
|
||||
keyValue: 1,
|
||||
column: "Role",
|
||||
value: "Admin");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "LdapGroupMappings",
|
||||
keyColumn: "Id",
|
||||
keyValue: 2,
|
||||
column: "Role",
|
||||
value: "Design");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "LdapGroupMappings",
|
||||
keyColumn: "Id",
|
||||
keyValue: 3,
|
||||
column: "Role",
|
||||
value: "Deployment");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "LdapGroupMappings",
|
||||
keyColumn: "Id",
|
||||
keyValue: 4,
|
||||
column: "Role",
|
||||
value: "Deployment");
|
||||
|
||||
// Best-effort reverse for operator-added (non-seed) rows. LOSSY:
|
||||
// Administrator→Admin and Viewer→AuditReadOnly cannot recover the
|
||||
// original Audit/Admin or Viewer/AuditReadOnly distinction (the
|
||||
// forward collapse merged those). Value-keyed and idempotent.
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE [LdapGroupMappings] SET [Role] = N'Admin' WHERE [Role] = N'Administrator';");
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE [LdapGroupMappings] SET [Role] = N'Design' WHERE [Role] = N'Designer';");
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE [LdapGroupMappings] SET [Role] = N'Deployment' WHERE [Role] = N'Deployer';");
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE [LdapGroupMappings] SET [Role] = N'AuditReadOnly' WHERE [Role] = N'Viewer';");
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1045,25 +1045,25 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||
{
|
||||
Id = 1,
|
||||
LdapGroupName = "SCADA-Admins",
|
||||
Role = "Admin"
|
||||
Role = "Administrator"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
LdapGroupName = "SCADA-Designers",
|
||||
Role = "Design"
|
||||
Role = "Designer"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
LdapGroupName = "SCADA-Deploy-All",
|
||||
Role = "Deployment"
|
||||
Role = "Deployer"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
LdapGroupName = "SCADA-Deploy-SiteA",
|
||||
Role = "Deployment"
|
||||
Role = "Deployer"
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user