fix(security): preflight reads the latest sealed artifact; byte[] length predicate untranslatable on SQL Server (#815 live gate)
This commit is contained in:
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
||||
|
||||
@@ -53,12 +54,15 @@ public sealed class CredentialReferencePreflight(IDbContextFactory<OtOpcUaConfig
|
||||
var devices = ids.Count == 0
|
||||
? []
|
||||
: await db.Devices.AsNoTracking().Where(d => ids.Contains(d.DriverInstanceId)).ToListAsync(ct).ConfigureAwait(false);
|
||||
// The latest SEALED deployment is what the fleet runs (a failed or in-flight one is not). The empty-blob check
|
||||
// stays client-side: `byte[].Length` in a predicate is not translatable on SQL Server (#815 live gate — the
|
||||
// whole scan threw on every boot while the EF InMemory suite passed).
|
||||
var latestBlob = await db.Deployments.AsNoTracking()
|
||||
.Where(d => d.ArtifactBlob.Length > 0)
|
||||
.Where(d => d.Status == DeploymentStatus.Sealed)
|
||||
.OrderByDescending(d => d.CreatedAtUtc)
|
||||
.Select(d => d.ArtifactBlob)
|
||||
.FirstOrDefaultAsync(ct).ConfigureAwait(false);
|
||||
var deployed = latestBlob is null ? [] : DeploymentArtifact.ParseDriverInstances(latestBlob);
|
||||
var deployed = latestBlob is not { Length: > 0 } ? [] : DeploymentArtifact.ParseDriverInstances(latestBlob);
|
||||
return Scan(ruled, devices, deployed);
|
||||
}
|
||||
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
using System.Text;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Deployments;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Deployments;
|
||||
|
||||
/// <summary>
|
||||
/// The SQL-Server-backed half of <see cref="CredentialReferencePreflightTests"/> (lmxopcua#815). EF InMemory runs
|
||||
/// <c>ScanAsync</c>'s deployment query as LINQ-to-Objects, so an untranslatable predicate passes there and throws on
|
||||
/// every real boot — exactly what the #815 live gate found (<c>ArtifactBlob.Length > 0</c>). Here it must translate.
|
||||
/// </summary>
|
||||
[Collection(nameof(AdminUiSqlServerCollection))]
|
||||
public sealed class CredentialReferencePreflightSqlServerTests(AdminUiSqlServerFixture sql)
|
||||
{
|
||||
[SqlServerFact]
|
||||
public async Task ScanAsync_translates_and_reads_the_latest_sealed_artifact()
|
||||
{
|
||||
// Far-future timestamps so this test's rows are the latest whatever else the shared database holds.
|
||||
var anchor = new DateTime(2999, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var sealedId = Guid.NewGuid();
|
||||
var failedId = Guid.NewGuid();
|
||||
const string artifact = """
|
||||
{"DriverInstances":[{"DriverInstanceId":"ouc-sql","Name":"UpstreamSql","DriverType":"OpcUaClient",
|
||||
"DriverConfig":"{\"Password\":\"secret:plant/opcua-pw\"}"}]}
|
||||
""";
|
||||
await using (var db = await sql.Factory.CreateDbContextAsync(TestContext.Current.CancellationToken))
|
||||
{
|
||||
db.Deployments.Add(new Deployment
|
||||
{
|
||||
DeploymentId = sealedId, RevisionHash = "preflight-sealed", CreatedBy = "t", Status = DeploymentStatus.Sealed,
|
||||
CreatedAtUtc = anchor, ArtifactBlob = Encoding.UTF8.GetBytes(artifact),
|
||||
});
|
||||
db.Deployments.Add(new Deployment
|
||||
{
|
||||
DeploymentId = failedId, RevisionHash = "preflight-failed", CreatedBy = "t", Status = DeploymentStatus.PartiallyFailed,
|
||||
CreatedAtUtc = anchor.AddMinutes(1), ArtifactBlob = Encoding.UTF8.GetBytes("""{"DriverInstances":[]}"""),
|
||||
});
|
||||
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var findings = await new CredentialReferencePreflight(sql.Factory).ScanAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
findings.ShouldContain(f => f.Source == CredentialFindingSource.Deployed && f.DriverName == "UpstreamSql");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await using var db = await sql.Factory.CreateDbContextAsync(CancellationToken.None);
|
||||
await db.Deployments.Where(d => d.DeploymentId == sealedId || d.DeploymentId == failedId).ExecuteDeleteAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -9,6 +9,7 @@ using ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Deployments;
|
||||
@@ -48,7 +49,16 @@ public sealed class CredentialReferencePreflightTests
|
||||
{"DriverInstances":[{"DriverInstanceId":"ouc","Name":"Upstream","DriverType":"OpcUaClient",
|
||||
"DriverConfig":"{\"Password\":\"secret:plant/opcua-pw\"}"}]}
|
||||
""";
|
||||
db.Deployments.Add(new Deployment { RevisionHash = "abc", CreatedBy = "t", ArtifactBlob = Encoding.UTF8.GetBytes(artifact) });
|
||||
db.Deployments.Add(new Deployment
|
||||
{
|
||||
RevisionHash = "abc", CreatedBy = "t", Status = DeploymentStatus.Sealed, ArtifactBlob = Encoding.UTF8.GetBytes(artifact),
|
||||
});
|
||||
// A newer deployment that never sealed is not what the fleet runs — it must not mask the sealed one.
|
||||
db.Deployments.Add(new Deployment
|
||||
{
|
||||
RevisionHash = "failed", CreatedBy = "t", Status = DeploymentStatus.PartiallyFailed,
|
||||
CreatedAtUtc = DateTime.UtcNow.AddMinutes(5), ArtifactBlob = Encoding.UTF8.GetBytes("""{"DriverInstances":[]}"""),
|
||||
});
|
||||
db.SaveChanges();
|
||||
return name;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user