docs(historian): pluggable backends, SQL Server backend, HistoryUpdate, outbox v2, spec corrections
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+13
-2
@@ -416,7 +416,9 @@ write and each Part 9 method call first asks `DataPlaneAuthorizer.AuthorizeKeyUs
|
||||
`AclGate: denied … key … is no longer enabled or has expired`), checked *before* the "no gateway ⇒
|
||||
`BadNotWritable`" availability answer. Things to know:
|
||||
|
||||
- **Only these two gates.** Read, Browse, Subscribe and HistoryRead stay ungated with the flag off —
|
||||
- **Only these two gates — plus HistoryUpdate.** (HistoryUpdate, added with the pluggable historian, is a third gated
|
||||
operation in both modes; see [HistoryUpdate](#historyupdate-historian-writes). It re-checks the key the same way.)
|
||||
Read, Browse, Subscribe and HistoryRead stay ungated with the flag off —
|
||||
that is the flag-off posture, and an expired key's session can still read and keep its subscriptions.
|
||||
Turn `Security:Acl:Enabled` on to have expiry reach those too.
|
||||
- **Anonymous is unaffected.** It carries no key id, so there is nothing to expire; its roles stay what
|
||||
@@ -484,6 +486,7 @@ the seams consult. Same dark-switch discipline as `MeshTransport`, `ConfigSource
|
||||
| Alarm Confirm | role `AlarmAck` | `AlarmConfirm` |
|
||||
| Alarm OneShotShelve / TimedShelve / Unshelve | role `AlarmAck` | `AlarmShelve` |
|
||||
| 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 |
|
||||
| Browse / TranslateBrowsePath | nothing | `Browse` on the source node, **and** on each returned reference |
|
||||
| CreateMonitoredItems / SubscribeToEvents | nothing | `Subscribe` on that node's scope, decided per item |
|
||||
@@ -872,7 +875,15 @@ 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.
|
||||
`HistoryUpdate` (bit 12) is a **separate** permission from `HistoryRead` so a read-only grant cannot also authorize OPC UA HistoryUpdate operations. `TriePermissionEvaluator` maps `OpcUaOperation.HistoryUpdate` to this bit. It is **not** included in any composite bundle (`ReadOnly` / `Operator` / `Engineer` / `Admin`) — it must be granted explicitly, and it is offered as its own checkbox in the AdminUI ACL editor (`AclEdit.razor`). The HistoryUpdate service itself is now implemented (SQL Server historian backend only) — see the next section.
|
||||
|
||||
### HistoryUpdate (historian writes)
|
||||
|
||||
HistoryUpdate rewrites recorded history, so unlike HistoryRead it is gated **whatever `Security:Acl:Enabled` says**, and `WriteOperate` (live writes) never implies it.
|
||||
|
||||
- **Flag off (default):** the session's API key is re-checked on the call (`DataPlaneAuthorizer.AuthorizeKeyUsable`, Gitea #563 — an expired or disabled key loses its activation-time roles), then the session must carry the **`HistoryUpdate` role** (`OpcUaDataPlaneRoles.HistoryUpdate`). `ApiKeyInterimRoleMapper` confers it **only from a `Cluster`-scoped grant** carrying `NodePermissions.HistoryUpdate` — narrower-scoped grants confer nothing, for the same reason as `WriteOperate` (honouring a flag without honouring its scope would widen it). A key holding only `WriteOperate` is denied `BadUserAccessDenied`.
|
||||
- **Flag on:** `DataPlaneAuthorizer.Authorize(identity, OpcUaOperation.HistoryUpdate, nodeId, realm)` decides per node and **replaces** the role check (never chained) — a session with no roles succeeds on the strength of a scoped `HistoryUpdate` grant, and a `WriteOperate` grant does not help.
|
||||
- **Availability is separate from authorization.** A node accepts HistoryUpdate only if it is historized **and** the selected historian backend has an updater when the node materialises (the `HistoryWrite` AccessLevel bit). `Backend=Gateway` has none, so it answers `BadHistoryOperationUnsupported` for every node whatever the grant. Operations, result codes and unsupported cases: [`docs/Historian.md` §HistoryUpdate](Historian.md#historyupdate).
|
||||
|
||||
### Evaluator — `PermissionTrie`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user