Files
lmxopcua/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/AdminProbeService.cs
T
Joseph Doherty 63331b0416 fix(security): per-instance credential references (#835) and confined OpcUaClient user-certificate path (#836)
#835: inside one driver namespace, a secret:<ns>/<X>/... reference whose first
segment names ANOTHER existing same-type driver instance (id or name,
case-insensitive; another-instance match beats an own-name match, so renaming
one's driver after a victim's unlocks nothing) is refused at save
(DriverConfigSaveGate.ValidateAsync), deploy (CredentialReferenceOtherInstance,
Heidenhain included), Test Connect (TestDriverConnect.DriverInstanceId, ConfigDb
read off the mailbox) and live browse (BrowserSessionService, fail-closed without
a ConfigDb). One rule: DriverCredentialPolicy.DescribeOtherInstanceProblems +
DriverInstanceIdentities. Shared names and the driver's own id stay valid; env:
is not instance-scoped. Running drivers are unchanged (warn-only window: the
deploy gate and the upgrade preflight / /raw banner list them).

#836: OpcUaClient UserCertificatePath is a relative path inside
Drivers:OpcUaClient:UserCertificateDirectory (default
<LocalAppData>/OtOpcUa/pki/user-certificates). Syntactic rule (CredentialFilePaths,
a DriverCredentialPolicy path field) at every #815 seam; at load time
ResolveSecretRefsAsync joins, canonicalises and requires lexical AND
link-resolved containment before the PFX password is resolved, and
LoadCertificate refuses an unresolved path. A running driver's pre-#836 absolute
path keeps loading with a rate-limited warning (LegacyCredentialReferenceTracker.ReportPath).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-11-11 03:52:48 -05:00

59 lines
2.8 KiB
C#

using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Clients;
/// <summary>
/// Thin AdminUI-side wrapper for the Test Connect operation. Dispatches a
/// <see cref="TestDriverConnect"/> through <c>IAdminOperationsClient</c>, applies a
/// 65-second outer wall (the actor itself clamps to [1,60]s; this guards against the
/// Ask never replying), and surfaces a friendly result for the Razor button to render.
/// </summary>
public sealed class AdminProbeService
{
private readonly IAdminOperationsClient _client;
/// <summary>Initializes a new instance of the <see cref="AdminProbeService"/>.</summary>
/// <param name="client">The admin operations client used to dispatch probe requests.</param>
public AdminProbeService(IAdminOperationsClient client) => _client = client;
/// <summary>
/// Dispatches a Test Connect probe for the supplied driver type and config JSON,
/// waiting up to 65 seconds for a reply before surfacing a timeout failure.
/// </summary>
/// <param name="driverType">Driver type key (must match an installed <c>IDriverProbe.DriverType</c>).</param>
/// <param name="configJson">Driver config as JSON (same shape as <c>DriverInstance.DriverConfig</c>).</param>
/// <param name="timeoutSeconds">Per-probe timeout; actor clamps to [1, 60].</param>
/// <param name="ct">Optional cancellation token from the caller.</param>
/// <param name="driverInstanceId">The owning driver instance (lmxopcua#835); null ⇒ a not-yet-created one.</param>
/// <returns>The probe result, or a timeout failure if no reply arrives in time.</returns>
public async Task<TestDriverConnectResult> TestAsync(
string driverType,
string configJson,
int timeoutSeconds,
CancellationToken ct = default,
string? driverInstanceId = null)
{
var correlationId = Guid.NewGuid();
var msg = new TestDriverConnect(driverType, configJson, timeoutSeconds, correlationId, driverInstanceId);
// 65s outer guard — the actor's CTS clamps to 60s; if the Ask never returns we still want
// a deterministic failure surface for the UI.
using var outerCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
outerCts.CancelAfter(TimeSpan.FromSeconds(65));
try
{
return await _client.AskAsync<TestDriverConnectResult>(msg, outerCts.Token);
}
catch (OperationCanceledException)
{
return new TestDriverConnectResult(false, "Probe request did not return within 65s.", null, correlationId);
}
catch (Exception ex)
{
return new TestDriverConnectResult(false, $"Probe dispatch failed: {ex.Message}", null, correlationId);
}
}
}