fix(security): a driver name may not claim another driver's credential segment, nor be renamed off its own (#848)
RawTreeService.CreateDriverAsync / RenameDriverAsync / UpdateDriverAsync run DriverConfigSaveGate.ValidateNameAsync -> DriverInstanceIdentities.DescribeNameChangeProblemsAsync: - claim: the new name may not be the instance part (secret: segment or env: part) of a reference STORED on another same-type driver's DriverConfig or DeviceConfig - naming a driver 'gw' used to fail every sharer's next deploy; - pin: a driver may not be renamed away from a name its own references are named after (unless they also name its id or the new name) - the rename would free that name for every other driver's save / Test Connect / browse. Re-pointing to the id in the same config-modal save is allowed; a case-only rename is never refused. Messages name the other driver, id, device and field path, never a value. The driver + device reads are pinned on SQL Server by a [SqlServerFact]. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,7 @@ public static class DriverConfigSaveGate
|
||||
/// plus the cross-instance rule (lmxopcua#835 — a <c>secret:galaxy/<X>/…</c> reference may not name ANOTHER
|
||||
/// existing instance of the same driver type), which needs the stored instance list.
|
||||
/// </summary>
|
||||
/// <param name="db">The config DB (read only when the config carries an in-scope <c>secret:</c> reference).</param>
|
||||
/// <param name="db">The config DB (read only when the config carries an in-scope <c>secret:</c>/<c>env:</c> reference).</param>
|
||||
/// <param name="driverType">The owning driver's type.</param>
|
||||
/// <param name="configJson">The DriverConfig or DeviceConfig JSON about to be stored.</param>
|
||||
/// <param name="ownDriverInstanceId">The owning driver; null when it is being created.</param>
|
||||
@@ -48,4 +48,28 @@ public static class DriverConfigSaveGate
|
||||
.DescribeOtherInstanceProblemsAsync(db, driverType, configJson, ownDriverInstanceId, ct).ConfigureAwait(false);
|
||||
return problems.Count == 0 ? null : string.Join(" ", problems);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The authority check <see cref="RawTreeService"/> runs on every driver create and rename (lmxopcua#848,
|
||||
/// <see cref="DriverInstanceIdentities.DescribeNameChangeProblemsAsync"/>): the new name may not be the instance part
|
||||
/// of another same-type driver's stored credential reference, and a rename may not free a name the driver's own
|
||||
/// references are named after.
|
||||
/// </summary>
|
||||
/// <param name="db">The config DB.</param>
|
||||
/// <param name="driverType">The driver's type.</param>
|
||||
/// <param name="ownDriverInstanceId">The driver being renamed; null on create.</param>
|
||||
/// <param name="currentName">Its stored name; null on create.</param>
|
||||
/// <param name="newName">The name about to be stored.</param>
|
||||
/// <param name="ownDriverConfig">The DriverConfig saved with it; null ⇒ the stored one.</param>
|
||||
/// <param name="ct">Cancellation.</param>
|
||||
/// <returns>The refusal (never containing a credential value), or null.</returns>
|
||||
public static async Task<string?> ValidateNameAsync(
|
||||
OtOpcUaConfigDbContext db, string? driverType, string? ownDriverInstanceId, string? currentName, string newName,
|
||||
string? ownDriverConfig, CancellationToken ct)
|
||||
{
|
||||
var problems = await DriverInstanceIdentities
|
||||
.DescribeNameChangeProblemsAsync(db, driverType, ownDriverInstanceId, currentName, newName, ownDriverConfig, ct)
|
||||
.ConfigureAwait(false);
|
||||
return problems.Count == 0 ? null : string.Join(" ", problems);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,6 +492,10 @@ public sealed class RawTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
if (await DriverConfigSaveGate.ValidateAsync(db, driverType, driverConfigJson, null, ct) is { } instanceRefusal)
|
||||
return new UnsMutationResult(false, instanceRefusal);
|
||||
|
||||
// lmxopcua#848: nor may its NAME be the instance part of another driver's stored reference.
|
||||
if (await DriverConfigSaveGate.ValidateNameAsync(db, driverType, null, null, name, driverConfigJson, ct) is { } nameRefusal)
|
||||
return new UnsMutationResult(false, nameRefusal);
|
||||
|
||||
if (!await db.ServerClusters.AnyAsync(c => c.ClusterId == clusterId, ct))
|
||||
return new UnsMutationResult(false, $"Cluster '{clusterId}' not found.");
|
||||
|
||||
@@ -549,6 +553,12 @@ public sealed class RawTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
&& d.Name == newName && d.DriverInstanceId != driverInstanceId, ct))
|
||||
return RawRenameResult.Failure($"A driver named '{newName}' already exists here.");
|
||||
|
||||
// lmxopcua#848: the new name may not be another driver's credential segment, nor may this rename free a name
|
||||
// the driver's own credential references are named after.
|
||||
if (await DriverConfigSaveGate.ValidateNameAsync(
|
||||
db, entity.DriverType, entity.DriverInstanceId, entity.Name, newName, null, ct) is { } nameRefusal)
|
||||
return RawRenameResult.Failure(nameRefusal);
|
||||
|
||||
var warnings = await BuildRenameWarningsAsync(db, await CollectTagsBeneathDriverAsync(db, driverInstanceId, ct), ct);
|
||||
|
||||
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion;
|
||||
@@ -1129,6 +1139,11 @@ public sealed class RawTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
&& d.Name == name && d.DriverInstanceId != driverInstanceId, ct))
|
||||
return new UnsMutationResult(false, $"A driver named '{name}' already exists here.");
|
||||
|
||||
// lmxopcua#848 — as RenameDriverAsync, judged against the config being saved with the rename.
|
||||
if (await DriverConfigSaveGate.ValidateNameAsync(
|
||||
db, entity.DriverType, entity.DriverInstanceId, entity.Name, name, driverConfigJson, ct) is { } nameRefusal)
|
||||
return new UnsMutationResult(false, nameRefusal);
|
||||
|
||||
db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion;
|
||||
entity.Name = name;
|
||||
entity.DriverConfig = string.IsNullOrWhiteSpace(driverConfigJson) ? "{}" : driverConfigJson;
|
||||
|
||||
Reference in New Issue
Block a user