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:
@@ -61,6 +61,22 @@ public static class DriverFactoryBootstrap
|
||||
services.TryAddSingleton<RedundancyRoleView>();
|
||||
services.TryAddSingleton<IDriverPrimaryRoleSource>(sp => sp.GetRequiredService<RedundancyRoleView>());
|
||||
|
||||
// lmxopcua#765: the node-wide cached set of API keys with a Galaxy ack identity, read without I/O by every
|
||||
// Galaxy driver's synchronous ack pre-check. Listed from this node's Secrets store (optional here, so a
|
||||
// test graph without one gets a directory that always answers Unknown = fail open), refreshed by the
|
||||
// hosted loop below and early on identity-secret changes (SecretsRegistration decorates the invalidator).
|
||||
services.TryAddSingleton(sp =>
|
||||
{
|
||||
var options = sp.GetService<IOptions<GalaxyAckIdentityCacheOptions>>()?.Value ?? new GalaxyAckIdentityCacheOptions();
|
||||
return new Driver.Galaxy.Runtime.GalaxyAckIdentityDirectory(
|
||||
sp.GetService<ISecretStore>(), options.RefreshInterval, options.StaleAfter,
|
||||
sp.GetService<TimeProvider>(),
|
||||
sp.GetService<ILoggerFactory>()?.CreateLogger<Driver.Galaxy.Runtime.GalaxyAckIdentityDirectory>());
|
||||
});
|
||||
services.TryAddSingleton<Driver.Galaxy.Runtime.IGalaxyAckIdentityDirectory>(
|
||||
sp => sp.GetRequiredService<Driver.Galaxy.Runtime.GalaxyAckIdentityDirectory>());
|
||||
services.AddHostedService<GalaxyAckIdentityRefreshService>();
|
||||
|
||||
services.AddSingleton<DriverFactoryRegistry>(sp =>
|
||||
{
|
||||
var registry = new DriverFactoryRegistry();
|
||||
@@ -86,7 +102,7 @@ public static class DriverFactoryBootstrap
|
||||
// failures; without this it writes to NullLogger in production.
|
||||
ConfigureAbTransportLogging(sp);
|
||||
Register(registry, loggerFactory, secretResolver, primaryRoleSource, ResolveAbCipDefaultTransport(sp),
|
||||
scriptRoot, scripting);
|
||||
sp.GetRequiredService<Driver.Galaxy.Runtime.IGalaxyAckIdentityDirectory>(), scriptRoot, scripting);
|
||||
return registry;
|
||||
});
|
||||
services.AddSingleton<IDriverFactory>(sp =>
|
||||
@@ -215,6 +231,7 @@ public static class DriverFactoryBootstrap
|
||||
ISecretResolver secretResolver,
|
||||
IDriverPrimaryRoleSource primaryRoleSource,
|
||||
Driver.AbCip.AbCipTransport abCipDefaultTransport,
|
||||
Driver.Galaxy.Runtime.IGalaxyAckIdentityDirectory galaxyAckIdentities,
|
||||
ZB.MOM.WW.OtOpcUa.Core.Scripting.ScriptRootLogger? scriptRoot = null,
|
||||
ZB.MOM.WW.OtOpcUa.Core.Scripting.ScriptingOptions? scripting = null)
|
||||
{
|
||||
@@ -222,7 +239,7 @@ public static class DriverFactoryBootstrap
|
||||
Driver.AbLegacy.AbLegacyDriverFactoryExtensions.Register(registry, loggerFactory);
|
||||
Driver.Calculation.CalculationDriverFactoryExtensions.Register(registry, loggerFactory, scriptRoot, scripting);
|
||||
Driver.FOCAS.FocasDriverFactoryExtensions.Register(registry, loggerFactory);
|
||||
Driver.Galaxy.GalaxyDriverFactoryExtensions.Register(registry, secretResolver, loggerFactory);
|
||||
Driver.Galaxy.GalaxyDriverFactoryExtensions.Register(registry, secretResolver, loggerFactory, galaxyAckIdentities);
|
||||
Driver.Modbus.ModbusDriverFactoryExtensions.Register(registry, loggerFactory);
|
||||
Driver.Mqtt.MqttDriverFactoryExtensions.Register(registry, loggerFactory, primaryRoleSource);
|
||||
// Tier A (the Register default): fully managed — HttpClient + System.Xml.Linq, no native SDK,
|
||||
|
||||
Reference in New Issue
Block a user