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:
Joseph Doherty
2026-06-02 08:00:47 -04:00
parent 6ae605160c
commit b104760b3a
52 changed files with 2388 additions and 402 deletions
@@ -18,7 +18,7 @@ namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests;
/// the seam (the real <c>LibraryInboundApiKeyAdmin</c> + SQLite mapping is covered end-to-end
/// by the Security project's <c>LibraryInboundApiKeyAdminTests</c>). They verify the actor's
/// dispatch, response shapes (string keyId, one-time token, methods), the preserved ScadaBridge
/// management-audit calls, and that the "Admin" role gate still applies to all five commands.
/// management-audit calls, and that the "Administrator" role gate still applies to all five commands.
/// </summary>
public class ApiKeyCreationTests : TestKit, IDisposable
{
@@ -48,7 +48,7 @@ public class ApiKeyCreationTests : TestKit, IDisposable
public void CreateApiKey_ReturnsKeyIdAndOneTimeToken()
{
var actor = CreateActor();
actor.Tell(Envelope(new CreateApiKeyCommand("MES-Production", new[] { "MethodA", "MethodB" }), "Admin"));
actor.Tell(Envelope(new CreateApiKeyCommand("MES-Production", new[] { "MethodA", "MethodB" }), "Administrator"));
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
@@ -73,7 +73,7 @@ public class ApiKeyCreationTests : TestKit, IDisposable
public void CreateApiKey_AuditsTheCreate()
{
var actor = CreateActor();
actor.Tell(Envelope(new CreateApiKeyCommand("MES-Production", new[] { "MethodA" }), "Admin"));
actor.Tell(Envelope(new CreateApiKeyCommand("MES-Production", new[] { "MethodA" }), "Administrator"));
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
_auditService.Received(1).LogAsync(
@@ -84,7 +84,7 @@ public class ApiKeyCreationTests : TestKit, IDisposable
public void CreateApiKey_ResponseDoesNotEchoAHash()
{
var actor = CreateActor();
actor.Tell(Envelope(new CreateApiKeyCommand("MES-Production", new[] { "MethodA" }), "Admin"));
actor.Tell(Envelope(new CreateApiKeyCommand("MES-Production", new[] { "MethodA" }), "Administrator"));
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
@@ -100,7 +100,7 @@ public class ApiKeyCreationTests : TestKit, IDisposable
_admin.Seed("key-2", "Service B", enabled: false, "M3");
var actor = CreateActor();
actor.Tell(Envelope(new ListApiKeysCommand(), "Admin"));
actor.Tell(Envelope(new ListApiKeysCommand(), "Administrator"));
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
@@ -125,7 +125,7 @@ public class ApiKeyCreationTests : TestKit, IDisposable
_admin.Seed("key-1", "Service A", enabled: true, "M1");
var actor = CreateActor();
actor.Tell(Envelope(new UpdateApiKeyCommand("key-1", false), "Admin"));
actor.Tell(Envelope(new UpdateApiKeyCommand("key-1", false), "Administrator"));
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
@@ -145,7 +145,7 @@ public class ApiKeyCreationTests : TestKit, IDisposable
_admin.Seed("key-1", "Service A", enabled: true, "M1");
var actor = CreateActor();
actor.Tell(Envelope(new DeleteApiKeyCommand("key-1"), "Admin"));
actor.Tell(Envelope(new DeleteApiKeyCommand("key-1"), "Administrator"));
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
@@ -162,7 +162,7 @@ public class ApiKeyCreationTests : TestKit, IDisposable
_admin.Seed("key-1", "Service A", enabled: true, "Old1", "Old2");
var actor = CreateActor();
actor.Tell(Envelope(new SetApiKeyMethodsCommand("key-1", new[] { "New1", "New2", "New3" }), "Admin"));
actor.Tell(Envelope(new SetApiKeyMethodsCommand("key-1", new[] { "New1", "New2", "New3" }), "Administrator"));
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
@@ -188,7 +188,7 @@ public class ApiKeyCreationTests : TestKit, IDisposable
{
// No keys seeded — "key-unknown" does not exist.
var actor = CreateActor();
actor.Tell(Envelope(new UpdateApiKeyCommand("key-unknown", false), "Admin"));
actor.Tell(Envelope(new UpdateApiKeyCommand("key-unknown", false), "Administrator"));
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
@@ -202,7 +202,7 @@ public class ApiKeyCreationTests : TestKit, IDisposable
public void SetApiKeyMethods_UnknownKey_ReturnsManagementError_AndDoesNotAudit()
{
var actor = CreateActor();
actor.Tell(Envelope(new SetApiKeyMethodsCommand("key-unknown", new[] { "M1" }), "Admin"));
actor.Tell(Envelope(new SetApiKeyMethodsCommand("key-unknown", new[] { "M1" }), "Administrator"));
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
@@ -215,7 +215,7 @@ public class ApiKeyCreationTests : TestKit, IDisposable
public void DeleteApiKey_UnknownKey_ReturnsManagementError_AndDoesNotAudit()
{
var actor = CreateActor();
actor.Tell(Envelope(new DeleteApiKeyCommand("key-unknown"), "Admin"));
actor.Tell(Envelope(new DeleteApiKeyCommand("key-unknown"), "Administrator"));
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
@@ -232,7 +232,7 @@ public class ApiKeyCreationTests : TestKit, IDisposable
public void CreateApiKey_EmptyMethods_ReturnsManagementError()
{
var actor = CreateActor();
actor.Tell(Envelope(new CreateApiKeyCommand("MES-Production", Array.Empty<string>()), "Admin"));
actor.Tell(Envelope(new CreateApiKeyCommand("MES-Production", Array.Empty<string>()), "Administrator"));
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
@@ -249,7 +249,7 @@ public class ApiKeyCreationTests : TestKit, IDisposable
_admin.Seed("key-1", "Service A", enabled: true, "M1");
var actor = CreateActor();
actor.Tell(Envelope(new SetApiKeyMethodsCommand("key-1", Array.Empty<string>()), "Admin"));
actor.Tell(Envelope(new SetApiKeyMethodsCommand("key-1", Array.Empty<string>()), "Administrator"));
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
@@ -265,11 +265,11 @@ public class ApiKeyCreationTests : TestKit, IDisposable
public void EveryApiKeyCommand_RequiresAdminRole(object command)
{
var actor = CreateActor();
// A Design-role caller (not Admin) must be rejected for every API-key command.
actor.Tell(Envelope(command, "Design"));
// A Designer-role caller (not Administrator) must be rejected for every API-key command.
actor.Tell(Envelope(command, "Designer"));
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Admin", response.Message);
Assert.Contains("Administrator", response.Message);
}
public static IEnumerable<object[]> AllApiKeyCommands() => new[]
@@ -125,7 +125,7 @@ public class AuditEndpointsTests
public async Task Query_ValidParams_ReturnsJsonPage()
{
var (client, _, host) = await BuildHostAsync(
roles: new[] { "Audit" },
roles: new[] { "Administrator" },
queryPages: new[] { (IReadOnlyList<AuditEvent>)new[] { SampleEvent() } });
using (host)
{
@@ -160,7 +160,7 @@ public class AuditEndpointsTests
new DateTime(2026, 5, 20, 11, 0, 0, DateTimeKind.Utc)),
};
var (client, repo, host) = await BuildHostAsync(
roles: new[] { "Audit" },
roles: new[] { "Administrator" },
queryPages: new[] { pageOne });
using (host)
{
@@ -193,9 +193,9 @@ public class AuditEndpointsTests
[Fact]
public async Task Query_WithoutOperationalAudit_Returns403()
{
// A user whose only role is Design holds neither OperationalAudit nor
// A user whose only role is Designer holds neither OperationalAudit nor
// AuditExport — the query endpoint must 403.
var (client, _, host) = await BuildHostAsync(roles: new[] { "Design" });
var (client, _, host) = await BuildHostAsync(roles: new[] { "Designer" });
using (host)
{
var response = await client.SendAsync(Get("/api/audit/query"));
@@ -206,7 +206,7 @@ public class AuditEndpointsTests
[Fact]
public async Task Query_WithoutCredentials_Returns401()
{
var (client, _, host) = await BuildHostAsync(roles: new[] { "Audit" });
var (client, _, host) = await BuildHostAsync(roles: new[] { "Administrator" });
using (host)
{
var response = await client.SendAsync(Get("/api/audit/query", credential: ""));
@@ -215,10 +215,11 @@ public class AuditEndpointsTests
}
[Fact]
public async Task Query_AuditReadOnlyRole_IsAllowed()
public async Task Query_ViewerRole_IsAllowed()
{
// AuditReadOnly satisfies OperationalAudit (read) — query must succeed.
var (client, _, host) = await BuildHostAsync(roles: new[] { "AuditReadOnly" });
// Viewer (post Task 1.7 home of the former AuditReadOnly role) satisfies
// OperationalAudit (read) — query must succeed.
var (client, _, host) = await BuildHostAsync(roles: new[] { "Viewer" });
using (host)
{
var response = await client.SendAsync(Get("/api/audit/query"));
@@ -234,7 +235,7 @@ public class AuditEndpointsTests
public async Task Export_Csv_StreamsContent_WithCsvContentType()
{
var (client, _, host) = await BuildHostAsync(
roles: new[] { "Audit" },
roles: new[] { "Administrator" },
queryPages: new[]
{
(IReadOnlyList<AuditEvent>)new[] { SampleEvent() },
@@ -263,7 +264,7 @@ public class AuditEndpointsTests
{
// No format= param → csv default.
var (client, _, host) = await BuildHostAsync(
roles: new[] { "Audit" },
roles: new[] { "Administrator" },
queryPages: new[] { (IReadOnlyList<AuditEvent>)Array.Empty<AuditEvent>() });
using (host)
{
@@ -277,7 +278,7 @@ public class AuditEndpointsTests
public async Task Export_Jsonl_StreamsOnePerLine()
{
var (client, _, host) = await BuildHostAsync(
roles: new[] { "Audit" },
roles: new[] { "Administrator" },
queryPages: new[]
{
(IReadOnlyList<AuditEvent>)new[]
@@ -313,7 +314,7 @@ public class AuditEndpointsTests
{
// Parquet archival is deferred to v1.x (Component-AuditLog.md) — no
// library is referenced, so the endpoint returns 501 with guidance.
var (client, _, host) = await BuildHostAsync(roles: new[] { "Audit" });
var (client, _, host) = await BuildHostAsync(roles: new[] { "Administrator" });
using (host)
{
var response = await client.SendAsync(Get("/api/audit/export?format=parquet"));
@@ -327,9 +328,10 @@ public class AuditEndpointsTests
[Fact]
public async Task Export_WithoutAuditExport_Returns403()
{
// AuditReadOnly grants read (OperationalAudit) but NOT bulk export
// (AuditExport) — the export endpoint must 403.
var (client, _, host) = await BuildHostAsync(roles: new[] { "AuditReadOnly" });
// Viewer (former AuditReadOnly) grants read (OperationalAudit) but NOT
// bulk export (AuditExport) — the export endpoint must 403. This is the
// preserved half-SoD after the Task 1.7 AuditReadOnly→Viewer collapse.
var (client, _, host) = await BuildHostAsync(roles: new[] { "Viewer" });
using (host)
{
var response = await client.SendAsync(Get("/api/audit/export?format=csv"));
@@ -340,7 +342,7 @@ public class AuditEndpointsTests
[Fact]
public async Task Export_UnsupportedFormat_Returns400()
{
var (client, _, host) = await BuildHostAsync(roles: new[] { "Audit" });
var (client, _, host) = await BuildHostAsync(roles: new[] { "Administrator" });
using (host)
{
var response = await client.SendAsync(Get("/api/audit/export?format=xml"));
@@ -496,7 +498,7 @@ public class AuditEndpointsTests
{
// End-to-end: a repeated channel= query param must surface at the
// repository as a two-element Channels list.
var (client, repo, host) = await BuildHostAsync(roles: new[] { "Audit" });
var (client, repo, host) = await BuildHostAsync(roles: new[] { "Administrator" });
using (host)
{
var response = await client.SendAsync(Get(
@@ -537,11 +539,11 @@ public class AuditEndpointsTests
[Fact]
public void ApplySiteScope_SystemWideUser_ReturnsFilterUnchanged()
{
// Empty PermittedSiteIds is the system-wide signal (Admin, system-wide
// Deployment, audit roles with no scope rules attached). The filter
// should pass through with no restriction added.
// Empty PermittedSiteIds is the system-wide signal (Administrator,
// system-wide Deployer, audit roles with no scope rules attached). The
// filter should pass through with no restriction added.
var user = new ZB.MOM.WW.ScadaBridge.Commons.Messages.Management.AuthenticatedUser(
"alice", "Alice", new[] { "Admin" }, Array.Empty<string>());
"alice", "Alice", new[] { "Administrator" }, Array.Empty<string>());
var filter = new AuditLogQueryFilter(SourceSiteIds: new[] { "plant-a" });
var result = AuditEndpoints.ApplySiteScope(filter, user);
@@ -557,7 +559,7 @@ public class AuditEndpointsTests
// the query to the user's permitted set, otherwise a site-scoped audit
// user could read every site's rows.
var user = new ZB.MOM.WW.ScadaBridge.Commons.Messages.Management.AuthenticatedUser(
"alice", "Alice", new[] { "AuditReadOnly" }, new[] { "plant-a", "plant-b" });
"alice", "Alice", new[] { "Viewer" }, new[] { "plant-a", "plant-b" });
var filter = new AuditLogQueryFilter();
var result = AuditEndpoints.ApplySiteScope(filter, user);
@@ -571,7 +573,7 @@ public class AuditEndpointsTests
public void ApplySiteScope_ScopedUser_ExplicitInScopeFilter_KeptVerbatim()
{
var user = new ZB.MOM.WW.ScadaBridge.Commons.Messages.Management.AuthenticatedUser(
"alice", "Alice", new[] { "AuditReadOnly" }, new[] { "plant-a", "plant-b" });
"alice", "Alice", new[] { "Viewer" }, new[] { "plant-a", "plant-b" });
var filter = new AuditLogQueryFilter(SourceSiteIds: new[] { "plant-a" });
var result = AuditEndpoints.ApplySiteScope(filter, user);
@@ -586,7 +588,7 @@ public class AuditEndpointsTests
// Caller explicitly asked for a site they cannot see — the helper signals
// "403" by returning null rather than silently producing an empty page.
var user = new ZB.MOM.WW.ScadaBridge.Commons.Messages.Management.AuthenticatedUser(
"alice", "Alice", new[] { "AuditReadOnly" }, new[] { "plant-a" });
"alice", "Alice", new[] { "Viewer" }, new[] { "plant-a" });
var filter = new AuditLogQueryFilter(SourceSiteIds: new[] { "plant-b" });
var result = AuditEndpoints.ApplySiteScope(filter, user);
@@ -598,7 +600,7 @@ public class AuditEndpointsTests
public void ApplySiteScope_ScopedUser_MixedInAndOutOfScopeFilter_IntersectedToInScopeOnly()
{
var user = new ZB.MOM.WW.ScadaBridge.Commons.Messages.Management.AuthenticatedUser(
"alice", "Alice", new[] { "AuditReadOnly" }, new[] { "plant-a" });
"alice", "Alice", new[] { "Viewer" }, new[] { "plant-a" });
var filter = new AuditLogQueryFilter(SourceSiteIds: new[] { "plant-a", "plant-b" });
var result = AuditEndpoints.ApplySiteScope(filter, user);
@@ -12,7 +12,7 @@ public class DebugStreamHubTests
public void IsInstanceAccessAllowed_SiteScopedUser_InScopeInstance_Allowed()
{
var allowed = DebugStreamHub.IsInstanceAccessAllowed(
roles: new[] { "Deployment" },
roles: new[] { "Deployer" },
permittedSiteIds: new[] { "1", "2" },
instanceSiteId: 2);
@@ -23,7 +23,7 @@ public class DebugStreamHubTests
public void IsInstanceAccessAllowed_SiteScopedUser_OutOfScopeInstance_Denied()
{
var allowed = DebugStreamHub.IsInstanceAccessAllowed(
roles: new[] { "Deployment" },
roles: new[] { "Deployer" },
permittedSiteIds: new[] { "1", "2" },
instanceSiteId: 99);
@@ -33,9 +33,9 @@ public class DebugStreamHubTests
[Fact]
public void IsInstanceAccessAllowed_SystemWideDeployment_AnySiteAllowed()
{
// Empty permitted set == system-wide Deployment.
// Empty permitted set == system-wide Deployer.
var allowed = DebugStreamHub.IsInstanceAccessAllowed(
roles: new[] { "Deployment" },
roles: new[] { "Deployer" },
permittedSiteIds: Array.Empty<string>(),
instanceSiteId: 99);
@@ -46,7 +46,7 @@ public class DebugStreamHubTests
public void IsInstanceAccessAllowed_AdminRole_BypassesSiteScope()
{
var allowed = DebugStreamHub.IsInstanceAccessAllowed(
roles: new[] { "Admin", "Deployment" },
roles: new[] { "Administrator", "Deployer" },
permittedSiteIds: new[] { "1" },
instanceSiteId: 99);
@@ -57,7 +57,7 @@ public class DebugStreamHubTests
public void IsInstanceAccessAllowed_AdminRoleCheck_IsCaseInsensitive()
{
var allowed = DebugStreamHub.IsInstanceAccessAllowed(
roles: new[] { "admin" },
roles: new[] { "administrator" },
permittedSiteIds: new[] { "1" },
instanceSiteId: 99);
@@ -61,12 +61,12 @@ public class ManagementActorTests : TestKit, IDisposable
public void CreateSiteCommand_WithDesignRole_ReturnsUnauthorized()
{
var actor = CreateActor();
var envelope = Envelope(new CreateSiteCommand("Site1", "SITE1", "Desc"), "Design");
var envelope = Envelope(new CreateSiteCommand("Site1", "SITE1", "Desc"), "Designer");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Admin", response.Message);
Assert.Contains("Administrator", response.Message);
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
}
@@ -79,19 +79,19 @@ public class ManagementActorTests : TestKit, IDisposable
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Admin", response.Message);
Assert.Contains("Administrator", response.Message);
}
[Fact]
public void DeploymentCommand_WithDesignRole_ReturnsUnauthorized()
{
var actor = CreateActor();
var envelope = Envelope(new CreateInstanceCommand("Inst1", 1, 1), "Design");
var envelope = Envelope(new CreateInstanceCommand("Inst1", 1, 1), "Designer");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Deployment", response.Message);
Assert.Contains("Deployer", response.Message);
}
[Fact]
@@ -109,19 +109,19 @@ public class ManagementActorTests : TestKit, IDisposable
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Admin", response.Message);
Assert.Contains("Administrator", response.Message);
}
[Fact]
public void QueryAuditLogCommand_WithDeploymentRole_ReturnsUnauthorized()
{
var actor = CreateActor();
var envelope = Envelope(new QueryAuditLogCommand(null, null, null, null, null, 1, 25), "Deployment");
var envelope = Envelope(new QueryAuditLogCommand(null, null, null, null, null, 1, 25), "Deployer");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Admin", response.Message);
Assert.Contains("Administrator", response.Message);
}
// ========================================================================
@@ -154,7 +154,7 @@ public class ManagementActorTests : TestKit, IDisposable
_services.AddScoped(_ => siteRepo);
var actor = CreateActor();
var envelope = Envelope(new ListSitesCommand(), "Design");
var envelope = Envelope(new ListSitesCommand(), "Designer");
actor.Tell(envelope);
@@ -220,7 +220,7 @@ public class ManagementActorTests : TestKit, IDisposable
var actor = CreateActor();
var envelope = Envelope(
new CreateInstanceCommand("Pump1", 1, 1),
"Deployment");
"Deployer");
actor.Tell(envelope);
@@ -264,7 +264,7 @@ public class ManagementActorTests : TestKit, IDisposable
var actor = CreateActor();
var envelope = Envelope(
new CreateInstanceCommand("BadInst", 99, 1),
"Deployment");
"Deployer");
actor.Tell(envelope);
@@ -280,16 +280,16 @@ public class ManagementActorTests : TestKit, IDisposable
[Fact]
public void DesignCommand_WithAdminRole_ReturnsUnauthorized()
{
// CreateTemplateCommand requires "Design" role, "Admin" alone is insufficient
// CreateTemplateCommand requires "Designer" role, "Administrator" alone is insufficient
var actor = CreateActor();
var envelope = Envelope(
new CreateTemplateCommand("T1", null, null),
"Admin");
"Administrator");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Design", response.Message);
Assert.Contains("Designer", response.Message);
}
[Fact]
@@ -305,7 +305,7 @@ public class ManagementActorTests : TestKit, IDisposable
var actor = CreateActor();
var envelope = Envelope(
new CreateSiteCommand("NewSite", "NS1", "Desc"),
"Admin");
"Administrator");
actor.Tell(envelope);
@@ -324,10 +324,10 @@ public class ManagementActorTests : TestKit, IDisposable
_services.AddScoped(_ => siteRepo);
var actor = CreateActor();
// "admin" lowercase should still match "Admin" requirement
// "administrator" lowercase should still match "Administrator" requirement
var envelope = Envelope(
new CreateSiteCommand("Site2", "S2", null),
"admin");
"administrator");
actor.Tell(envelope);
@@ -343,84 +343,84 @@ public class ManagementActorTests : TestKit, IDisposable
public void SharedScriptCreate_WithAdminRole_ReturnsUnauthorized()
{
var actor = CreateActor();
var envelope = Envelope(new CreateSharedScriptCommand("Script1", "code", null, null), "Admin");
var envelope = Envelope(new CreateSharedScriptCommand("Script1", "code", null, null), "Administrator");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Design", response.Message);
Assert.Contains("Designer", response.Message);
}
[Fact]
public void DatabaseConnectionCreate_WithDeploymentRole_ReturnsUnauthorized()
{
var actor = CreateActor();
var envelope = Envelope(new CreateDatabaseConnectionDefCommand("DB1", "Server=test"), "Deployment");
var envelope = Envelope(new CreateDatabaseConnectionDefCommand("DB1", "Server=test"), "Deployer");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Design", response.Message);
Assert.Contains("Designer", response.Message);
}
[Fact]
public void ApiMethodCreate_WithAdminRole_ReturnsUnauthorized()
{
var actor = CreateActor();
var envelope = Envelope(new CreateApiMethodCommand("Method1", "code", 30, null, null), "Admin");
var envelope = Envelope(new CreateApiMethodCommand("Method1", "code", 30, null, null), "Administrator");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Design", response.Message);
Assert.Contains("Designer", response.Message);
}
[Fact]
public void AddTemplateAttribute_WithDeploymentRole_ReturnsUnauthorized()
{
var actor = CreateActor();
var envelope = Envelope(new AddTemplateAttributeCommand(1, "Attr1", "Float", null, null, null, false), "Deployment");
var envelope = Envelope(new AddTemplateAttributeCommand(1, "Attr1", "Float", null, null, null, false), "Deployer");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Design", response.Message);
Assert.Contains("Designer", response.Message);
}
[Fact]
public void UpdateApiKey_WithDesignRole_ReturnsUnauthorized()
{
var actor = CreateActor();
var envelope = Envelope(new UpdateApiKeyCommand("key-1", true), "Design");
var envelope = Envelope(new UpdateApiKeyCommand("key-1", true), "Designer");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Admin", response.Message);
Assert.Contains("Administrator", response.Message);
}
[Fact]
public void AddScopeRule_WithDesignRole_ReturnsUnauthorized()
{
var actor = CreateActor();
var envelope = Envelope(new AddScopeRuleCommand(1, 1), "Design");
var envelope = Envelope(new AddScopeRuleCommand(1, 1), "Designer");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Admin", response.Message);
Assert.Contains("Administrator", response.Message);
}
[Fact]
public void UpdateArea_WithAdminRole_ReturnsUnauthorized()
{
var actor = CreateActor();
var envelope = Envelope(new UpdateAreaCommand(1, "NewName"), "Admin");
var envelope = Envelope(new UpdateAreaCommand(1, "NewName"), "Administrator");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Design", response.Message);
Assert.Contains("Designer", response.Message);
}
// ========================================================================
@@ -486,7 +486,7 @@ public class ManagementActorTests : TestKit, IDisposable
_services.AddScoped(_ => secRepo);
var actor = CreateActor();
var envelope = Envelope(new ListScopeRulesCommand(1), "Admin");
var envelope = Envelope(new ListScopeRulesCommand(1), "Administrator");
actor.Tell(envelope);
@@ -545,7 +545,7 @@ public class ManagementActorTests : TestKit, IDisposable
.Returns(new Instance("Pump7") { Id = 7, SiteId = 2 });
var actor = CreateActor();
var envelope = ScopedEnvelope(new GetInstanceCommand(7), new[] { "1" }, "Deployment");
var envelope = ScopedEnvelope(new GetInstanceCommand(7), new[] { "1" }, "Deployer");
actor.Tell(envelope);
@@ -560,7 +560,7 @@ public class ManagementActorTests : TestKit, IDisposable
.Returns(new Instance("Pump7") { Id = 7, SiteId = 1 });
var actor = CreateActor();
var envelope = ScopedEnvelope(new GetInstanceCommand(7), new[] { "1" }, "Deployment");
var envelope = ScopedEnvelope(new GetInstanceCommand(7), new[] { "1" }, "Deployer");
actor.Tell(envelope);
@@ -573,7 +573,7 @@ public class ManagementActorTests : TestKit, IDisposable
AddSiteRepoWithSite(2, "SITE2");
var actor = CreateActor();
var envelope = ScopedEnvelope(new GetSiteCommand(2), new[] { "1" }, "Deployment");
var envelope = ScopedEnvelope(new GetSiteCommand(2), new[] { "1" }, "Deployer");
actor.Tell(envelope);
@@ -591,7 +591,7 @@ public class ManagementActorTests : TestKit, IDisposable
_services.AddScoped(_ => uiRepo);
var actor = CreateActor();
var envelope = ScopedEnvelope(new ListAreasCommand(2), new[] { "1" }, "Deployment");
var envelope = ScopedEnvelope(new ListAreasCommand(2), new[] { "1" }, "Deployer");
actor.Tell(envelope);
@@ -608,7 +608,7 @@ public class ManagementActorTests : TestKit, IDisposable
_services.AddScoped(_ => siteRepo);
var actor = CreateActor();
var envelope = ScopedEnvelope(new GetDataConnectionCommand(5), new[] { "1" }, "Deployment");
var envelope = ScopedEnvelope(new GetDataConnectionCommand(5), new[] { "1" }, "Deployer");
actor.Tell(envelope);
@@ -623,7 +623,7 @@ public class ManagementActorTests : TestKit, IDisposable
AddSiteRepoWithSite(2, "SITE2");
var actor = CreateActor();
var envelope = ScopedEnvelope(new GetSiteCommand(2), new[] { "1" }, "Admin");
var envelope = ScopedEnvelope(new GetSiteCommand(2), new[] { "1" }, "Administrator");
actor.Tell(envelope);
@@ -637,7 +637,7 @@ public class ManagementActorTests : TestKit, IDisposable
AddSiteRepoWithSite(2, "SITE2");
var actor = CreateActor();
var envelope = ScopedEnvelope(new QueryEventLogsCommand("SITE2"), new[] { "1" }, "Deployment");
var envelope = ScopedEnvelope(new QueryEventLogsCommand("SITE2"), new[] { "1" }, "Deployer");
actor.Tell(envelope);
@@ -651,7 +651,7 @@ public class ManagementActorTests : TestKit, IDisposable
AddSiteRepoWithSite(2, "SITE2");
var actor = CreateActor();
var envelope = ScopedEnvelope(new QueryParkedMessagesCommand("SITE2"), new[] { "1" }, "Deployment");
var envelope = ScopedEnvelope(new QueryParkedMessagesCommand("SITE2"), new[] { "1" }, "Deployer");
actor.Tell(envelope);
@@ -665,7 +665,7 @@ public class ManagementActorTests : TestKit, IDisposable
AddSiteRepoWithSite(2, "SITE2");
var actor = CreateActor();
var envelope = ScopedEnvelope(new RetryParkedMessageCommand("SITE2", "msg-1"), new[] { "1" }, "Deployment");
var envelope = ScopedEnvelope(new RetryParkedMessageCommand("SITE2", "msg-1"), new[] { "1" }, "Deployer");
actor.Tell(envelope);
@@ -679,7 +679,7 @@ public class ManagementActorTests : TestKit, IDisposable
AddSiteRepoWithSite(2, "SITE2");
var actor = CreateActor();
var envelope = ScopedEnvelope(new DiscardParkedMessageCommand("SITE2", "msg-1"), new[] { "1" }, "Deployment");
var envelope = ScopedEnvelope(new DiscardParkedMessageCommand("SITE2", "msg-1"), new[] { "1" }, "Deployer");
actor.Tell(envelope);
@@ -696,7 +696,7 @@ public class ManagementActorTests : TestKit, IDisposable
AddSiteRepoWithSite(2, "SITE2");
var actor = CreateActor();
var envelope = ScopedEnvelope(new DebugSnapshotCommand(9), new[] { "1" }, "Deployment");
var envelope = ScopedEnvelope(new DebugSnapshotCommand(9), new[] { "1" }, "Deployer");
actor.Tell(envelope);
@@ -808,12 +808,12 @@ public class ManagementActorTests : TestKit, IDisposable
public void QueryDeployments_WithDesignRole_ReturnsUnauthorized()
{
var actor = CreateActor();
var envelope = Envelope(new QueryDeploymentsCommand(), "Design");
var envelope = Envelope(new QueryDeploymentsCommand(), "Designer");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Deployment", response.Message);
Assert.Contains("Deployer", response.Message);
}
[Fact]
@@ -828,7 +828,7 @@ public class ManagementActorTests : TestKit, IDisposable
_services.AddScoped(_ => deployRepo);
var actor = CreateActor();
var envelope = Envelope(new QueryDeploymentsCommand(), "Deployment");
var envelope = Envelope(new QueryDeploymentsCommand(), "Deployer");
actor.Tell(envelope);
@@ -846,7 +846,7 @@ public class ManagementActorTests : TestKit, IDisposable
_services.AddScoped(_ => deployRepo);
var actor = CreateActor();
var envelope = Envelope(new QueryDeploymentsCommand(InstanceId: 5), "Deployment");
var envelope = Envelope(new QueryDeploymentsCommand(InstanceId: 5), "Deployer");
actor.Tell(envelope);
@@ -864,7 +864,7 @@ public class ManagementActorTests : TestKit, IDisposable
_services.AddScoped(_ => deployRepo);
var actor = CreateActor();
var envelope = ScopedEnvelope(new QueryDeploymentsCommand(InstanceId: 5), new[] { "1" }, "Deployment");
var envelope = ScopedEnvelope(new QueryDeploymentsCommand(InstanceId: 5), new[] { "1" }, "Deployer");
actor.Tell(envelope);
@@ -885,7 +885,7 @@ public class ManagementActorTests : TestKit, IDisposable
_services.AddScoped(_ => deployRepo);
var actor = CreateActor();
var envelope = ScopedEnvelope(new QueryDeploymentsCommand(InstanceId: 5), new[] { "1" }, "Deployment");
var envelope = ScopedEnvelope(new QueryDeploymentsCommand(InstanceId: 5), new[] { "1" }, "Deployer");
actor.Tell(envelope);
@@ -914,7 +914,7 @@ public class ManagementActorTests : TestKit, IDisposable
_services.AddScoped(_ => deployRepo);
var actor = CreateActor();
var envelope = ScopedEnvelope(new QueryDeploymentsCommand(), new[] { "1" }, "Deployment");
var envelope = ScopedEnvelope(new QueryDeploymentsCommand(), new[] { "1" }, "Deployer");
actor.Tell(envelope);
@@ -951,7 +951,7 @@ public class ManagementActorTests : TestKit, IDisposable
_services.AddScoped(_ => deployRepo);
var actor = CreateActor();
var envelope = ScopedEnvelope(new QueryDeploymentsCommand(), new[] { "1" }, "Deployment");
var envelope = ScopedEnvelope(new QueryDeploymentsCommand(), new[] { "1" }, "Deployer");
actor.Tell(envelope);
@@ -974,7 +974,7 @@ public class ManagementActorTests : TestKit, IDisposable
_services.AddScoped(_ => deployRepo);
var actor = CreateActor();
var envelope = ScopedEnvelope(new QueryDeploymentsCommand(), new[] { "1" }, "Admin", "Deployment");
var envelope = ScopedEnvelope(new QueryDeploymentsCommand(), new[] { "1" }, "Administrator", "Deployer");
actor.Tell(envelope);
@@ -1006,7 +1006,7 @@ public class ManagementActorTests : TestKit, IDisposable
// "Good" is valid, "Bogus" is not — the whole command must fail with
// nothing written.
var overrides = new Dictionary<string, string?> { ["Good"] = "1", ["Bogus"] = "2" };
var envelope = Envelope(new SetInstanceOverridesCommand(3, overrides), "Deployment");
var envelope = Envelope(new SetInstanceOverridesCommand(3, overrides), "Deployer");
actor.Tell(envelope);
@@ -1036,7 +1036,7 @@ public class ManagementActorTests : TestKit, IDisposable
var actor = CreateActor();
var overrides = new Dictionary<string, string?> { ["A"] = "1", ["B"] = "2" };
var envelope = Envelope(new SetInstanceOverridesCommand(4, overrides), "Deployment");
var envelope = Envelope(new SetInstanceOverridesCommand(4, overrides), "Deployer");
actor.Tell(envelope);
@@ -1095,7 +1095,7 @@ public class ManagementActorTests : TestKit, IDisposable
var actor = CreateActor();
var envelope = Envelope(
new UpdateSmtpConfigCommand(1, "new.example.com", 465, "Basic", "new@example.com", "SSL", "user:pass"),
"Design");
"Designer");
actor.Tell(envelope);
@@ -1125,7 +1125,7 @@ public class ManagementActorTests : TestKit, IDisposable
var actor = CreateActor();
var envelope = Envelope(
new UpdateSmtpConfigCommand(1, "new.example.com", 465, "Basic", "new@example.com"),
"Design");
"Designer");
actor.Tell(envelope);
@@ -1148,7 +1148,7 @@ public class ManagementActorTests : TestKit, IDisposable
.Returns((Template?)null);
var actor = CreateActor();
var envelope = Envelope(new CreateInstanceCommand("BadInst", 99, 1), "Deployment");
var envelope = Envelope(new CreateInstanceCommand("BadInst", 99, 1), "Deployer");
actor.Tell(envelope);
@@ -1228,12 +1228,12 @@ public class ManagementActorTests : TestKit, IDisposable
// ExportBundle requires the Design role; an Admin-only caller is rejected.
AddBundleSubstitutes();
var actor = CreateActor();
var envelope = Envelope(AllExportCommand(), "Admin");
var envelope = Envelope(AllExportCommand(), "Administrator");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Design", response.Message);
Assert.Contains("Designer", response.Message);
}
[Fact]
@@ -1244,12 +1244,12 @@ public class ManagementActorTests : TestKit, IDisposable
// configuration).
AddBundleSubstitutes();
var actor = CreateActor();
var envelope = Envelope(new PreviewBundleCommand("AA==", null), "Design");
var envelope = Envelope(new PreviewBundleCommand("AA==", null), "Designer");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Admin", response.Message);
Assert.Contains("Administrator", response.Message);
}
[Fact]
@@ -1257,12 +1257,12 @@ public class ManagementActorTests : TestKit, IDisposable
{
AddBundleSubstitutes();
var actor = CreateActor();
var envelope = Envelope(new ImportBundleCommand("AA==", null, "skip"), "Design");
var envelope = Envelope(new ImportBundleCommand("AA==", null, "skip"), "Designer");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Admin", response.Message);
Assert.Contains("Administrator", response.Message);
}
[Fact]
@@ -1286,7 +1286,7 @@ public class ManagementActorTests : TestKit, IDisposable
SourceEnvironment: "test-env");
var actor = CreateActor();
var envelope = Envelope(cmd, "Design");
var envelope = Envelope(cmd, "Designer");
actor.Tell(envelope);
@@ -1331,7 +1331,7 @@ public class ManagementActorTests : TestKit, IDisposable
// base64 check before reaching the importer.
var payload = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 });
var actor = CreateActor();
var envelope = Envelope(new ImportBundleCommand(payload, null, "skip"), "Admin");
var envelope = Envelope(new ImportBundleCommand(payload, null, "skip"), "Administrator");
actor.Tell(envelope);
@@ -1399,7 +1399,7 @@ public class ManagementActorTests : TestKit, IDisposable
var actor = CreateActor();
// "overwrite" policy so the final (Identical) row would otherwise differ
// from the Modified row's action — proves the last-write-wins semantics.
var envelope = Envelope(new ImportBundleCommand(payload, null, "overwrite"), "Admin");
var envelope = Envelope(new ImportBundleCommand(payload, null, "overwrite"), "Administrator");
actor.Tell(envelope);
@@ -1425,7 +1425,7 @@ public class ManagementActorTests : TestKit, IDisposable
var actor = CreateActor();
var envelope = Envelope(
new AddTemplateNativeAlarmSourceCommand(1, "Pressure", "Opc", "ns=2;s=T01", null, "desc", false),
"Design");
"Designer");
actor.Tell(envelope);
@@ -1440,12 +1440,12 @@ public class ManagementActorTests : TestKit, IDisposable
var actor = CreateActor();
var envelope = Envelope(
new AddTemplateNativeAlarmSourceCommand(1, "Pressure", "Opc", "ns=2;s=T01", null, null, false),
"Deployment");
"Deployer");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Design", response.Message);
Assert.Contains("Designer", response.Message);
}
[Fact]
@@ -1473,7 +1473,7 @@ public class ManagementActorTests : TestKit, IDisposable
var actor = CreateActor();
var envelope = Envelope(
new SetInstanceNativeAlarmSourceOverrideCommand(1, "Pressure", "Opc2", "ns=2;s=NEW", null),
"Deployment");
"Deployer");
actor.Tell(envelope);
@@ -1488,11 +1488,11 @@ public class ManagementActorTests : TestKit, IDisposable
var actor = CreateActor();
var envelope = Envelope(
new SetInstanceNativeAlarmSourceOverrideCommand(1, "Pressure", "Opc2", "ns=2;s=NEW", null),
"Design");
"Designer");
actor.Tell(envelope);
var response = ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
Assert.Contains("Deployment", response.Message);
Assert.Contains("Deployer", response.Message);
}
}