fix(alarms): refuse a keyed native ack with no Galaxy identity synchronously, from a cached key set (lmxopcua#765)
v2-ci / unit-tests (push) Waiting to run
v2-ci / unit-tests (push) Waiting to run
Item 3 of #765. A keyed (API-key) session whose key had no Galaxy ack identity got Good and then a revert, because whether the key has an identity lives in the Secrets store, which must not be read under the SDK lock. GalaxyAckIdentityDirectory (Driver.Galaxy) keeps a node-wide set of the key ids that have a galaxy-ack-user secret. It lists this node's Secrets store (metadata only, nothing decrypted) and is read lock-free. GalaxyDriver.GetAcknowledgeRefusal (IAlarmAcknowledgePreflight, published through NativeAlarmAckPreflightView as in #760) now refuses a keyed session whose key the directory reports Absent, when the driver has no fallback ack account. A refusal also asks for a refresh. Fails open: before the first refresh, once the snapshot is older than StaleAfter, and from a signalled identity-secret change until the next refresh, the lookup answers Unknown and the ack takes the old Good-then-revert path. Refresh: GalaxyAckIdentityRefreshService runs on driver nodes, every Drivers:Galaxy:AckIdentityCache:RefreshIntervalSeconds (default 30; StaleAfterSeconds 0 = three intervals; validated at start). It also refreshes early on secret changes. The Secrets library has no change event, so SecretsRegistration decorates ISecretCacheInvalidator, which is called by AdminUI /api-keys, /admin/secrets and the replication appliers. API-key changes need no event: a revoked key is already denied before the pre-check. Tests: Galaxy.Tests GalaxyAckIdentityDirectoryTests (16) and Host.Tests GalaxyAckIdentityCacheWiringTests (9). Seven separate mutations each turn at least one test red: pre-check ignores the directory, factory drops it, no invalidator decoration, no staleness check, no change-pending check, no refresh on refusal, loop never wakes early. Docs: Galaxy.md, AlarmTracking.md, Configuration.md, .claude/rules/alarms.md. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -468,8 +468,10 @@ from the scripted `AlarmCommandRouter`:
|
||||
not yet spawned) or is a stub on this node is reverted the same way by
|
||||
the host itself (#765). Only an ack still pending in `NativeAlarmProjector` is
|
||||
reverted: a driver transition projected since then is authoritative. A
|
||||
refusal the driver can decide without I/O (Galaxy: an anonymous session
|
||||
and no fallback ack account) is instead returned synchronously as
|
||||
refusal the driver can decide without I/O (Galaxy, with no fallback ack
|
||||
account: an anonymous session, or a keyed session whose key the cached
|
||||
`GalaxyAckIdentityDirectory` knows has no ack identity — #765) is instead
|
||||
returned synchronously as
|
||||
`BadUserAccessDenied` by the node manager's `NativeAlarmAckPreflight`
|
||||
before anything is applied — as is every native ack while the driver host
|
||||
is Stale (configuration database unreachable at boot), when no ack can be
|
||||
|
||||
@@ -536,6 +536,21 @@ deploy that recreates the driver), not on `ReinitializeAsync`. The init log line
|
||||
`transport=Libplctag (node default)` when the pin is what selected the stack. There is **no AbLegacy
|
||||
counterpart**: an AbLegacy driver with no key always runs libplctag (Native is opt-in per driver).
|
||||
|
||||
### `Drivers:Galaxy:AckIdentityCache` (cached API-key → Galaxy ack identity set, #765)
|
||||
|
||||
- **Purpose:** the node-wide set of API key ids that have a Galaxy ack identity (a `galaxy-ack-user` secret),
|
||||
which the Galaxy driver's synchronous ack pre-check reads to refuse a keyed session without one
|
||||
(`BadUserAccessDenied`) instead of returning Good and reverting. See
|
||||
[`drivers/Galaxy.md` § Alarm acknowledge credential](drivers/Galaxy.md#alarm-acknowledge-credential).
|
||||
- **Options class:** `GalaxyAckIdentityCacheOptions` (`SectionName = "Drivers:Galaxy:AckIdentityCache"`) —
|
||||
`src/Server/ZB.MOM.WW.OtOpcUa.Host/Configuration/GalaxyAckIdentityCacheOptions.cs`; bound and validated
|
||||
in `Program.cs`. The directory and its refresh loop exist on driver-role nodes only.
|
||||
|
||||
| Key | Type | Default | Meaning |
|
||||
|---|---|---|---|
|
||||
| `RefreshIntervalSeconds` | int | `30` | How often the Secrets store is re-listed (1..3600). An in-process identity change or a refusal also triggers an early refresh. |
|
||||
| `StaleAfterSeconds` | int | `0` | Snapshot age after which keyed acks fall back to Good-then-revert (fail open). `0` = three refresh intervals; otherwise must exceed `RefreshIntervalSeconds`. A bad value fails host start. |
|
||||
|
||||
---
|
||||
|
||||
## Environment variables
|
||||
|
||||
+17
-6
@@ -108,12 +108,23 @@ Galaxy password, so `GalaxyAckCredentialResolver` picks the account per acknowle
|
||||
3. **Neither** → the driver throws `DriverOperationRefusedException`; **nothing is sent to the
|
||||
gateway**. The refusal is excluded from the resilience breaker (it says nothing about gateway
|
||||
health). How the client learns of it (#760):
|
||||
- **Synchronously** when the driver can tell without I/O — an *anonymous* session on a driver with
|
||||
no fallback account: the Part 9 `Acknowledge` call itself fails `BadUserAccessDenied` with the
|
||||
reason, and nothing is applied (`IAlarmAcknowledgePreflight`, published per condition by
|
||||
`DriverHostActor` into `NativeAlarmAckPreflightView`).
|
||||
- **By revert** otherwise — a keyed session whose key has no identity (that lives in the Secrets
|
||||
store, which is not read under the SDK lock), or MXAccess rejecting the account (a non-zero
|
||||
- **Synchronously** when the driver can tell without I/O, on a driver with no fallback account —
|
||||
an *anonymous* session (#760), or a *keyed* session whose key has no identity (#765): the Part 9
|
||||
`Acknowledge` call itself fails `BadUserAccessDenied` with the reason, and nothing is applied
|
||||
(`IAlarmAcknowledgePreflight`, published per condition by `DriverHostActor` into
|
||||
`NativeAlarmAckPreflightView`). The keyed case reads `GalaxyAckIdentityDirectory`, a node-wide
|
||||
cached set of the key ids that have a `galaxy-ack-user` secret, listed from this node's Secrets
|
||||
store (metadata only — nothing decrypted) every `Drivers:Galaxy:AckIdentityCache:RefreshIntervalSeconds`
|
||||
(default 30) by `GalaxyAckIdentityRefreshService`, and early whenever an identity secret changes
|
||||
in-process (the Secrets cache invalidator is decorated, so AdminUI `/api-keys`, `/admin/secrets` and
|
||||
replication all signal it) or a refusal asks for one. ⚠️ It **fails open**: before the first refresh,
|
||||
once the snapshot is older than `StaleAfterSeconds` (default three intervals — e.g. the store cannot
|
||||
be listed), and from a signalled identity change until the refresh that follows it, every keyed ack
|
||||
takes the revert path below instead of being refused. An identity written straight into the store
|
||||
by the `secret` CLI is seen at the next refresh; the refusal itself requests one, so a retry a moment
|
||||
later goes through.
|
||||
- **By revert** otherwise — a keyed session the directory cannot vouch for (see above), a key whose
|
||||
identity is broken (a user with no password secret), or MXAccess rejecting the account (a non-zero
|
||||
native `hresult`, e.g. a wrong password, now thrown as a refusal): the call returned `Good`, then
|
||||
`DriverHostActor` re-projects the condition **unacknowledged** (a fresh condition event,
|
||||
`Retain=true`) and logs the reason. A driver transition projected after the ack wins over a late
|
||||
|
||||
Reference in New Issue
Block a user