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:
+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