docs(alarms): OPC UA Enable/Disable wired + native-ack→AVEVA with principal + HistoryUpdate permission bit

This commit is contained in:
Joseph Doherty
2026-06-15 14:59:10 -04:00
parent 30315185a3
commit db22c2b19a
3 changed files with 64 additions and 9 deletions
+36
View File
@@ -79,6 +79,42 @@ the dedup.
## Acknowledge routing — Galaxy / driver alarms
### Native alarm acknowledge → AVEVA
When an OPC UA client Acknowledges a **native** (driver-fed, e.g. Galaxy)
`AlarmConditionState` node, the node manager's `OnAcknowledge` handler
branches on native-ness and routes through a dedicated path — separate
from the scripted `AlarmCommandRouter`:
1. **`OtOpcUaNodeManager.HandleNativeAlarmAck`** — gates on the caller's
`AlarmAck` role (fails closed: no role → `BadUserAccessDenied`), then
dispatches a `NativeAlarmAck(ConditionNodeId, Comment, OperatorUser)`
to the `NativeAlarmAckRouter` seam (fire-and-forget, non-blocking under
the node-manager Lock). `OperatorUser` carries the authenticated
session principal's display name.
2. **`DriverHostActor.HandleRouteNativeAlarmAck`** — receives a
`RouteNativeAlarmAck` message (the host maps `NativeAlarmAck` at the
wiring boundary to keep Runtime Akka-free of the OPC UA layer). Applied
**Primary-gate first**: a Secondary or Detached node drops the message
silently. On Primary, resolves the condition NodeId from the
`_driverRefByAlarmNodeId` inverse map (NodeId → `(DriverInstanceId,
FullName)`) and Tells the owning `DriverInstanceActor` a
`RouteAlarmAck(FullName, Comment, OperatorUser)`.
3. **Galaxy driver**`DriverInstanceActor` calls the driver's
`IAlarmSource.AcknowledgeAsync` with an `AlarmAcknowledgeRequest`
carrying the authored `FullName` as the `ConditionId` and the
authenticated `OperatorUser`. The driver forwards this to the Galaxy
gateway → AVEVA via `GatewayGalaxyAlarmAcknowledger`.
**Fire-and-forget** — a failed upstream ack is not surfaced back to the
OPC UA client (mirrors the Galaxy write-outcome limitation; the local
`AlarmConditionState` SDK update already committed at step 1).
Only the **Acknowledge** is routed to the driver. `Confirm` / `AddComment`
/ `Shelve` operations on a native condition stay on the scripted
`AlarmCommandRouter` path (Phase 3 scope is Acknowledge → AVEVA only).
### Legacy sub-attribute path
`DriverNodeManager` picks the acknowledger when registering each
condition (PR B.3 logic):
+25 -9
View File
@@ -109,7 +109,7 @@ Every mutation the state machine produces is immediately persisted inside the en
Two mapping notes specific to this adapter:
- `SubscribeAlarmsAsync` accepts a list of source-node-id filters, interpreted as Equipment-path prefixes. Empty list matches every alarm. Each emission is matched against every live subscription — the adapter keeps no per-subscription cursor.
- `IAlarmSource.AcknowledgeAsync` does not carry a user identity. The adapter defaults the audit user to `"opcua-client"` so callers using the base interface still produce an audit entry. The server's Part 9 method handlers call the engine's richer `AcknowledgeAsync` / `ConfirmAsync` / `OneShotShelveAsync` / `TimedShelveAsync` / `UnshelveAsync` / `AddCommentAsync` directly with the authenticated principal instead.
- `IAlarmSource.AcknowledgeAsync` accepts an `AlarmAcknowledgeRequest` list; the `OperatorUser` field carries the authenticated principal when available. The adapter passes `OperatorUser` through to the engine's `AcknowledgeAsync`; when `OperatorUser` is null (non-OPC-UA callers using the raw interface) it falls back to `"opcua-client"` so callers still produce an audit entry. The server's Part 9 method handlers call the engine's richer `AcknowledgeAsync` / `ConfirmAsync` / `OneShotShelveAsync` / `TimedShelveAsync` / `UnshelveAsync` / `AddCommentAsync` directly with the authenticated principal instead of going through this adapter.
## Native driver alarms (equipment-tag path)
@@ -193,15 +193,27 @@ The alarm is authored on the `Tags` tab of the equipment page (`/uns/equipment/{
by editing the tag's raw `TagConfig` JSON to include the `"alarm"` object. No
other configuration is required.
### Deferred follow-ups
### Native-alarm OPC UA operator operations
Two items are explicitly out of scope for Phase B:
An OPC UA client can **Acknowledge** a native (e.g. Galaxy) condition and the ack
now propagates to the device. The `OnAcknowledge` handler on a native condition
routes through a separate `NativeAlarmAckRouter` seam (instead of the scripted
`AlarmCommandRouter`) → `DriverHostActor` (a condition NodeId → `(DriverInstanceId,
FullName)` inverse map, Primary-gated) → the owning driver's
`IAlarmSource.AcknowledgeAsync` → Galaxy gateway → AVEVA. The call carries the
authenticated operator's display name via the `AlarmAcknowledgeRequest.OperatorUser`
field. This is fire-and-forget — a failed upstream ack is not surfaced back to the
OPC UA client (mirrors the Galaxy write-outcome limitation). See [AlarmTracking.md
§Native alarm acknowledge → AVEVA](AlarmTracking.md#native-alarm-acknowledge--aveva)
for the full routing diagram.
1. **Inbound device-ack**: an OPC UA client `Acknowledge` currently updates the
local `AlarmConditionState` but does **not** yet propagate back to the device
via `IAlarmSource.AcknowledgeAsync` (→ AVEVA). Device-ack round-trip is a
deferred follow-up.
2. **AdminUI Galaxy address-picker pre-fill**: the `alarm` object must be authored
**Enable/Disable** on a native condition returns `BadNotSupported` — the driver
backing a native alarm has no enable/disable surface distinct from OPC UA; the
Part 9 enable/disable concept maps to the scripted-alarm engine only.
One item remains explicitly out of scope:
1. **AdminUI Galaxy address-picker pre-fill**: the `alarm` object must be authored
by editing the tag's raw `TagConfig` JSON today; a future picker enhancement
could pre-fill `alarmType` / `severity` from driver discovery
(`DriverAttributeInfo.IsAlarm`).
@@ -212,10 +224,14 @@ Operators interact with active scripted alarms through two surfaces — both con
### AlarmAck gate (OPC UA method path)
`OtOpcUaNodeManager` wires the OPC UA Part 9 condition methods (Acknowledge / Confirm / AddComment / OneShotShelve / TimedShelve / Unshelve) on each `AlarmConditionState` node. Every method call is gated on the `AlarmAck` LDAP role — fail-closed: a session with no resolved roles or no `AlarmAck` group membership receives `BadUserAccessDenied` immediately without reaching the engine. The role is carried on the session by `RoleCarryingUserIdentity` (a `UserIdentity` subclass that preserves the LDAP-resolved role set past `OpcUaApplicationHost`).
`OtOpcUaNodeManager` wires the OPC UA Part 9 condition methods (Acknowledge / Confirm / AddComment / OneShotShelve / TimedShelve / Unshelve / Enable / Disable) on each `AlarmConditionState` node. Every method call is gated on the `AlarmAck` LDAP role — fail-closed: a session with no resolved roles or no `AlarmAck` group membership receives `BadUserAccessDenied` immediately without reaching the engine. The role is carried on the session by `RoleCarryingUserIdentity` (a `UserIdentity` subclass that preserves the LDAP-resolved role set past `OpcUaApplicationHost`).
On allow, the handler publishes a `Commons.OpcUa.AlarmCommand` (containing command kind, condition id, comment, and operator principal) onto the `alarm-commands` DPS topic. The node manager itself stays Akka-free: the dispatch action is a settable `Action<AlarmCommand>` injected at boot by the hosted service.
**Scripted vs native conditions — Enable/Disable and Acknowledge:**
- **Scripted conditions** — all eight Part 9 operations (including Enable/Disable) route through `AlarmCommandRouter` onto the `alarm-commands` topic, which `ScriptedAlarmHostActor` dispatches to the engine (`EnableAsync` / `DisableAsync` / `AcknowledgeAsync` / …). On enable, `ActiveState` is re-derived from the next predicate evaluation.
- **Native (driver-fed) conditions** — `OnAcknowledge` branches to `NativeAlarmAckRouter` and routes the ack to the owning driver rather than the scripted engine (see §[Native-alarm OPC UA operator operations](#native-alarm-opc-ua-operator-operations) above). `OnEnableDisable` returns `BadNotSupported` immediately — native conditions have no engine-side enable/disable surface.
`OnTimedUnshelve` (the SDK's internal auto-unshelve timer) bypasses the client gate — it is system-initiated and not subject to operator role checks.
### Delta-gate de-duplication
+3
View File
@@ -205,6 +205,7 @@ public enum NodePermissions : int
AlarmConfirm = 1 << 9,
AlarmShelve = 1 << 10,
MethodCall = 1 << 11,
HistoryUpdate = 1 << 12, // OPC UA annotation / insert / delete (separate from HistoryRead)
ReadOnly = Browse | Read | Subscribe | HistoryRead | AlarmRead,
Operator = ReadOnly | WriteOperate | AlarmAcknowledge | AlarmConfirm,
@@ -215,6 +216,8 @@ public enum NodePermissions : int
The three Write tiers map to Galaxy's v1 `SecurityClassification``FreeAccess`/`Operate``WriteOperate`, `Tune``WriteTune`, `Configure``WriteConfigure`. `SecuredWrite` / `VerifiedWrite` / `ViewOnly` classifications remain read-only from OPC UA regardless of grant.
`HistoryUpdate` (bit 12) is a **separate** permission from `HistoryRead` so a read-only grant cannot also authorize OPC UA HistoryUpdate (annotation / insert / delete) operations. `TriePermissionEvaluator` maps `OpcUaOperation.HistoryUpdate` to this bit, closing the read⇒update hole that existed when the only history permission was `HistoryRead`. Note: `HistoryUpdate` is **not** included in any composite bundle (`ReadOnly` / `Operator` / `Engineer` / `Admin`) because the HistoryUpdate service surface (the actual insert/replace/delete backend RPC) is not yet implemented — a client calling HistoryUpdate still receives the SDK's default reject regardless of the grant.
### Evaluator — `PermissionTrie`
`src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/`: