diff --git a/docs/AlarmTracking.md b/docs/AlarmTracking.md index cc836c98..a0bc1588 100644 --- a/docs/AlarmTracking.md +++ b/docs/AlarmTracking.md @@ -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): diff --git a/docs/ScriptedAlarms.md b/docs/ScriptedAlarms.md index f437e58a..76334a42 100644 --- a/docs/ScriptedAlarms.md +++ b/docs/ScriptedAlarms.md @@ -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` 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 diff --git a/docs/security.md b/docs/security.md index e6e16dba..748fe58f 100644 --- a/docs/security.md +++ b/docs/security.md @@ -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/`: