feat(secrets): resolve MxGateway ApiKey secret: refs at connect time (ScadaBridge G-3 T9)

This commit is contained in:
Joseph Doherty
2026-07-16 15:29:03 -04:00
parent 56b428d2ef
commit 128f159692
4 changed files with 188 additions and 4 deletions
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
using ZB.MOM.WW.ScadaBridge.Commons.Serialization;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.Secrets.Abstractions;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
@@ -24,6 +25,7 @@ public class MxGatewayDataConnection : IDataConnection, IBrowsableDataConnection
{
private readonly IMxGatewayClientFactory _clientFactory;
private readonly ILogger<MxGatewayDataConnection> _logger;
private readonly ISecretResolver? _secretResolver;
private IMxGatewayClient? _client;
private ConnectionHealth _status = ConnectionHealth.Disconnected;
private CancellationTokenSource? _eventLoopCts;
@@ -51,10 +53,19 @@ public class MxGatewayDataConnection : IDataConnection, IBrowsableDataConnection
/// <summary>Initializes a new instance of <see cref="MxGatewayDataConnection"/>.</summary>
/// <param name="clientFactory">Factory used to create gateway client instances.</param>
/// <param name="logger">Logger instance.</param>
public MxGatewayDataConnection(IMxGatewayClientFactory clientFactory, ILogger<MxGatewayDataConnection> logger)
/// <param name="secretResolver">
/// Optional secret resolver used to resolve a <c>secret:&lt;name&gt;</c> ApiKey reference at
/// connect time. When null, a literal ApiKey still works, but a <c>secret:</c> reference
/// fails closed (a connection is never established with an unresolved key).
/// </param>
public MxGatewayDataConnection(
IMxGatewayClientFactory clientFactory,
ILogger<MxGatewayDataConnection> logger,
ISecretResolver? secretResolver = null)
{
_clientFactory = clientFactory;
_logger = logger;
_secretResolver = secretResolver;
}
/// <inheritdoc />
@@ -68,6 +79,25 @@ public class MxGatewayDataConnection : IDataConnection, IBrowsableDataConnection
{
var cfg = MxGatewayEndpointConfigSerializer.FromFlatDict(connectionDetails);
// Resolve a `secret:<name>` ApiKey reference BEFORE any teardown/client creation
// so a fail-closed throw here leaves adapter state untouched (no half-open client,
// previous connection — if any — still intact for a later retry). A literal key
// (no `secret:` prefix) passes through unchanged for back-compat.
var apiKey = cfg.ApiKey;
if (!string.IsNullOrEmpty(apiKey) &&
apiKey.StartsWith("secret:", StringComparison.OrdinalIgnoreCase))
{
if (_secretResolver is null)
{
throw new InvalidOperationException(
"MxGateway ApiKey uses a 'secret:' reference but no ISecretResolver is available to resolve it.");
}
var secretName = apiKey["secret:".Length..];
apiKey = await _secretResolver.GetAsync(new SecretName(secretName), cancellationToken)
?? throw new InvalidOperationException(
$"MxGateway ApiKey secret '{secretName}' could not be resolved (not found or tombstoned).");
}
// Reconnect reuses this adapter: cancel the previous event loop and dispose
// the previous client so a flapping gateway cannot accumulate orphaned
// streams/sockets, and the old loop's fault path cannot signal Disconnected
@@ -93,7 +123,7 @@ public class MxGatewayDataConnection : IDataConnection, IBrowsableDataConnection
await _client.ConnectAsync(new MxGatewayConnectionOptions(
cfg.Endpoint,
cfg.ApiKey,
apiKey,
string.IsNullOrWhiteSpace(cfg.ClientName) ? "scadabridge" : cfg.ClientName,
cfg.WriteUserId,
cfg.UseTls,
@@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
using ZB.MOM.WW.Secrets.Abstractions;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer;
@@ -13,6 +14,7 @@ public class DataConnectionFactory : IDataConnectionFactory
{
private readonly Dictionary<string, Func<IDictionary<string, string>, IDataConnection>> _factories = new(StringComparer.OrdinalIgnoreCase);
private readonly ILoggerFactory _loggerFactory;
private readonly ISecretResolver? _secretResolver;
/// <summary>
/// Initializes a new <see cref="DataConnectionFactory"/> with default OPC UA global options.
@@ -26,9 +28,19 @@ public class DataConnectionFactory : IDataConnectionFactory
/// </summary>
/// <param name="loggerFactory">Logger factory used when creating protocol adapters.</param>
/// <param name="opcUaGlobalOptions">Global OPC UA options applied to all OPC UA connections created by this factory.</param>
public DataConnectionFactory(ILoggerFactory loggerFactory, IOptions<OpcUaGlobalOptions> opcUaGlobalOptions)
/// <param name="secretResolver">
/// Optional secret resolver threaded into the MxGateway adapter so a per-endpoint
/// <c>secret:&lt;name&gt;</c> ApiKey reference is resolved at connect time. DI supplies it on
/// the Site container (where <see cref="ISecretResolver"/> is registered); direct
/// construction without it keeps literal-key behaviour.
/// </param>
public DataConnectionFactory(
ILoggerFactory loggerFactory,
IOptions<OpcUaGlobalOptions> opcUaGlobalOptions,
ISecretResolver? secretResolver = null)
{
_loggerFactory = loggerFactory;
_secretResolver = secretResolver;
var globalOptions = opcUaGlobalOptions.Value;
// Register built-in protocols.
@@ -44,7 +56,8 @@ public class DataConnectionFactory : IDataConnectionFactory
// flat details dict (see MxGatewayEndpointConfigSerializer).
RegisterAdapter("MxGateway", details => new MxGatewayDataConnection(
new RealMxGatewayClientFactory(_loggerFactory),
_loggerFactory.CreateLogger<MxGatewayDataConnection>()));
_loggerFactory.CreateLogger<MxGatewayDataConnection>(),
_secretResolver));
}
/// <summary>
@@ -17,6 +17,7 @@
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" />
<PackageReference Include="ZB.MOM.WW.MxGateway.Client" />
<PackageReference Include="ZB.MOM.WW.Secrets.Abstractions" />
<PackageReference Include="ZB.MOM.WW.Configuration" />
</ItemGroup>