fix(acl): authorize scripted alarm commands at their owning equipment's scope (#797)
Scripted condition NodeIds are opaque ScriptedAlarmIds, so splitting them as a topology path could never match an Equipment grant: a narrow equipment grant was denied while a Cluster grant passed. - OtOpcUaNodeManager binds each LIVE scripted condition instance to the EquipmentId it was materialised under (only when it hangs under that folder); dropped on remove / drop-recreate / equipment-subtree removal / rebuild; a same-id re-materialise under another equipment re-parents and re-binds. - DataPlaneAuthoritySnapshot carries EquipmentScopePaths (EquipmentId -> [uns, Area, Line, Equipment]) decoded from the artifact by the same AclScopePathResolver call that plants Equipment grants. - DataPlaneAuthorizer.AuthorizeScriptedCondition re-reads key, grant and generation per call (uncached) and evaluates at the owner path; unbound, replaced or unplaceable owners deny under every grant. No id-split or realm-root fallback. - Native conditions (RawPath) and the ACL-off role gate are unchanged. NodeIds and AlarmCommand.AlarmId are unchanged. Sdk2OpaqueScriptedScopeBaselineTests deliberately flipped: it pinned the denial and now pins owner-grant allow / sibling deny / cluster allow. Follow-up #808 (folder ids + Server-object event visibility for narrow UNS grants).
This commit is contained in:
@@ -561,6 +561,22 @@ the seams consult. Same dark-switch discipline as `MeshTransport`, `ConfigSource
|
||||
| Alarm Acknowledge / AddComment / Enable / Disable | role `AlarmAck` | `AlarmAcknowledge` on the condition's scope |
|
||||
| Alarm Confirm | role `AlarmAck` | `AlarmConfirm` |
|
||||
| Alarm OneShotShelve / TimedShelve / Unshelve | role `AlarmAck` | `AlarmShelve` |
|
||||
|
||||
**Whose scope a condition's command is decided at (Gitea #797).** A *native* condition's id is its raw tag's
|
||||
RawPath, so its commands are decided on that path (a `Tag` / `RawDevice` / … grant covering the raw tag), as
|
||||
before. A *scripted* condition's id is its opaque, operator-authored `ScriptedAlarmId` (e.g. `scripted-eqa-hi`)
|
||||
and is **never** split into a scope: `OtOpcUaNodeManager` binds each LIVE scripted condition instance to the
|
||||
`EquipmentId` it was materialised under (only when it actually hangs under that equipment's folder), and
|
||||
`DataPlaneAuthorizer.AuthorizeScriptedCondition` resolves that equipment to its `[uns, Area, Line, Equipment]`
|
||||
path through the CURRENT authority generation's `EquipmentScopePaths` (decoded from the artifact by the same
|
||||
`AclScopePathResolver` call that places an `Equipment` grant). So a grant on the owning equipment or any
|
||||
ancestor (line, area, UNS namespace, cluster) authorizes its scripted alarms; a sibling equipment's grant or a
|
||||
grant on one tag beneath the equipment does not. Fail-closed: an unbound condition (owner folder missing), a
|
||||
removed/replaced instance, or an owner the generation cannot place is denied for **every** grant, `Cluster`
|
||||
included — there is no realm-root or id-split fallback. The key, grant and generation are re-read on every call
|
||||
(uncached). NodeIds and `AlarmCommand.AlarmId` are unchanged. With the flag **off** none of this is consulted.
|
||||
⚠️ Read/Browse/Subscribe of UNS folders and scripted conditions, and Server-object event visibility, are still
|
||||
decided on the NodeId's split, and UNS folder ids are opaque entity ids — Gitea #808.
|
||||
| HistoryRead (all four variants) | **nothing** | `HistoryRead` on each node's scope, decided **per node in the batch** |
|
||||
| HistoryUpdate (insert / replace / update / delete) | role `HistoryUpdate` (from a `Cluster`-scoped grant) + key re-check | `HistoryUpdate` on that node's scope, decided **per node in the batch** |
|
||||
| Read (every attribute, not just Value) | nothing | `Read` on that node's scope, decided per node in the batch |
|
||||
|
||||
Reference in New Issue
Block a user