feat(historian): OPC UA HistoryUpdate in the node manager + HistoryUpdate authorization (T6)
- OtOpcUaNodeManager overrides HistoryUpdateData / HistoryDeleteRawModified / HistoryDeleteAtTime, block-bridging to the optional IHistorianDataUpdater under the shared HistoryRead batch limiter + deadline. Insert/Replace/Update map to HistoryUpdateMode; Remove, IsDeleteModified, events and structure data are BadHistoryOperationUnsupported. Per-item codes stitched back in request order (MinValue SourceTimestamp => BadInvalidTimestamp, untypeable value => BadTypeMismatch); success clears the base's pre-seeded Unsupported error. - HistoryWrite AccessLevel bit OR-ed in for historized nodes when an updater is wired (raw + UNS); it is dispatch-load-bearing in the SDK base. OtOpcUaSdkServer.SetHistorianDataUpdater; the hosted service assigns it from an optional HistorianUpdaterHolder before SetSink, clears it on stop. - AuthorizeHistoryUpdate: ACL on => scoped OpcUaOperation.HistoryUpdate decision (replaces the role check); ACL off => #563 key re-check + new OpcUaDataPlaneRoles.HistoryUpdate role, conferred by ApiKeyInterimRoleMapper only from a Cluster-scoped HistoryUpdate grant (WriteOperate does not). - AdminUI ACL editor offers NodePermissions.HistoryUpdate. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -54,7 +54,7 @@ public static class ApiKeyInterimRoleMapper
|
||||
effective |= grant.PermissionFlags;
|
||||
}
|
||||
|
||||
var roles = new List<string>(2);
|
||||
var roles = new List<string>(3);
|
||||
if (effective.HasFlag(NodePermissions.WriteOperate)) roles.Add(OpcUaDataPlaneRoles.WriteOperate);
|
||||
|
||||
// AlarmAcknowledge is the flag an operator authors; AlarmAck is the role the Part 9 method gates
|
||||
@@ -62,6 +62,10 @@ public static class ApiKeyInterimRoleMapper
|
||||
// separately — mapping them would imply a granularity the gate does not have.
|
||||
if (effective.HasFlag(NodePermissions.AlarmAcknowledge)) roles.Add(OpcUaDataPlaneRoles.AlarmAck);
|
||||
|
||||
// HistoryUpdate has its own role: WriteOperate (live writes) never implies authority to rewrite
|
||||
// recorded history, so the role is conferred by the HistoryUpdate flag alone.
|
||||
if (effective.HasFlag(NodePermissions.HistoryUpdate)) roles.Add(OpcUaDataPlaneRoles.HistoryUpdate);
|
||||
|
||||
return roles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ILogger<OtOpcUaServerHostedService> _logger;
|
||||
private readonly IDriverPrimaryRoleSource? _primaryRoleSource;
|
||||
private readonly HistorianUpdaterHolder _historianUpdater;
|
||||
|
||||
private OpcUaApplicationHost? _appHost;
|
||||
private OtOpcUaSdkServer? _server;
|
||||
@@ -94,6 +95,9 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl
|
||||
/// Wired onto the node manager so a native-condition Acknowledge on a non-Primary node is refused rather than
|
||||
/// applied and then reverted. Optional: a node with no driver role registers none, and null means "allow" —
|
||||
/// the single-node posture <c>UnmanagedDriverPrimaryRoleSource</c> also answers with.</param>
|
||||
/// <param name="historianUpdater">The selected historian backend's optional HistoryUpdate capability.
|
||||
/// Optional: when unregistered it resolves to <see cref="HistorianUpdaterHolder.None"/> and HistoryUpdate is
|
||||
/// reported unsupported on every node.</param>
|
||||
public OtOpcUaServerHostedService(
|
||||
IOptions<OpcUaApplicationHostOptions> options,
|
||||
DeferredAddressSpaceSink deferredSink,
|
||||
@@ -108,7 +112,8 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl
|
||||
IOptions<AclOptions> aclOptions,
|
||||
TimeProvider time,
|
||||
ILoggerFactory loggerFactory,
|
||||
IDriverPrimaryRoleSource? primaryRoleSource = null)
|
||||
IDriverPrimaryRoleSource? primaryRoleSource = null,
|
||||
HistorianUpdaterHolder? historianUpdater = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(aclOptions);
|
||||
ArgumentNullException.ThrowIfNull(time);
|
||||
@@ -131,6 +136,7 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl
|
||||
_actorRegistry = actorRegistry;
|
||||
_loggerFactory = loggerFactory;
|
||||
_primaryRoleSource = primaryRoleSource;
|
||||
_historianUpdater = historianUpdater ?? HistorianUpdaterHolder.None;
|
||||
_logger = loggerFactory.CreateLogger<OtOpcUaServerHostedService>();
|
||||
}
|
||||
|
||||
@@ -168,6 +174,13 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl
|
||||
return;
|
||||
}
|
||||
|
||||
// The HistoryUpdate backend MUST be wired before SetSink: the node manager decides each historized
|
||||
// node's HistoryWrite AccessLevel bit at materialise time from whether an updater is present, and the
|
||||
// SDK base admits a node to HistoryUpdate only when that bit is set. Installing the real sink below is
|
||||
// what lets OpcUaPublishActor's deploy applies start materialising nodes, so assigning the updater
|
||||
// first guarantees every node sees it (nodes are never retro-updated).
|
||||
_server.SetHistorianDataUpdater(_historianUpdater.Updater);
|
||||
|
||||
_deferredSink.SetSink(new SdkAddressSpaceSink(_server.NodeManager));
|
||||
|
||||
// Wire the reverse-path inbound-alarm-command router: a client Acknowledge/Confirm/Shelve that
|
||||
@@ -371,6 +384,7 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl
|
||||
_server?.SetNativeAlarmAckPrimaryGate(null);
|
||||
// Restore the Null historian so a late HistoryRead doesn't hit a disposed read client.
|
||||
_server?.SetHistorianDataSource(null);
|
||||
_server?.SetHistorianDataUpdater(null);
|
||||
// Restore the empty authority so a late session activation cannot authorize against a stopping node.
|
||||
_server?.SetDataPlaneAuthority(null);
|
||||
// Detach the ACL gate too. Leaving it attached alongside an emptied authority would deny every late
|
||||
|
||||
Reference in New Issue
Block a user