// #800 Windows service gate: seed a deployment artifact into an existing (service-created) LocalDb through // the production cache API. Usage: Seeder . 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 { ["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().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 }));