test(opcua): flag-OFF HistoryUpdate over the wire - Cluster HistoryUpdate grant allowed, WriteOperate alone denied

The rig's key roles are now settable; the test derives them with the
production ApiKeyInterimRoleMapper from the same grants it installs.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-09 14:31:01 -04:00
parent f8ad74dc19
commit 7fafeb433c
2 changed files with 55 additions and 6 deletions
@@ -6,6 +6,8 @@ using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
using ZB.MOM.WW.OtOpcUa.Host.OpcUa;
using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security;
using CoreHistoryReadResult = ZB.MOM.WW.OtOpcUa.Core.Abstractions.HistoryReadResult;
using static ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.ServerEventRig;
@@ -104,6 +106,47 @@ public sealed class HistoryUpdateOverTheWireTests
finally { await rig.DisposeAsync(); }
}
/// <summary>
/// ACL flag OFF over the wire: a key's grants collapse into server-wide roles at activation (the production
/// <see cref="ApiKeyInterimRoleMapper"/>). A key whose principal holds only a Cluster-scoped WriteOperate grant
/// is denied — live-write authority never implies rewriting history — and the same key with a Cluster-scoped
/// HistoryUpdate grant is admitted. The allow half keeps the deny from passing against a build that refuses
/// every HistoryUpdate with the flag off.
/// </summary>
[Fact]
public async Task Acl_off_a_cluster_history_update_grant_allows_and_write_operate_alone_is_denied()
{
var ct = TestContext.Current.CancellationToken;
var store = new SharedStore();
var rig = await StartAsync("HuWireAclOff", encrypted: false, ct);
try
{
rig.Nm.HistorianDataUpdater = store;
rig.Nm.HistorianDataSource = store;
var id = rig.EnsureRawVariable("Speed", "Double", writable: false, Tagname);
var writeOnly = ClusterGrant(KeyPrincipal, "acl-writer", NodePermissions.Browse | NodePermissions.Read | NodePermissions.WriteOperate);
rig.Enforce(false, [Key()], writeOnly);
rig.KeyRoles = ApiKeyInterimRoleMapper.Map([writeOnly], KeyPrincipal);
rig.KeyRoles.ShouldNotContain(OpcUaDataPlaneRoles.HistoryUpdate);
using (var writer = await rig.ConnectAsync(anonymous: false, ct))
{
var denial = await UpdateAsync(writer, RawNode(writer, id), PerformUpdateType.Insert, ct, (T1, 1.0));
denial.StatusCode.Code.ShouldBe(StatusCodes.BadUserAccessDenied);
}
store.UpdateCalls.ShouldBe(0, "a denied session never reaches the updater");
var editor = ClusterGrant(KeyPrincipal, "acl-editor", NodePermissions.Browse | NodePermissions.Read | NodePermissions.HistoryUpdate);
rig.Enforce(false, [Key()], editor);
rig.KeyRoles = ApiKeyInterimRoleMapper.Map([editor], KeyPrincipal);
using var allowed = await rig.ConnectAsync(anonymous: false, ct);
var ok = await UpdateAsync(allowed, RawNode(allowed, id), PerformUpdateType.Insert, ct, (T1, 1.0));
ok.OperationResults.Select(c => c.Code).ShouldBe([StatusCodes.GoodEntryInserted]);
store.UpdateCalls.ShouldBe(1);
}
finally { await rig.DisposeAsync(); }
}
/// <summary>The HistoryWrite bit is visible on both AccessLevel and UserAccessLevel when an updater is wired.</summary>
[Fact]
public async Task The_history_write_bit_is_visible_over_the_wire()