271b96a644
I-1: Test Connect / browse configs may carry unsaved edits (the device modal merges the stored DriverConfig with the typed device config). The server (DriverInstanceIdentities.EffectiveOwnerAsync, used by AdminOperationsActor and BrowserSessionService) honours the named owner only when every endpoint value (new DriverCredentialRules.EndpointFields: Galaxy Endpoint, OpcUaClient EndpointUrl/EndpointUrls/DiscoveryUrl, Heidenhain Host/Port) is one that driver stores; otherwise the owner is null and its instance-named secrets are refused. Every Test Connect / browse whose config carries an in-scope secret:/env: reference is recorded (CredentialUseAudit: IAuditWriter event category CredentialUse + a structured Information line: user, driver, endpoint hosts, outcome). L-1: the UserCertificatePath rule is skipped only in an object that explicitly sets AuthType to non-Certificate (string or ordinal); the deploy gate also checks the runtime-merged driver+device config so the switch cannot be split from the path; the form no longer persists the hidden field. L-2: a ConfigDb fault in the browse check is a refusal; RawBrowseModal catches open failures into its error message. L-4: a relative UserCertificateDirectory resolves against AppContext.BaseDirectory. Co-Authored-By: Claude <noreply@anthropic.com>
107 lines
6.3 KiB
C#
107 lines
6.3 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
|
|
|
/// <summary>
|
|
/// Loads the driver-instance identities the cross-instance credential rule (lmxopcua#835,
|
|
/// <see cref="DriverCredentialPolicy.DescribeOtherInstanceProblems"/>) matches against — the ONE query the AdminUI save
|
|
/// path, live browse and Test Connect share (the deploy gate reads the same rows from its draft snapshot).
|
|
/// </summary>
|
|
public static class DriverInstanceIdentities
|
|
{
|
|
/// <summary>Every stored driver instance of <paramref name="driverType"/>, fleet-wide.</summary>
|
|
/// <param name="db">The config DB.</param>
|
|
/// <param name="driverType">The driver type (compared case-insensitively, client-side).</param>
|
|
/// <param name="ct">Cancellation.</param>
|
|
/// <returns>The identities.</returns>
|
|
public static async Task<IReadOnlyList<DriverInstanceIdentity>> LoadAsync(
|
|
OtOpcUaConfigDbContext db, string driverType, CancellationToken ct)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(db);
|
|
var rows = await db.DriverInstances.AsNoTracking()
|
|
.Select(d => new { d.DriverInstanceId, d.Name, d.DriverType })
|
|
.ToListAsync(ct).ConfigureAwait(false);
|
|
return rows
|
|
.Where(r => string.Equals(r.DriverType, driverType, StringComparison.OrdinalIgnoreCase))
|
|
.Select(r => new DriverInstanceIdentity(r.DriverInstanceId, r.Name, r.DriverType))
|
|
.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// #835 review I-1: the owner a Test Connect / browse config may claim. The caller names a driver, but the config can
|
|
/// carry UNSAVED edits (the device modal merges the stored DriverConfig with the typed device config), so a Designer
|
|
/// could type their own host as the endpoint of a victim's device and Test Connect it with the victim's own
|
|
/// <c>secret:galaxy/<victim-id>/…</c>. The claim is honoured only when the driver exists, has the same type, and every
|
|
/// endpoint value of the config is one it already STORES (DriverConfig or any DeviceConfig) — otherwise the owner is
|
|
/// null, i.e. the strict "new driver" treatment (no instance-named secrets at all).
|
|
/// </summary>
|
|
/// <param name="db">The config DB.</param>
|
|
/// <param name="driverType">The driver type of the request.</param>
|
|
/// <param name="configJson">The config about to be probed / browsed.</param>
|
|
/// <param name="claimedOwner">The driver the caller says the config belongs to.</param>
|
|
/// <param name="ct">Cancellation.</param>
|
|
/// <returns>The owner to use, or null.</returns>
|
|
public static async Task<string?> EffectiveOwnerAsync(
|
|
OtOpcUaConfigDbContext db, string? driverType, string? configJson, string? claimedOwner, CancellationToken ct)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(db);
|
|
if (string.IsNullOrEmpty(claimedOwner) || driverType is null) return null;
|
|
var driver = await db.DriverInstances.AsNoTracking()
|
|
.Where(d => d.DriverInstanceId == claimedOwner)
|
|
.Select(d => new { d.DriverType, d.DriverConfig })
|
|
.FirstOrDefaultAsync(ct).ConfigureAwait(false);
|
|
if (driver is null || !string.Equals(driver.DriverType, driverType, StringComparison.OrdinalIgnoreCase)) return null;
|
|
var deviceConfigs = await db.Devices.AsNoTracking()
|
|
.Where(d => d.DriverInstanceId == claimedOwner)
|
|
.Select(d => d.DeviceConfig)
|
|
.ToListAsync(ct).ConfigureAwait(false);
|
|
return DriverCredentialPolicy.EndpointsMatchStored(driverType, configJson, [driver.DriverConfig, .. deviceConfigs])
|
|
? claimedOwner
|
|
: null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The cross-instance problems of a Test Connect / browse config: <see cref="EffectiveOwnerAsync"/> first (an
|
|
/// unsaved endpoint does not get the owner's own secrets), then <see cref="DescribeOtherInstanceProblemsAsync"/>.
|
|
/// </summary>
|
|
/// <param name="db">The config DB.</param>
|
|
/// <param name="driverType">The driver type.</param>
|
|
/// <param name="configJson">The config.</param>
|
|
/// <param name="claimedOwner">The driver the caller names.</param>
|
|
/// <param name="ct">Cancellation.</param>
|
|
/// <returns>The problems.</returns>
|
|
public static async Task<IReadOnlyList<string>> DescribeProbeProblemsAsync(
|
|
OtOpcUaConfigDbContext db, string? driverType, string? configJson, string? claimedOwner, CancellationToken ct)
|
|
{
|
|
if (driverType is null || !DriverCredentialPolicy.HasInstanceSegments(driverType, configJson)) return [];
|
|
var owner = await EffectiveOwnerAsync(db, driverType, configJson, claimedOwner, ct).ConfigureAwait(false);
|
|
var problems = await DescribeOtherInstanceProblemsAsync(db, driverType, configJson, owner, ct).ConfigureAwait(false);
|
|
if (problems.Count == 0 || owner is not null || string.IsNullOrEmpty(claimedOwner)) return problems;
|
|
return
|
|
[
|
|
.. problems,
|
|
$"(The endpoint differs from what driver {claimedOwner} has saved, so its own secrets are not sent to it from an "
|
|
+ "unsaved form — save the change first; lmxopcua#835.)",
|
|
];
|
|
}
|
|
|
|
/// <summary>
|
|
/// The cross-instance problems of <paramref name="configJson"/> — reading the ConfigDb only when the config carries
|
|
/// an in-scope <c>secret:</c> reference at all.
|
|
/// </summary>
|
|
/// <param name="db">The config DB.</param>
|
|
/// <param name="driverType">The owning driver's type.</param>
|
|
/// <param name="configJson">The config about to be stored / probed / browsed.</param>
|
|
/// <param name="ownDriverInstanceId">The owning driver; null when it is being created.</param>
|
|
/// <param name="ct">Cancellation.</param>
|
|
/// <returns>The problems.</returns>
|
|
public static async Task<IReadOnlyList<string>> DescribeOtherInstanceProblemsAsync(
|
|
OtOpcUaConfigDbContext db, string? driverType, string? configJson, string? ownDriverInstanceId, CancellationToken ct)
|
|
{
|
|
if (driverType is null || !DriverCredentialPolicy.HasInstanceSegments(driverType, configJson)) return [];
|
|
var instances = await LoadAsync(db, driverType, ct).ConfigureAwait(false);
|
|
return DriverCredentialPolicy.DescribeOtherInstanceProblems(driverType, configJson, ownDriverInstanceId, instances);
|
|
}
|
|
}
|