63331b0416
#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>
105 lines
6.1 KiB
C#
105 lines
6.1 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
/// <summary>
|
|
/// ⚠️ DEPRECATED compatibility path (lmxopcua#815) — the ONE switch between "enforce" and "warn-only" credential
|
|
/// resolution, and it is chosen by CALL SITE, never by a global flag: a deployed, running driver (Galaxy
|
|
/// <c>InitializeAsync</c>/<c>ReopenAsync</c> + ack fallback, OpcUaClient session open) passes its instance's tracker
|
|
/// and so keeps resolving a pre-#815 out-of-namespace reference with its OLD semantics, warning instead of
|
|
/// faulting; every admin-node path (Test Connect probe, AdminUI live browse) and Heidenhain pass
|
|
/// <see langword="null"/> and stay hard-fail.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Why it is safe to keep for running drivers: the AdminUI save path and the deploy gate refuse every
|
|
/// out-of-namespace reference, so a Designer can no longer CREATE one — what remains are configs deployed
|
|
/// before the upgrade, whose endpoint and reference an administrator already deployed. Such a config runs
|
|
/// only until its next deploy (which the gate refuses until it is migrated).
|
|
/// </para>
|
|
/// <para>
|
|
/// Removing it (the planned hard-fail release): delete this type, the per-driver <c>Legacy…</c> resolve
|
|
/// methods it gates and the <c>legacy</c> parameters; every caller then takes the enforce branch it already
|
|
/// has. Nothing else changes.
|
|
/// </para>
|
|
/// <para>
|
|
/// Each use is reported through <paramref name="warn"/> at most once per <see cref="WarnInterval"/> per field
|
|
/// (a Galaxy gateway reconnect re-resolves every time), and is exposed as <see cref="Diagnostics"/> so the
|
|
/// driver's <see cref="IDriverDiagnostics"/> bag carries it to the AdminUI (<c>/hosts</c> and the <c>/raw</c>
|
|
/// credential banner). Messages name the driver, field and allowed forms — never the value.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <param name="scope">The driver's scope (for the allowed forms).</param>
|
|
/// <param name="driverInstanceId">The driver instance (for messages).</param>
|
|
/// <param name="warn">Sink for a rate-limited warning line.</param>
|
|
/// <param name="clock">Clock (tests); null ⇒ <see cref="DateTime.UtcNow"/>.</param>
|
|
public sealed class LegacyCredentialReferenceTracker(
|
|
CredentialScope scope, string driverInstanceId, Action<string> warn, Func<DateTime>? clock = null)
|
|
{
|
|
/// <summary>The <see cref="Diagnostics"/> key prefix (one entry per field).</summary>
|
|
public const string DiagnosticsKeyPrefix = "credential-compat:";
|
|
|
|
/// <summary>Minimum spacing between two warnings for the same field.</summary>
|
|
public static readonly TimeSpan WarnInterval = TimeSpan.FromHours(1);
|
|
|
|
private readonly object _gate = new();
|
|
private readonly Dictionary<string, DateTime> _lastWarned = new(StringComparer.Ordinal);
|
|
private readonly SortedDictionary<string, string> _diagnostics = new(StringComparer.Ordinal);
|
|
|
|
/// <summary>The scope this tracker reports against.</summary>
|
|
public CredentialScope Scope => scope;
|
|
|
|
/// <summary>The current legacy uses, keyed <c>credential-compat:<field></c>.</summary>
|
|
public IReadOnlyDictionary<string, string> Diagnostics
|
|
{
|
|
get
|
|
{
|
|
lock (_gate) return new Dictionary<string, string>(_diagnostics, StringComparer.Ordinal);
|
|
}
|
|
}
|
|
|
|
/// <summary>Records that <paramref name="field"/> was resolved through the legacy path (warns, rate-limited).</summary>
|
|
/// <param name="field">The field label.</param>
|
|
public void Report(string field)
|
|
{
|
|
var forms = $"secret:{scope.SecretNamespaces[0]}<name> or env:{scope.EnvVariablePrefixes[0]}<NAME>";
|
|
Record(
|
|
field,
|
|
$"out-of-namespace ref still resolved (#815 warn-only, deprecated); next deploy refuses it — use {forms}",
|
|
$"{scope.Label} driver '{driverInstanceId}': {field} is a credential reference outside the {scope.Label} "
|
|
+ $"namespace. It is still resolved for this already-deployed driver (lmxopcua#815 warn-only "
|
|
+ $"compatibility — deprecated, a later release fails closed) but the deploy gate refuses it: migrate "
|
|
+ $"it to {forms} before the next deploy. See docs/security.md §\"Credential references are namespaced per driver\".");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Records that the key-material file field <paramref name="field"/> (lmxopcua#836, e.g. OpcUaClient
|
|
/// <c>UserCertificatePath</c>) was loaded from a path outside the node's certificate directory through the same
|
|
/// warn-only window (warns, rate-limited). Never carries the path.
|
|
/// </summary>
|
|
/// <param name="field">The field label.</param>
|
|
/// <param name="directoryDescription">How the operator configures the directory.</param>
|
|
public void ReportPath(string field, string directoryDescription) =>
|
|
Record(
|
|
field,
|
|
$"path outside the certificate directory still loaded (#836 warn-only, deprecated); next deploy refuses it — "
|
|
+ $"move the file under {directoryDescription} and author it as a relative path",
|
|
$"{scope.Label} driver '{driverInstanceId}': {field} is not a relative path inside the node's certificate "
|
|
+ $"directory ({directoryDescription}). It is still loaded for this already-deployed driver (lmxopcua#836 "
|
|
+ "warn-only compatibility — deprecated, a later release fails closed) but the deploy gate refuses it: move "
|
|
+ "the file into that directory and set the field to its relative path before the next deploy. See "
|
|
+ "docs/security.md §\"Key-material file paths are confined to a node directory\".");
|
|
|
|
private void Record(string field, string diagnostic, string warning)
|
|
{
|
|
var now = (clock ?? (() => DateTime.UtcNow))();
|
|
bool log;
|
|
lock (_gate)
|
|
{
|
|
_diagnostics[DiagnosticsKeyPrefix + field] = diagnostic;
|
|
log = !_lastWarned.TryGetValue(field, out var last) || now - last >= WarnInterval;
|
|
if (log) _lastWarned[field] = now;
|
|
}
|
|
|
|
if (log) warn(warning);
|
|
}
|
|
}
|