sqlinsight-pro/tests/SQLInsight.Servers.Tests/GrantScriptTests.cs
Søren Eggert Lundsteen Olsen b878fd3ab6
Some checks failed
deploy community / deploy (push) Successful in 16s
deploy hub / deploy (push) Successful in 18s
CI / Build og unit tests (push) Successful in 9s
CI / MSI-installer (push) Successful in 15s
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Has been cancelled
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Has been cancelled
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Has been cancelled
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Has been cancelled
Version 1.5.1: Windows-only tests pass in the Linux CI build
- AxLogonClient.Command always builds a Windows path to powershell.exe
- The grant-script test is skipped outside Windows (needs Windows PowerShell)
- The DNS test looks up an absolute name, so a search domain can't resolve it

v1.5.0 was tagged but never released because these failed in CI.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 16:49:05 +02:00

127 lines
6.1 KiB
C#

using System.Diagnostics;
using System.Text;
namespace SQLInsight.Servers.Tests;
/// <summary>
/// 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.
/// </summary>
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()
{
if (!OperatingSystem.IsWindows())
{
// The script runs in Windows PowerShell; the CI build runs on Linux.
return;
}
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<Dictionary<string, string>> 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.");
}
}