fix(management): canonicalize role-mapping casing at write path, closing UI/actor case split (arch-review C5)

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 05:52:45 -04:00
parent 83e09fc210
commit 8221ee797e
3 changed files with 58 additions and 20 deletions
@@ -109,9 +109,47 @@ public class RoleMappingValidationTests : TestKit, IDisposable
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
Assert.NotNull(inserted);
// Membership check does not alter the stored value's casing (case-sensitivity
// asymmetry is a separately-deferred change).
// A correctly-cased canonical role round-trips unchanged.
Assert.Equal(Roles.Deployer, inserted!.Role);
Assert.Equal("SCADA-Deploy", inserted.LdapGroupName);
}
[Fact]
public void CreateRoleMapping_LowercaseRole_StoresCanonicalCasing()
{
// arch-review C5: ASP.NET RequireClaim policies compare Role ordinal
// case-sensitively while the actor compares OrdinalIgnoreCase. A row stored
// as "deployer" would pass every actor gate but fail every UI policy (a
// CLI-works/UI-403s split-brain). The write path now canonicalizes to the
// Roles.All casing, so the stored value is always "Deployer".
LdapGroupMapping? inserted = null;
_securityRepo
.When(r => r.AddMappingAsync(Arg.Any<LdapGroupMapping>(), Arg.Any<CancellationToken>()))
.Do(ci => inserted = ci.Arg<LdapGroupMapping>());
var actor = CreateActor();
actor.Tell(Envelope(new CreateRoleMappingCommand("SCADA-Deploy", "deployer")));
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
Assert.NotNull(inserted);
Assert.Equal("Deployer", inserted!.Role); // canonical, not the verbatim "deployer"
}
[Fact]
public void UpdateRoleMapping_LowercaseRole_StoresCanonicalCasing()
{
var existing = new LdapGroupMapping("SCADA-Deploy", Roles.Viewer) { Id = 7 };
_securityRepo.GetMappingByIdAsync(7, Arg.Any<CancellationToken>()).Returns(existing);
LdapGroupMapping? updated = null;
_securityRepo
.When(r => r.UpdateMappingAsync(Arg.Any<LdapGroupMapping>(), Arg.Any<CancellationToken>()))
.Do(ci => updated = ci.Arg<LdapGroupMapping>());
var actor = CreateActor();
actor.Tell(Envelope(new UpdateRoleMappingCommand(7, "SCADA-Deploy", "deployer")));
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
Assert.NotNull(updated);
Assert.Equal("Deployer", updated!.Role);
}
}