fix(authz): give HistoryUpdate its own NodePermissions bit (was aliased to HistoryRead) [H2]

This commit is contained in:
Joseph Doherty
2026-06-15 14:09:35 -04:00
parent 6ab3d8630b
commit c236263e8d
3 changed files with 38 additions and 1 deletions
@@ -191,6 +191,38 @@ public sealed class TriePermissionEvaluatorTests
"a session bound to a generation absent from the cache must fail closed");
}
/// <summary>Verifies that a HistoryRead-only grant does NOT authorize HistoryUpdate.</summary>
[Fact]
public void HistoryRead_grant_does_not_authorize_HistoryUpdate()
{
// Before the fix HistoryUpdate was mapped to the HistoryRead bit, so a read-only grant
// would wrongly authorise a write operation.
var evaluator = MakeEvaluator([Row("cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.HistoryRead)]);
var decision = evaluator.Authorize(Session(["cn=ops"]), OpcUaOperation.HistoryUpdate, Scope());
decision.IsAllowed.ShouldBeFalse("HistoryRead grant must NOT imply HistoryUpdate");
}
/// <summary>Verifies that a HistoryUpdate grant authorizes HistoryUpdate.</summary>
[Fact]
public void HistoryUpdate_grant_authorizes_HistoryUpdate()
{
var evaluator = MakeEvaluator([Row("cn=ops", NodeAclScopeKind.Cluster, null, NodePermissions.HistoryUpdate)]);
var decision = evaluator.Authorize(Session(["cn=ops"]), OpcUaOperation.HistoryUpdate, Scope());
decision.IsAllowed.ShouldBeTrue("HistoryUpdate grant must authorize HistoryUpdate operation");
}
/// <summary>Verifies that the HistoryUpdate bit is 1&lt;&lt;12 and does not collide with MethodCall.</summary>
[Fact]
public void HistoryUpdate_bit_value_and_no_collision()
{
((int)NodePermissions.HistoryUpdate).ShouldBe(1 << 12);
(NodePermissions.HistoryUpdate & NodePermissions.MethodCall).ShouldBe(NodePermissions.None);
}
/// <summary>Verifies that the operation-to-permission mapping is total.</summary>
[Fact]
public void OperationToPermission_Mapping_IsTotal()