using System.Diagnostics; using System.Text; namespace SQLInsight.Servers.Tests; /// /// The security descriptor logic of db/permissions/grant-windows-server.ps1, run in Windows PowerShell against real /// default SDDLs (Service Control Manager, a WMI namespace, WinRM's RootSDDL): exactly one ACE is added in canonical /// order, an account that already has the right or has a Deny ACE is left alone, and removing gives back the original /// SDDL character for character. Nothing on this computer is read or changed. /// public sealed class GrantScriptTests { private const string Harness = """ param([string] $Script) $ErrorActionPreference = 'Stop' $ast = [System.Management.Automation.Language.Parser]::ParseFile($Script, [ref] $null, [ref] $null) foreach ($f in $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true)) { if ('Get-Aces', 'Test-Denied', 'Test-Allowed', 'Remove-Ace', 'Add-Ace' -contains $f.Name) { . ([scriptblock]::Create($f.Extent.Text)) } } $accessAllowed = [Security.AccessControl.AceQualifier]::AccessAllowed $accessDenied = [Security.AccessControl.AceQualifier]::AccessDenied $allSections = [Security.AccessControl.AccessControlSections]::All $sidObj = New-Object Security.Principal.SecurityIdentifier('S-1-5-20') $scm = 'D:(A;;CC;;;AU)(A;;CCLCRPRC;;;IU)(A;;CCLCRPRC;;;SU)(A;;CCLCRPWPRC;;;SY)(A;;KA;;;BA)(A;;CC;;;AC)S:(AU;FA;KA;;;WD)(AU;OIIOFA;GA;;;WD)' $new = Add-Ace $scm 0x20015 "scm-new=$new" "scm-allowed=$(Test-Allowed $new 0x20015)" "scm-back=$((Remove-Ace $new 0x20015) -eq $scm)" $sidObj = New-Object Security.Principal.SecurityIdentifier('S-1-5-21-1-2-3-1105') $wmi = 'O:BAG:BAD:(A;CIID;CCDCLCSWRPWPRCWD;;;BA)(A;CIID;CCDCRP;;;NS)(A;CIID;CCDCRP;;;LS)(A;CIID;CCDCRP;;;AU)' $w = Add-Ace $wmi 0x21 "wmi-new=$w" "wmi-back=$((Remove-Ace $w 0x21) -eq $wmi)" $root = 'O:NSG:BAD:P(A;;GA;;;BA)(A;;GR;;;IU)S:P(AU;FA;GA;;;WD)(AU;SA;GXGW;;;WD)' $r = Add-Ace $root ([int] 0x80000000) "root-new=$r" "root-back=$((Remove-Ace $r ([int] 0x80000000)) -eq $root)" $pre = 'D:(A;;CCLCRPRC;;;S-1-5-21-1-2-3-1105)(A;;CC;;;AU)' "pre-allowed=$(Test-Allowed $pre 0x20015)" "pre-remove-other-mask=$((Remove-Ace $pre 0x21) -eq $pre)" "deny=$(Test-Denied 'D:(D;;CC;;;S-1-5-21-1-2-3-1105)(A;;CC;;;AU)')" "no-deny=$(Test-Denied $scm)" """; [Fact] public async Task Exactly_one_ace_is_added_and_removing_it_restores_the_original() { var output = await RunAsync(); Assert.Equal("D:(A;;CC;;;AU)(A;;CCLCRPRC;;;IU)(A;;CCLCRPRC;;;SU)(A;;CCLCRPWPRC;;;SY)(A;;KA;;;BA)(A;;CC;;;AC)(A;;CCLCRPRC;;;NS)S:(AU;FA;KA;;;WD)(AU;OIIOFA;GA;;;WD)", output["scm-new"]); Assert.Equal("True", output["scm-allowed"]); Assert.Equal("True", output["scm-back"]); // WMI: Enable Account (CC) + Remote Enable (WP), not inherited, before the inherited ACEs. Assert.Equal("O:BAG:BAD:(A;;CCWP;;;S-1-5-21-1-2-3-1105)(A;CIID;CCDCLCSWRPWPRCWD;;;BA)(A;CIID;CCDCRP;;;NS)(A;CIID;CCDCRP;;;LS)(A;CIID;CCDCRP;;;AU)", output["wmi-new"]); Assert.Equal("True", output["wmi-back"]); // WinRM: read (GR), never execute (GX). Assert.Equal("O:NSG:BAD:P(A;;GA;;;BA)(A;;GR;;;IU)(A;;GR;;;S-1-5-21-1-2-3-1105)S:P(AU;FA;GA;;;WD)(AU;SA;GXGW;;;WD)", output["root-new"]); Assert.Equal("True", output["root-back"]); Assert.Equal("True", output["pre-allowed"]); // already there: the script adds nothing (and -Remove takes nothing) Assert.Equal("True", output["pre-remove-other-mask"]); // only the script's own ACE is ever removed Assert.Equal("True", output["deny"]); // a Deny ACE stops the script Assert.Equal("False", output["no-deny"]); } private static async Task> RunAsync() { var script = Path.Combine(RepositoryRoot(), "db", "permissions", "grant-windows-server.ps1"); var harness = Path.Combine(Path.GetTempPath(), $"sqlinsight-grant-{Guid.NewGuid():N}.ps1"); await File.WriteAllTextAsync(harness, Harness, new UTF8Encoding(true)); try { var start = new ProcessStartInfo("powershell.exe") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, StandardOutputEncoding = Encoding.UTF8, }; foreach (var argument in new[] { "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", harness, "-Script", script }) { start.ArgumentList.Add(argument); } using var process = Process.Start(start)!; using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(2)); var stdout = process.StandardOutput.ReadToEndAsync(timeout.Token); var stderr = process.StandardError.ReadToEndAsync(timeout.Token); await process.WaitForExitAsync(timeout.Token); Assert.True(process.ExitCode == 0, await stderr); return (await stdout).Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Select(l => l.Split('=', 2)) .Where(p => p.Length == 2) .ToDictionary(p => p[0], p => p[1], StringComparer.Ordinal); } finally { File.Delete(harness); } } private static string RepositoryRoot() { for (var directory = new DirectoryInfo(AppContext.BaseDirectory); directory is not null; directory = directory.Parent) { if (File.Exists(Path.Combine(directory.FullName, "SQLInsight.slnx"))) { return directory.FullName; } } throw new DirectoryNotFoundException("SQLInsight.slnx blev ikke fundet over testens mappe."); } }