Files
lmxopcua/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests/ScriptWorkerIdentityTests.cs
T
Joseph Doherty 2a8e2981fe
v2-ci / unit-tests (push) Has been cancelled
feat(scripting): ScriptWorker isolation on Windows: restricted worker identity, pipe ACL, firewall + PeCacheDir installer (lmxopcua#780)
Owner decisions §14 #5/#6 as built (design §12.5):

- Scripting:Worker:Identity (default unset, Windows only, fail-closed validation): each worker
  starts under a restricted token derived from the host's. Every privilege but SeChangeNotify is
  removed; the restricting SIDs are NT SERVICE\OtOpcUaScriptWorker's SID + Everyone + Users +
  RESTRICTED; the token and default DACLs are host + SYSTEM + identity + RESTRICTED. The worker
  is created suspended, put in its Job Object before it runs, inherits only its stdio pipes, gets
  an ACL'd work dir as TEMP, and runs with DOTNET_EnableDiagnostics=0. The identity is granted on
  the host's window station and desktop, because USER32 init made coreclr.dll fail with
  ERROR_DLL_INIT_FAILED on windev. The logon SID is not used because the host's process DACL
  gives it PROCESS_VM_READ.
- The worker pipe on Windows always has an explicit protected DACL: host account + identity
  only. There is no owner ACE, so an elevated host's Administrators gets no access.
- The worker treats access denied on its parent-pid poll as alive; the Job Object covers that case.
- scripts/install/Install-ScriptWorkerIsolation.ps1 + ScriptWorkerIsolation.psm1 are idempotent
  and support -WhatIf, -Uninstall and -PlanOnly. They create the disabled anchor service, the
  PeCacheDir ACL (protected, no Users, key and markers host-only, pe\ Modify for the identity),
  read+execute on script-worker\, a ReadData deny on the install root, the outbound firewall
  block on the worker exe, and optionally the host-service env.
- Tests: the dry-run harness scripts/install/tests/Test-ScriptWorkerIsolation.ps1 (46
  assertions, mutation-checked); ScriptWorkerIdentityTests; WorkerIsolationTests, which cover
  the pipe ACL, token access and a real restricted worker running a script, all green on windev.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 08:46:23 -05:00

77 lines
3.6 KiB
C#

using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests;
/// <summary>
/// <c>Scripting:Worker:Identity</c> (lmxopcua#780 design §6, §14 #5): the virtual account's SID is computed exactly as
/// Windows computes it, so the host's restricted token, the installer's ACLs (which compute the same SID in
/// PowerShell, <c>scripts/install/tests/Test-ScriptWorkerIsolation.ps1</c> pins the same value) and the pipe ACL agree.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ScriptWorkerIdentityTests
{
/// <summary>The installer's default identity; the PowerShell harness pins the same string.</summary>
private const string OtOpcUaScriptWorkerSid = "S-1-5-80-1846135827-3903057317-1998105452-3938448374-3023753135";
[Fact]
public void The_service_sid_algorithm_reproduces_a_well_known_virtual_account()
=> ScriptWorkerIdentity.ServiceSid("TrustedInstaller")
.ShouldBe("S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464");
[Fact]
public void The_default_identity_resolves_to_the_pinned_sid_case_insensitively()
{
ScriptWorkerIdentity.TryResolveSid(ScriptWorkerIdentity.DefaultIdentity, out var sid, out var error).ShouldBeTrue(error);
sid.ShouldBe(OtOpcUaScriptWorkerSid);
ScriptWorkerIdentity.TryResolveSid(@"nt service\otopcuascriptworker", out var lower, out _).ShouldBeTrue();
lower.ShouldBe(OtOpcUaScriptWorkerSid);
}
[Fact]
public void A_sid_string_passes_through()
{
ScriptWorkerIdentity.TryResolveSid("S-1-5-21-1-2-3-1001", out var sid, out _).ShouldBeTrue();
sid.ShouldBe("S-1-5-21-1-2-3-1001");
}
[Theory]
[InlineData("", "no identity")]
[InlineData(@"OTOPCUA\svc-worker", "expected 'NT SERVICE")]
[InlineData(@"NT SERVICE\bad name", "virtual account name")]
[InlineData("S-1-1-0", "well-known group")]
[InlineData("S-1-5-32-545", "well-known group")]
[InlineData("S-1-5-18", "well-known group")]
[InlineData("S-1-5-11", "well-known group")]
public void A_domain_account_a_well_known_group_or_garbage_is_refused(string identity, string why)
{
ScriptWorkerIdentity.TryResolveSid(identity, out _, out var error).ShouldBeFalse();
error!.ShouldContain(why);
}
[Fact]
public void The_restricting_set_is_the_identity_then_everyone_users_and_restricted_and_never_authenticated_users()
{
var set = ScriptWorkerIdentity.RestrictingSids(OtOpcUaScriptWorkerSid);
set.ShouldBe([OtOpcUaScriptWorkerSid, "S-1-1-0", "S-1-5-32-545", "S-1-5-12"]);
set.ShouldNotContain("S-1-5-11");
}
[Fact]
public void Validation_refuses_an_identity_off_windows_and_a_malformed_one_on_windows()
{
new ScriptWorkerOptions().ValidateIdentity(isWindows: false).ShouldBeNull("unset is always fine");
new ScriptWorkerOptions { Identity = ScriptWorkerIdentity.DefaultIdentity }.ValidateIdentity(isWindows: true)
.ShouldBeNull("positive control");
new ScriptWorkerOptions { Identity = ScriptWorkerIdentity.DefaultIdentity }.ValidateIdentity(isWindows: false)
.ShouldNotBeNull().ShouldContain("only supported on Windows");
new ScriptWorkerOptions { Identity = @"OTOPCUA\svc" }.ValidateIdentity(isWindows: true)
.ShouldNotBeNull().ShouldContain("Scripting:Worker:Identity");
}
[Fact]
public void Sanitized_keeps_the_identity()
=> new ScriptWorkerOptions { Identity = ScriptWorkerIdentity.DefaultIdentity }.Sanitized(out _).Identity
.ShouldBe(ScriptWorkerIdentity.DefaultIdentity);
}