Files
lmxopcua/scripts/validation/opcua-lifecycle-memory/baseline-canonical/BaselineRig.cs
T

108 lines
5.7 KiB
C#

using System.Net;
using System.Net.Sockets;
using System.Reflection;
using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security;
internal sealed class BaselineRig : IAsyncDisposable
{
internal const string Cluster = "SITE-A", RawDeviceFolder = "Plant/Modbus/dev1", RawAlarmPath = "Plant/Modbus/dev1/temp_hi";
internal const string EqA = "EqA", EqB = "EqB", KeyPrincipal = "event-probe", KeyId = "k-server-events";
internal static string KeyPassword => Environment.GetEnvironmentVariable("OTOPCUA_BASELINE_CANONICAL_KEY")
?? throw new InvalidOperationException("An ephemeral fixture credential is required");
private readonly OpcUaApplicationHostOptions _options;
private OpcUaApplicationHost _host = null!;
private bool _disposed;
internal OtOpcUaSdkServer Server { get; private set; } = null!;
internal OtOpcUaNodeManager Nm => Server.NodeManager!;
internal SdkAddressSpaceSink Sink { get; private set; } = null!;
internal string EndpointUrl => $"opc.tcp://127.0.0.1:{_options.OpcUaPort}/OtOpcUa";
internal string PkiRoot { get; }
internal CertificateValidator Validator
{
get
{
var applicationField = typeof(OpcUaApplicationHost).GetField("_application", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("Original host application field is missing");
return applicationField.GetValue(_host) is Opc.Ua.Configuration.ApplicationInstance application
&& application.ApplicationConfiguration.CertificateValidator is CertificateValidator validator ? validator
: throw new InvalidOperationException("Original host application field type changed");
}
}
internal BaselineRig(string directory)
{
PkiRoot = Path.Combine(directory, "server-pki");
using var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
_options = new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SDK2LifecycleSoak", ApplicationUri = $"urn:OtOpcUa.SDK2LifecycleSoak.{Guid.NewGuid():N}",
OpcUaPort = ((IPEndPoint)listener.LocalEndpoint).Port, PublicHostname = "127.0.0.1", PkiStoreRoot = PkiRoot,
EnabledSecurityProfiles = [OpcUaSecurityProfile.None, OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt],
AutoAcceptUntrustedClientCertificates = true,
};
}
internal async Task BootAsync(CancellationToken ct)
{
Server = new OtOpcUaSdkServer();
_host = new OpcUaApplicationHost(_options, NullLogger<OpcUaApplicationHost>.Instance, new Auth());
await _host.StartAsync(Server, ct);
Canonical.Ledger.Record("host-start");
Sink = new SdkAddressSpaceSink(Nm);
}
internal async Task RestartAsync(CancellationToken ct)
{
await _host.DisposeAsync();
Canonical.Ledger.Record("host-stop");
await BootAsync(ct);
}
internal DataPlaneAuthorityState Enforce(bool enabled, ApiKey[] keys, params NodeAcl[] grants)
{
var state = new DataPlaneAuthorityState();
state.Apply(Cluster, "rev-1", keys, grants, new Dictionary<AclScopeRef, IReadOnlyList<NodeAclPath>>
{
[new AclScopeRef(NodeAclScopeKind.Equipment, "eq-" + EqA)] = [new NodeAclPath([NodeAclRealms.Uns, EqA])],
[new AclScopeRef(NodeAclScopeKind.Equipment, "eq-" + EqB)] = [new NodeAclPath([NodeAclRealms.Uns, EqB])]
});
Nm.DataPlaneAuthority = state;
Nm.Authorizer = new DataPlaneAuthorizer(state, new TriePermissionEvaluator(state.Tries), enabled, NullLogger.Instance, TimeProvider.System);
Canonical.Ledger.Record("authority-restore");
return state;
}
internal static ApiKey Key() => new() { ApiKeyId = KeyId, Name = KeyId, SecretHash = "hash", Principal = KeyPrincipal,
Enabled = true, CreatedUtc = DateTime.UtcNow.AddDays(-1) };
internal static NodeAcl ClusterGrant(string principal, string id, NodePermissions permissions) => new()
{ NodeAclId = id, ClusterId = Cluster, Principal = principal, ScopeKind = NodeAclScopeKind.Cluster, PermissionFlags = permissions };
internal void MaterialiseNative()
{
Sink.EnsureFolder(RawDeviceFolder, null, "dev1", AddressSpaceRealm.Raw);
Sink.MaterialiseAlarmCondition(RawAlarmPath, RawDeviceFolder, "temp_hi", "OffNormalAlarm", 700, AddressSpaceRealm.Raw, isNative: true);
Sink.EnsureFolder(EqA, null, "A", AddressSpaceRealm.Uns);
Sink.EnsureFolder(EqB, null, "B", AddressSpaceRealm.Uns);
Sink.WireAlarmNotifiers(RawAlarmPath, AddressSpaceRealm.Raw, [EqA, EqB], AddressSpaceRealm.Uns);
}
public async ValueTask DisposeAsync()
{
if (_disposed) return;
_disposed = true;
try
{
if (_host is not null) { await _host.DisposeAsync(); Canonical.Ledger.Record("host-stop"); }
}
finally { if (Directory.Exists(PkiRoot)) Directory.Delete(PkiRoot, true); }
}
private sealed class Auth : IOpcUaUserAuthenticator
{
public Task<OpcUaUserAuthResult> AuthenticateUserNameAsync(string username, string password, CancellationToken ct) =>
Task.FromResult(username == "apikey" && password.StartsWith($"otua_{KeyId}_", StringComparison.Ordinal)
? OpcUaUserAuthResult.AllowKey(KeyPrincipal, [OpcUaDataPlaneRoles.AlarmAck, OpcUaDataPlaneRoles.WriteOperate], KeyId)
: OpcUaUserAuthResult.Deny("Invalid fixture key"));
}
}