fix(install): Install-ScriptWorkerIsolation.ps1 works on PowerShell 7 and 5.1, -Uninstall included (#883)
First real run of the #780 installer (windev, #800 gate) failed on both hosts and its rollback failed too: - PowerShell 7: DirectoryInfo.GetAccessControl/SetAccessControl do not exist on .NET. Use Get-Acl / Set-Acl. - Windows PowerShell 5.1: an if-expression unrolled the wanted ACE to a scalar ($null on -Remove), so .Count threw under StrictMode. Wrap it in @(...). - Found on the re-run: an apply from 5.1 was not idempotent. 5.1 passes the literal quotes in sc.exe binPath= with legacy quoting, so the anchor path was stored unquoted and AnchorService reported 'changed' on every run. sc create/config now runs through Invoke-OtOpcUaSc with an explicit command line, identical on both hosts. Adds scripts/install/tests/Test-ScriptWorkerIsolation.Windows.ps1: a Windows-executed dry run -> apply -> re-apply -> uninstall -> uninstall test (scratch names, stand-in host service) for either host. The plan-only harness cannot see these. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
@@ -188,16 +188,17 @@ function Set-OtOpcUaExplicitAce {
|
||||
if ($Remove) { return 'unchanged' }
|
||||
throw "$Path does not exist"
|
||||
}
|
||||
$item = Get-Item -LiteralPath $Path
|
||||
$acl = $item.GetAccessControl('Access')
|
||||
$acl = Get-Acl -LiteralPath $Path # #800: DirectoryInfo.GetAccessControl does not exist on PowerShell 7 (.NET)
|
||||
$identity = [System.Security.Principal.SecurityIdentifier]::new($Sid)
|
||||
$explicit = @($acl.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
|
||||
Where-Object { $_.IdentityReference -eq $identity })
|
||||
$inherit = [System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'
|
||||
$wanted = if ($Remove) { @() } else {
|
||||
, [System.Security.AccessControl.FileSystemAccessRule]::new($identity, [System.Security.AccessControl.FileSystemRights]$Mask,
|
||||
# #800: @() keeps $wanted an array; the if-expression unrolled it to a scalar ($null on -Remove), and .Count on that
|
||||
# throws under Set-StrictMode -Version Latest on Windows PowerShell 5.1.
|
||||
$wanted = @(if (-not $Remove) {
|
||||
[System.Security.AccessControl.FileSystemAccessRule]::new($identity, [System.Security.AccessControl.FileSystemRights]$Mask,
|
||||
$inherit, [System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.AccessControlType]$Type)
|
||||
}
|
||||
})
|
||||
$same = ($explicit.Count -eq $wanted.Count) -and (
|
||||
$wanted.Count -eq 0 -or (
|
||||
[int]$explicit[0].FileSystemRights -eq $Mask -and $explicit[0].AccessControlType -eq $wanted[0].AccessControlType -and
|
||||
@@ -205,10 +206,27 @@ function Set-OtOpcUaExplicitAce {
|
||||
if ($same) { return 'unchanged' }
|
||||
$acl.PurgeAccessRules($identity)
|
||||
foreach ($rule in $wanted) { $acl.AddAccessRule($rule) }
|
||||
$item.SetAccessControl($acl)
|
||||
Set-Acl -LiteralPath $Path -AclObject $acl
|
||||
'changed'
|
||||
}
|
||||
|
||||
function Invoke-OtOpcUaSc {
|
||||
# Runs sc.exe with an explicitly quoted command line (CommandLineToArgvW rules), identical on PowerShell 5.1 and 7.
|
||||
# Returns the exit code. Used where an argument contains literal double quotes.
|
||||
param([string[]]$Arguments)
|
||||
$quoted = foreach ($a in $Arguments) { '"' + ($a -replace '(\\*)"', '$1$1\"' -replace '(\\+)$', '$1$1') + '"' }
|
||||
$psi = [System.Diagnostics.ProcessStartInfo]::new((Join-Path ([Environment]::SystemDirectory) 'sc.exe'), ($quoted -join ' '))
|
||||
$psi.UseShellExecute = $false
|
||||
$psi.CreateNoWindow = $true
|
||||
$psi.RedirectStandardOutput = $true
|
||||
$psi.RedirectStandardError = $true
|
||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||
$null = $proc.StandardOutput.ReadToEnd()
|
||||
$null = $proc.StandardError.ReadToEnd()
|
||||
$proc.WaitForExit()
|
||||
$proc.ExitCode
|
||||
}
|
||||
|
||||
function Set-OtOpcUaStepAce {
|
||||
param([hashtable]$Data)
|
||||
if ($Data.ContainsKey('Remove')) { return (Set-OtOpcUaExplicitAce -Path $Data.Path -Sid $Data.Sid -Remove) }
|
||||
@@ -237,9 +255,12 @@ function Invoke-ScriptWorkerIsolationStep {
|
||||
if ($cfg -and $cfg.StartMode -eq 'Disabled' -and $cfg.StartName -ieq $d.Identity -and $cfg.PathName -eq "`"$($d.BinPath)`"") {
|
||||
return 'unchanged'
|
||||
}
|
||||
& sc.exe $verb $d.Name binPath= "`"$($d.BinPath)`"" start= disabled obj= $d.Identity `
|
||||
DisplayName= 'OtOpcUa ScriptWorker identity (anchor, never started)' | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "sc.exe $verb $($d.Name) failed ($LASTEXITCODE)" }
|
||||
# #883: the binPath value carries literal quotes. Windows PowerShell 5.1 passes native arguments with legacy quoting
|
||||
# (embedded quotes are dropped or mangled, so the stored path was unquoted and every re-run reported 'changed'),
|
||||
# PowerShell 7 escapes them. Build the command line explicitly so both hosts store the same value.
|
||||
$scExit = Invoke-OtOpcUaSc @($verb, $d.Name, 'binPath=', "`"$($d.BinPath)`"", 'start=', 'disabled', 'obj=', $d.Identity,
|
||||
'DisplayName=', 'OtOpcUa ScriptWorker identity (anchor, never started)')
|
||||
if ($scExit -ne 0) { throw "sc.exe $verb $($d.Name) failed ($scExit)" }
|
||||
& sc.exe sidtype $d.Name unrestricted | Out-Null
|
||||
& sc.exe description $d.Name 'Anchors the NT SERVICE virtual account OtOpcUa ScriptWorker processes are restricted to (lmxopcua#780). Never started: the OtOpcUaHost launches workers itself under a restricted token.' | Out-Null
|
||||
return 'changed'
|
||||
@@ -252,12 +273,11 @@ function Invoke-ScriptWorkerIsolationStep {
|
||||
}
|
||||
$changed = $false
|
||||
if (-not (Test-Path -LiteralPath $d.Path)) { New-Item -ItemType Directory -Path $d.Path -Force | Out-Null; $changed = $true }
|
||||
$item = Get-Item -LiteralPath $d.Path
|
||||
$current = $item.GetAccessControl('Access, Owner').GetSecurityDescriptorSddlForm('Access, Owner')
|
||||
$current = (Get-Acl -LiteralPath $d.Path).GetSecurityDescriptorSddlForm('Access, Owner')
|
||||
$desired = [System.Security.AccessControl.DirectorySecurity]::new()
|
||||
$desired.SetSecurityDescriptorSddlForm($d.Sddl, 'Access, Owner')
|
||||
if ($current -ne $desired.GetSecurityDescriptorSddlForm('Access, Owner')) {
|
||||
$item.SetAccessControl($desired) # propagates to the key file, pe\ and workers\ (inherited ACEs)
|
||||
Set-Acl -LiteralPath $d.Path -AclObject $desired # propagates to the key file, pe\ and workers\ (inherited ACEs)
|
||||
$changed = $true
|
||||
}
|
||||
return $(if ($changed) { 'changed' } else { 'unchanged' })
|
||||
|
||||
Reference in New Issue
Block a user