Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/TestAckCredentials.cs
T
Joseph Doherty dc0a24f224
v2-ci / unit-tests (push) Failing after 1h8m43s
feat(galaxy): operator-authenticated alarm ack — send Galaxy credential to mxaccessgw (#749)
mxaccessgw 488649c+ acknowledges by AuthenticateUser as the operator and
writing <alarm>.AckMsg, so AcknowledgeAlarmRequest now needs a real Galaxy
operator_user + operator_domain + operator_password. The OPC UA data plane is
API-key/anonymous and carries no Galaxy password, so the driver resolves the
account per ack:

1. the session's API key's Galaxy ack identity, set on AdminUI /apikeys and
   held ONLY in the encrypted Secrets store
   (otopcua/apikeys/<keyId>/galaxy-ack-{user,password}); a key with a user but
   no password secret is refused, never silently downgraded;
2. else the driver instance's AlarmAck fallback account (DriverConfig
   FallbackUser/FallbackDomain/FallbackPasswordSecretRef; secret:/${secret:}/
   env:/file:/dev: only — a literal is refused);
3. else DriverOperationRefusedException: nothing is sent, never an empty
   operator_user. Refusals are excluded from the resilience retry/breaker.

- Pins ZB.MOM.WW.MxGateway.Client/Contracts 0.2.1 -> 0.3.0 (vendored into
  nuget-packages/ from the Gitea feed, per the local-mxgw source mapping).
- The session ApiKeyId (not a secret) rides NativeAlarmAck ->
  RouteNativeAlarmAck -> RouteAlarmAck -> AlarmAcknowledgeRequest; the password
  is resolved in the driver and never travels through actor messages.
- The OPC UA principal + key id are appended to the AckMsg comment
  ("[OPC UA: <principal>, key <id>]") and logged, so a shared fallback
  account does not lose who acknowledged.
- Password never logged: GalaxyAckCredential.ToString omits it, the
  acknowledger scrubs it from every gateway message it echoes and withholds
  any exception that echoes it; audit rows carry only the key id.
- AdminUI: /apikeys "Ack identity" editor (set/remove; delete-key clears it),
  Galaxy driver form gains the fallback section.
- Synchronous OPC UA status for a refusal is follow-up #760 (the native ack
  path is fire-and-forget after the Part 9 method returns).

Tests: request mapping (password populated), password absent from logs and
exception text (mutation-checked), key-mapped / fallback / refused paths at
resolver and driver level, breaker non-trip, key-id propagation through node
manager and DriverHostActor, AdminUI service + form round-trip.

Closes #749
2026-11-06 16:23:00 -05:00

34 lines
1.5 KiB
C#

using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
/// <summary>
/// <see cref="IGalaxyAckCredentialResolver"/> test double that resolves every acknowledge to one fixed
/// credential (or refuses all), recording the key ids it was asked about.
/// </summary>
internal sealed class FixedAckCredentialResolver : IGalaxyAckCredentialResolver
{
private readonly GalaxyAckCredential? _credential;
private FixedAckCredentialResolver(GalaxyAckCredential? credential) => _credential = credential;
/// <summary>Gets the key ids the driver resolved for, in order.</summary>
public List<string?> RequestedKeyIds { get; } = [];
/// <summary>A resolver that always returns a fallback account <c>ACKDOM\ack-op</c> / <c>pw</c>.</summary>
public static FixedAckCredentialResolver Fallback() =>
new(new GalaxyAckCredential("ack-op", "ACKDOM", "pw", GalaxyAckCredentialSource.Fallback));
/// <summary>A resolver that refuses every acknowledge.</summary>
public static FixedAckCredentialResolver RefuseAll() => new(null);
/// <inheritdoc />
public Task<GalaxyAckCredentialResolution> ResolveAsync(string? apiKeyId, CancellationToken cancellationToken)
{
RequestedKeyIds.Add(apiKeyId);
return Task.FromResult(_credential is null
? GalaxyAckCredentialResolution.Refused("test refusal")
: GalaxyAckCredentialResolution.Resolved(_credential));
}
}