108 lines
5.7 KiB
C#
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"));
|
|
}
|
|
}
|