ac4c4d40dc
v2-ci / unit-tests (push) Has been cancelled
Adds scripts/validation/opcua-windows-service/: a phased PowerShell gate (Invoke-ServiceGate.ps1) that installs the known-good build as the real OtOpcUaHost service via Install-Services.ps1, seeds LocalDb through the production cache API (Seeder, built against the baseline tree), upgrades in place with config/PKI/LocalDb carried with their ACLs, starts/stops through the SCM, runs a strict SDK 2 probe matrix (Sign + SignAndEncrypt, valid/wrong/unknown/disabled/anonymous keys, untrusted client and server), exercises the ScriptWorker isolation, rolls back and cleans up. windev 2026-11-12 (efb9197a->438484fd->efb9197a): app cert CEF9D91F... reused byte-for-byte with identical PKI SDDL on every start, all gating checks pass, 0 secrets in logs. Findings filed: #879 (None endpoint cannot be dropped), #880 (SDK 2 client 65-char key vs SDK 1.5 server: rollback/QA risk), #883 (Install-ScriptWorkerIsolation.ps1 fails on pwsh 7 and 5.1; runtime isolation proven with the patch in the evidence directory). Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
55 lines
4.0 KiB
C#
55 lines
4.0 KiB
C#
// #800 Windows service gate: seed a deployment artifact into an existing (service-created) LocalDb through
|
|
// the production cache API. Usage: Seeder <localdb-path>. Credentials come from the environment only:
|
|
// GATE_API_KEY (enabled key, production 65-character form), GATE_API_KEY_SHORT (enabled key whose secret is
|
|
// short enough that the whole password stays <= 64 bytes, so an SDK 2 client sends it in the legacy
|
|
// UserName-token format instead of RsaEncryptedSecret), GATE_API_KEY_DISABLED (carried with Enabled=false) and
|
|
// Security__ApiKeys__Pepper. Nothing secret is printed.
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
|
|
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.DeploymentCache;
|
|
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(60));
|
|
var localDb = Path.GetFullPath(args[0]);
|
|
if (!File.Exists(localDb)) throw new InvalidOperationException("The LocalDb must already exist (created by the service).");
|
|
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?> { ["LocalDb:Path"] = localDb }).Build();
|
|
await using var provider = new ServiceCollection().AddLogging().AddOtOpcUaLocalDb(config).BuildServiceProvider();
|
|
var pepper = Environment.GetEnvironmentVariable("Security__ApiKeys__Pepper") ?? throw new InvalidOperationException("Missing pepper.");
|
|
|
|
object Key(string variable, string name, bool enabled)
|
|
{
|
|
var presented = Environment.GetEnvironmentVariable(variable) ?? throw new InvalidOperationException("Missing " + variable + ".");
|
|
if (!ApiKeySecret.TryParse(presented, out var id, out var secret)) throw new InvalidOperationException("Invalid key format in " + variable + ".");
|
|
return new { ApiKeyId = id, Name = name, Principal = name, Enabled = enabled,
|
|
SecretHash = ApiKeySecret.ComputeHash(secret, pepper), CreatedUtc = DateTime.UtcNow };
|
|
}
|
|
|
|
const string source = "return 800;";
|
|
var artifact = JsonSerializer.SerializeToUtf8Bytes(new
|
|
{
|
|
Clusters = new[] { new { ClusterId = "gate800", Name = "Gate800" } },
|
|
RawFolders = new[] { new { RawFolderId = "gate800", Name = "Gate800", ClusterId = "gate800" } },
|
|
UnsAreas = new[] { new { UnsAreaId = "gate800-area", Name = "GateArea", ClusterId = "gate800" } },
|
|
UnsLines = new[] { new { UnsLineId = "gate800-line", UnsAreaId = "gate800-area", Name = "GateLine" } },
|
|
Equipment = new[] { new { EquipmentId = "EQ-000000000800", UnsLineId = "gate800-line", Name = "GateEquipment", MachineCode = "GATE800" } },
|
|
Scripts = new[] { new { ScriptId = "gate800-script", Name = "Gate800Script", SourceCode = source,
|
|
SourceHash = Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(source))), Language = "CSharp" } },
|
|
VirtualTags = new[] { new { VirtualTagId = "gate800-vt", EquipmentId = "EQ-000000000800", Name = "GateValue",
|
|
DataType = "Int32", ScriptId = "gate800-script", ChangeTriggered = false, TimerIntervalMs = 1000, Historize = false, Enabled = true } },
|
|
ApiKeys = new[] { Key("GATE_API_KEY", "gate800-enabled", true), Key("GATE_API_KEY_SHORT", "gate800-short", true),
|
|
Key("GATE_API_KEY_DISABLED", "gate800-disabled", false) },
|
|
NodeAcls = new[]
|
|
{
|
|
new { NodeAclId = "gate800-read", ClusterId = "gate800", Principal = "gate800-enabled", ScopeKind = 0, ScopeId = (string?)null, PermissionFlags = 7 },
|
|
new { NodeAclId = "gate800-short-read", ClusterId = "gate800", Principal = "gate800-short", ScopeKind = 0, ScopeId = (string?)null, PermissionFlags = 7 },
|
|
},
|
|
});
|
|
var revision = Convert.ToHexStringLower(SHA256.HashData(artifact));
|
|
await provider.GetRequiredService<IDeploymentArtifactCache>().StoreAsync("gate800",
|
|
ZB.MOM.WW.OtOpcUa.Commons.Types.DeploymentId.NewId().ToString(), revision, artifact, deadline.Token);
|
|
Console.WriteLine(JsonSerializer.Serialize(new { result = "seeded-through-public-cache", localDb, revision, bytes = artifact.Length }));
|