From 8f5b619bd9322c7fcb3a0850ce1f4a373a25bccd Mon Sep 17 00:00:00 2001 From: "Joseph L. Casale" <9957114+jcasale@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:07:41 -0600 Subject: [PATCH 01/15] Converts solution to use central package management. --- src/Directory.Build.props | 4 +-- src/Directory.Packages.props | 31 +++++++++++++++++++ .../Tests.Application.csproj | 26 ++++++++-------- src/Tests.Cli/Tests.Cli.csproj | 24 +++++++------- src/Tests/Tests.csproj | 2 +- .../UserRights.Application.csproj | 4 +-- src/UserRights.Cli/UserRights.Cli.csproj | 4 +-- .../UserRights.Extensions.csproj | 2 +- src/UserRights/UserRights.csproj | 20 ++++++------ 9 files changed, 74 insertions(+), 43 deletions(-) create mode 100644 src/Directory.Packages.props diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 2b9f876..2ce7e9c 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -8,11 +8,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props new file mode 100644 index 0000000..a1725a1 --- /dev/null +++ b/src/Directory.Packages.props @@ -0,0 +1,31 @@ + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Tests.Application/Tests.Application.csproj b/src/Tests.Application/Tests.Application.csproj index e96f27f..fab47e4 100644 --- a/src/Tests.Application/Tests.Application.csproj +++ b/src/Tests.Application/Tests.Application.csproj @@ -10,22 +10,22 @@ - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive + all - - runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Tests.Cli/Tests.Cli.csproj b/src/Tests.Cli/Tests.Cli.csproj index a2858a5..dcf8996 100644 --- a/src/Tests.Cli/Tests.Cli.csproj +++ b/src/Tests.Cli/Tests.Cli.csproj @@ -10,21 +10,21 @@ - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive + all - - runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj index e16c39f..548839b 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/UserRights.Application/UserRights.Application.csproj b/src/UserRights.Application/UserRights.Application.csproj index d6fb065..62a3825 100644 --- a/src/UserRights.Application/UserRights.Application.csproj +++ b/src/UserRights.Application/UserRights.Application.csproj @@ -8,8 +8,8 @@ - - + + all diff --git a/src/UserRights.Cli/UserRights.Cli.csproj b/src/UserRights.Cli/UserRights.Cli.csproj index f092145..40a1f4f 100644 --- a/src/UserRights.Cli/UserRights.Cli.csproj +++ b/src/UserRights.Cli/UserRights.Cli.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/src/UserRights.Extensions/UserRights.Extensions.csproj b/src/UserRights.Extensions/UserRights.Extensions.csproj index c13be21..b1bd7d9 100644 --- a/src/UserRights.Extensions/UserRights.Extensions.csproj +++ b/src/UserRights.Extensions/UserRights.Extensions.csproj @@ -8,7 +8,7 @@ - + \ No newline at end of file diff --git a/src/UserRights/UserRights.csproj b/src/UserRights/UserRights.csproj index 60f8e7c..457d03d 100644 --- a/src/UserRights/UserRights.csproj +++ b/src/UserRights/UserRights.csproj @@ -9,16 +9,16 @@ - - - - - - - - - - + + + + + + + + + + From b0d5fcedc078005fda74e3a833199ccce3f87407 Mon Sep 17 00:00:00 2001 From: "Joseph L. Casale" <9957114+jcasale@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:15:20 -0600 Subject: [PATCH 02/15] Migrates solution file. --- .github/workflows/main.yml | 6 ++-- src/UserRights.sln | 67 -------------------------------------- src/UserRights.slnx | 15 +++++++++ 3 files changed, 18 insertions(+), 70 deletions(-) delete mode 100644 src/UserRights.sln create mode 100644 src/UserRights.slnx diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6a06894..cafedee 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,10 +37,10 @@ jobs: "label=$label" >> $env:GITHUB_OUTPUT - name: Run tests - run: dotnet.exe test .\src\UserRights.sln --configuration Release --runtime win-x64 + run: dotnet.exe test .\src\UserRights.slnx --configuration Release --runtime win-x64 - name: Clean solution - run: dotnet.exe clean .\src\UserRights.sln --configuration Release + run: dotnet.exe clean .\src\UserRights.slnx --configuration Release - name: Publish runtime-dependent release run: dotnet.exe publish .\src\UserRights\UserRights.csproj --configuration Release --runtime win-x64 --no-self-contained --output .\publish @@ -58,7 +58,7 @@ jobs: if-no-files-found: error - name: Clean solution - run: dotnet.exe clean .\src\UserRights.sln --configuration Release + run: dotnet.exe clean .\src\UserRights.slnx --configuration Release - name: Publish self-contained release run: dotnet.exe publish .\src\UserRights\UserRights.csproj --configuration Release --runtime win-x64 --self-contained --output .\publish-packed -p:PublishSingleFile=true -p:PublishReadyToRun=true diff --git a/src/UserRights.sln b/src/UserRights.sln deleted file mode 100644 index e88fee3..0000000 --- a/src/UserRights.sln +++ /dev/null @@ -1,67 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.3.32819.101 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{0D6C4680-42C1-4D26-AF5C-23E4DFCFB706}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.Application", "Tests.Application\Tests.Application.csproj", "{47F5A9D7-27F8-4C23-B9F3-EACB1F42FED1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.Cli", "Tests.Cli\Tests.Cli.csproj", "{9CE42DB9-F4F1-4696-B4DF-341F94BA63E7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserRights", "UserRights\UserRights.csproj", "{6429FDB1-01EF-41E4-AF0D-AD5A1A017A05}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserRights.Application", "UserRights.Application\UserRights.Application.csproj", "{7B1432F5-892C-4551-B009-DE599AAF6C2E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserRights.Cli", "UserRights.Cli\UserRights.Cli.csproj", "{E4403039-A7DD-48CA-8C9D-2D553D759944}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserRights.Extensions", "UserRights.Extensions\UserRights.Extensions.csproj", "{ED142222-6CB8-4128-8758-1EDA01CEC44D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0BC816A9-57A2-4AAC-9FB2-090CEE526DC9}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - global.json = global.json - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0D6C4680-42C1-4D26-AF5C-23E4DFCFB706}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0D6C4680-42C1-4D26-AF5C-23E4DFCFB706}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0D6C4680-42C1-4D26-AF5C-23E4DFCFB706}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0D6C4680-42C1-4D26-AF5C-23E4DFCFB706}.Release|Any CPU.Build.0 = Release|Any CPU - {47F5A9D7-27F8-4C23-B9F3-EACB1F42FED1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {47F5A9D7-27F8-4C23-B9F3-EACB1F42FED1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {47F5A9D7-27F8-4C23-B9F3-EACB1F42FED1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {47F5A9D7-27F8-4C23-B9F3-EACB1F42FED1}.Release|Any CPU.Build.0 = Release|Any CPU - {9CE42DB9-F4F1-4696-B4DF-341F94BA63E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9CE42DB9-F4F1-4696-B4DF-341F94BA63E7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9CE42DB9-F4F1-4696-B4DF-341F94BA63E7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9CE42DB9-F4F1-4696-B4DF-341F94BA63E7}.Release|Any CPU.Build.0 = Release|Any CPU - {6429FDB1-01EF-41E4-AF0D-AD5A1A017A05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6429FDB1-01EF-41E4-AF0D-AD5A1A017A05}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6429FDB1-01EF-41E4-AF0D-AD5A1A017A05}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6429FDB1-01EF-41E4-AF0D-AD5A1A017A05}.Release|Any CPU.Build.0 = Release|Any CPU - {7B1432F5-892C-4551-B009-DE599AAF6C2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7B1432F5-892C-4551-B009-DE599AAF6C2E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7B1432F5-892C-4551-B009-DE599AAF6C2E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7B1432F5-892C-4551-B009-DE599AAF6C2E}.Release|Any CPU.Build.0 = Release|Any CPU - {E4403039-A7DD-48CA-8C9D-2D553D759944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E4403039-A7DD-48CA-8C9D-2D553D759944}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E4403039-A7DD-48CA-8C9D-2D553D759944}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E4403039-A7DD-48CA-8C9D-2D553D759944}.Release|Any CPU.Build.0 = Release|Any CPU - {ED142222-6CB8-4128-8758-1EDA01CEC44D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ED142222-6CB8-4128-8758-1EDA01CEC44D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ED142222-6CB8-4128-8758-1EDA01CEC44D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ED142222-6CB8-4128-8758-1EDA01CEC44D}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {06F68D03-8D08-4AB8-902E-3D62CB5AA683} - EndGlobalSection -EndGlobal diff --git a/src/UserRights.slnx b/src/UserRights.slnx new file mode 100644 index 0000000..898aaae --- /dev/null +++ b/src/UserRights.slnx @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + From e0db26de6388b051e8d4e5ab6e94a3adc1393167 Mon Sep 17 00:00:00 2001 From: "Joseph L. Casale" <9957114+jcasale@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:15:37 -0600 Subject: [PATCH 03/15] Updates analyzer configuration. --- src/.editorconfig | 46 ++++++++++++------------ src/Directory.Packages.props | 2 +- src/Tests.Application/.editorconfig | 2 ++ src/UserRights.Application/.editorconfig | 2 ++ src/UserRights/.editorconfig | 2 ++ 5 files changed, 29 insertions(+), 25 deletions(-) create mode 100644 src/Tests.Application/.editorconfig create mode 100644 src/UserRights.Application/.editorconfig create mode 100644 src/UserRights/.editorconfig diff --git a/src/.editorconfig b/src/.editorconfig index e8a9830..b009955 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -92,30 +92,28 @@ resharper_object_creation_when_type_not_evident = target_typed resharper_redundant_name_qualifier_highlighting = none # Analyzer inspection severities -dotnet_diagnostic.IDE0001.severity = none -dotnet_diagnostic.IDE0003.severity = none # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0003-ide0009 -dotnet_diagnostic.IDE0039.severity = none -dotnet_diagnostic.IDE0046.severity = none -dotnet_diagnostic.IDE0058.severity = none - -dotnet_diagnostic.CA1031.severity = none -dotnet_diagnostic.CA1515.severity = none -dotnet_diagnostic.CA1822.severity = none -dotnet_diagnostic.CA1848.severity = none - -dotnet_diagnostic.MA0003.severity = none -dotnet_diagnostic.MA0007.severity = none -dotnet_diagnostic.MA0015.severity = none -dotnet_diagnostic.MA0038.severity = none -dotnet_diagnostic.MA0041.severity = none -dotnet_diagnostic.MA0051.severity = none - -dotnet_diagnostic.SA1101.severity = none -dotnet_diagnostic.SA1118.severity = none -dotnet_diagnostic.SA1309.severity = none -dotnet_diagnostic.SA1312.severity = none -dotnet_diagnostic.SA1413.severity = none -dotnet_diagnostic.SA1633.severity = none +dotnet_diagnostic.IDE0001.severity = none # Simplify name. +dotnet_diagnostic.IDE0003.severity = none # Remove this or Me qualification. +dotnet_diagnostic.IDE0039.severity = none # Use local function instead of lambda. +dotnet_diagnostic.IDE0046.severity = none # Use conditional expression for return. +dotnet_diagnostic.IDE0058.severity = none # Remove unnecessary expression value. + +dotnet_diagnostic.CA1515.severity = none # Consider making public types internal. +dotnet_diagnostic.CA1822.severity = none # Mark members as static. +dotnet_diagnostic.CA1848.severity = none # Use the LoggerMessage delegates. +dotnet_diagnostic.CA1873.severity = none # Evaluation of this argument may be expensive and unnecessary if logging is disabled. + +dotnet_diagnostic.MA0003.severity = none # Add parameter name to improve readability. +dotnet_diagnostic.MA0007.severity = none # Add a comma after the last value. +dotnet_diagnostic.MA0051.severity = none # Method is too long. +dotnet_diagnostic.MA0177.severity = none # Use single-line XML comment syntax when possible. + +dotnet_diagnostic.SA1101.severity = none # Prefix local calls with this. +dotnet_diagnostic.SA1118.severity = none # Parameter must not span multiple lines. +dotnet_diagnostic.SA1309.severity = none # Field names must not begin with underscore. +dotnet_diagnostic.SA1312.severity = none # Variable names must begin with lower case letter. +dotnet_diagnostic.SA1413.severity = none # Use trailing commas in multi line initializers. +dotnet_diagnostic.SA1633.severity = none # File must have header. #### C# Coding Conventions #### [*.cs] diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index a1725a1..524c5b3 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -5,7 +5,7 @@ - + diff --git a/src/Tests.Application/.editorconfig b/src/Tests.Application/.editorconfig new file mode 100644 index 0000000..c36ad37 --- /dev/null +++ b/src/Tests.Application/.editorconfig @@ -0,0 +1,2 @@ +[UserRightsManagerPrivilegeTests.cs] +dotnet_diagnostic.MA0110.severity = none # Use the Regex source generator. \ No newline at end of file diff --git a/src/UserRights.Application/.editorconfig b/src/UserRights.Application/.editorconfig new file mode 100644 index 0000000..423178b --- /dev/null +++ b/src/UserRights.Application/.editorconfig @@ -0,0 +1,2 @@ +[UserRightsManager.cs] +dotnet_diagnostic.MA0015.severity = none # Use an overload of 'System.ArgumentException' with the parameter name. \ No newline at end of file diff --git a/src/UserRights/.editorconfig b/src/UserRights/.editorconfig new file mode 100644 index 0000000..57092af --- /dev/null +++ b/src/UserRights/.editorconfig @@ -0,0 +1,2 @@ +[Program.cs] +dotnet_diagnostic.CA1031.severity = none # Modify 'Main' to catch a more specific allowed exception type, or rethrow the exception. \ No newline at end of file From 0537c003d613cdc72241b001c307496c4603741e Mon Sep 17 00:00:00 2001 From: "Joseph L. Casale" <9957114+jcasale@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:17:59 -0600 Subject: [PATCH 04/15] Simplifies informational version. --- src/Directory.Build.props | 1 + src/UserRights.Cli/CliBuilder.cs | 15 ++++----------- src/UserRights.Cli/ProgramInfo.cs | 10 +--------- src/UserRights.Cli/VersionAction.cs | 22 ---------------------- 4 files changed, 6 insertions(+), 42 deletions(-) delete mode 100644 src/UserRights.Cli/VersionAction.cs diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 2ce7e9c..3cb3bd1 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,6 +4,7 @@ Windows User Rights Assignment Utility Utility for managing user right assignments. Copyright © Joseph L. Casale 2022 + false true diff --git a/src/UserRights.Cli/CliBuilder.cs b/src/UserRights.Cli/CliBuilder.cs index 2a5897f..a3dab98 100644 --- a/src/UserRights.Cli/CliBuilder.cs +++ b/src/UserRights.Cli/CliBuilder.cs @@ -53,19 +53,12 @@ public CommandLineConfiguration Build() foreach (var option in rootCommand.Options) { - switch (option) + // Replace the default help action with one that adds examples. + if (option is HelpOption helpOption) { - // Replace the default help action with one that adds examples. - case HelpOption helpOption: - helpOption.Action = new HelpExamplesAction((HelpAction)helpOption.Action!); + helpOption.Action = new HelpExamplesAction((HelpAction)helpOption.Action!); - break; - - // Replace the default version action with one that produces a shorter informational version. - case VersionOption versionOption: - versionOption.Action = new VersionAction(); - - break; + break; } } diff --git a/src/UserRights.Cli/ProgramInfo.cs b/src/UserRights.Cli/ProgramInfo.cs index dfc629a..7255e25 100644 --- a/src/UserRights.Cli/ProgramInfo.cs +++ b/src/UserRights.Cli/ProgramInfo.cs @@ -24,15 +24,7 @@ static ProgramInfo() Version = assembly.GetName().Version?.ToString() ?? string.Empty; var assemblyVersionAttribute = assembly.GetCustomAttribute(); - if (assemblyVersionAttribute is null) - { - InformationalVersion = Version; - } - else - { - var parts = assemblyVersionAttribute.InformationalVersion.Split('+'); - InformationalVersion = parts.Length > 1 ? parts[0] : Version; - } + InformationalVersion = assemblyVersionAttribute is null ? Version : assemblyVersionAttribute.InformationalVersion; } } diff --git a/src/UserRights.Cli/VersionAction.cs b/src/UserRights.Cli/VersionAction.cs deleted file mode 100644 index 4d55349..0000000 --- a/src/UserRights.Cli/VersionAction.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace UserRights.Cli; - -using System; -using System.CommandLine; -using System.CommandLine.Invocation; - -/// -/// Represents an action that outputs the version of the application with a shortened git hash. -/// -public class VersionAction : SynchronousCommandLineAction -{ - /// - public override int Invoke(ParseResult parseResult) - { - ArgumentNullException.ThrowIfNull(parseResult); - - // Write a more sane informational version using only part of the git hash. - parseResult.Configuration.Output.WriteLine(ProgramInfo.InformationalVersion); - - return 0; - } -} \ No newline at end of file From c7f1e6326dee54de33396089a10de0718e7279df Mon Sep 17 00:00:00 2001 From: "Joseph L. Casale" <9957114+jcasale@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:19:33 -0600 Subject: [PATCH 05/15] Updates mode logging message to use informational version. --- src/UserRights.Cli/CliBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UserRights.Cli/CliBuilder.cs b/src/UserRights.Cli/CliBuilder.cs index a3dab98..eeb1ae9 100644 --- a/src/UserRights.Cli/CliBuilder.cs +++ b/src/UserRights.Cli/CliBuilder.cs @@ -118,7 +118,7 @@ private Command BuildListCommand() command.SetAction(async (parseResult, cancellationToken) => { - _logger.LogInformation(OperationId.ListMode, "{Program:l} v{Version} executing in {Mode:l} mode.", ProgramInfo.Program, ProgramInfo.Version, command.Name); + _logger.LogInformation(OperationId.ListMode, "{Program:l} v{Version} executing in {Mode:l} mode.", ProgramInfo.Program, ProgramInfo.InformationalVersion, command.Name); var json = parseResult.GetValue(jsonOption); var path = parseResult.GetValue(pathOption); @@ -343,7 +343,7 @@ private Command BuildPrincipalCommand() _logger.BeginScope(new Dictionary(StringComparer.Ordinal) { { "DryRun", dryRun } }); - _logger.LogInformation(OperationId.PrincipalMode, "{Program:l} v{Version} executing in {Mode:l} mode.", ProgramInfo.Program, ProgramInfo.Version, command.Name); + _logger.LogInformation(OperationId.PrincipalMode, "{Program:l} v{Version} executing in {Mode:l} mode.", ProgramInfo.Program, ProgramInfo.InformationalVersion, command.Name); _policy.Connect(systemName); @@ -572,7 +572,7 @@ private Command BuildPrivilegeCommand() _logger.BeginScope(new Dictionary(StringComparer.Ordinal) { { "DryRun", dryRun } }); - _logger.LogInformation(OperationId.PrivilegeMode, "{Program:l} v{Version} executing in {Mode:l} mode.", ProgramInfo.Program, ProgramInfo.Version, command.Name); + _logger.LogInformation(OperationId.PrivilegeMode, "{Program:l} v{Version} executing in {Mode:l} mode.", ProgramInfo.Program, ProgramInfo.InformationalVersion, command.Name); var revokeRegex = string.IsNullOrWhiteSpace(revokePattern) ? null From cc8009fb62ac43bafb9a47d7dacb19f59ee01093 Mon Sep 17 00:00:00 2001 From: "Joseph L. Casale" <9957114+jcasale@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:22:28 -0600 Subject: [PATCH 06/15] Updates System.CommandLine. --- src/Directory.Packages.props | 2 +- src/Tests.Cli/ListCommandTests.cs | 8 +- src/Tests.Cli/ListSyntaxTests.cs | 20 ++-- src/Tests.Cli/PrincipalCommandTests.cs | 20 ++-- src/Tests.Cli/PrincipalSyntaxTests.cs | 60 +++++----- src/Tests.Cli/PrivilegeCommandTests.cs | 28 ++--- src/Tests.Cli/PrivilegeSyntaxTests.cs | 90 +++++++------- src/Tests.Cli/Tests.Cli.csproj | 1 - src/UserRights.Cli/CliBuilder.cs | 146 +++++++++++++---------- src/UserRights.Cli/CliExtensions.cs | 45 ++++++- src/UserRights.Cli/HelpExamplesAction.cs | 5 +- src/UserRights/Program.cs | 9 +- src/UserRights/UserRights.csproj | 1 - 13 files changed, 250 insertions(+), 185 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 524c5b3..e44f49a 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -22,7 +22,7 @@ - + diff --git a/src/Tests.Cli/ListCommandTests.cs b/src/Tests.Cli/ListCommandTests.cs index d909e6d..77e44ea 100644 --- a/src/Tests.Cli/ListCommandTests.cs +++ b/src/Tests.Cli/ListCommandTests.cs @@ -63,7 +63,7 @@ public async Task PathAndJsonShouldWork() var builder = ServiceProvider.GetRequiredService(); - var configuration = builder.Build(); + var rootCommand = builder.Build(); var file = Path.GetTempFileName(); var args = new[] @@ -78,7 +78,7 @@ public async Task PathAndJsonShouldWork() UserRightEntry[] actual; try { - rc = await configuration.Parse(args).Validate().InvokeAsync(); + rc = await rootCommand.Parse(args).ThrowIfInvalid().RunAsync(); await using var stream = File.OpenRead(file); @@ -135,7 +135,7 @@ public async Task PathShouldWork() var builder = ServiceProvider.GetRequiredService(); - var configuration = builder.Build(); + var rootCommand = builder.Build(); var file = Path.GetTempFileName(); var args = new[] @@ -149,7 +149,7 @@ public async Task PathShouldWork() UserRightEntry[] actual; try { - rc = await configuration.Parse(args).Validate().InvokeAsync(); + rc = await rootCommand.Parse(args).ThrowIfInvalid().RunAsync(); var csvConfiguration = new CsvConfiguration(CultureInfo.InvariantCulture) { diff --git a/src/Tests.Cli/ListSyntaxTests.cs b/src/Tests.Cli/ListSyntaxTests.cs index dc9a523..9853605 100644 --- a/src/Tests.Cli/ListSyntaxTests.cs +++ b/src/Tests.Cli/ListSyntaxTests.cs @@ -31,9 +31,9 @@ public ListSyntaxTests() public void CsvToStdoutShouldWork() { var args = new[] { "list" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -45,9 +45,9 @@ public void CsvToStdoutShouldWork() public void CsvToPathShouldWork() { var args = new[] { "list", "--path", "file.csv" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -60,7 +60,7 @@ public void CsvToPathShouldWork() [InlineData("list", "--path", "")] [InlineData("list", "--path", " ")] public void PathWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); + => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures an empty or whitespace system name is rejected. @@ -70,7 +70,7 @@ public void PathWithInvalidStringThrowsException(params string[] args) [InlineData("list", "--system-name", "")] [InlineData("list", "--system-name", " ")] public void SystemNameWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); + => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Verifies list mode with JSON formatted output sent to STDOUT is parsed successfully. @@ -79,9 +79,9 @@ public void SystemNameWithInvalidStringThrowsException(params string[] args) public void JsonToStdoutShouldWork() { var args = new[] { "list", "--json" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -93,9 +93,9 @@ public void JsonToStdoutShouldWork() public void JsonToPathShouldWork() { var args = new[] { "list", "--json", "--path", "file.csv" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } diff --git a/src/Tests.Cli/PrincipalCommandTests.cs b/src/Tests.Cli/PrincipalCommandTests.cs index e97e376..31f2bf1 100644 --- a/src/Tests.Cli/PrincipalCommandTests.cs +++ b/src/Tests.Cli/PrincipalCommandTests.cs @@ -54,7 +54,7 @@ public void GrantAndRevokeOthersShouldWork() var builder = ServiceProvider.GetRequiredService(); - var configuration = builder.Build(); + var rootCommand = builder.Build(); var args = new[] { @@ -65,7 +65,7 @@ public void GrantAndRevokeOthersShouldWork() "--revoke-others" }; - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); Assert.Equal([PrincipalSid1, PrincipalSid2], policy.LsaEnumerateAccountsWithUserRight().Order()); @@ -111,7 +111,7 @@ public void GrantAndRevokeShouldWork() var builder = ServiceProvider.GetRequiredService(); - var configuration = builder.Build(); + var rootCommand = builder.Build(); var args = new[] { @@ -123,7 +123,7 @@ public void GrantAndRevokeShouldWork() Privilege1 }; - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); @@ -169,7 +169,7 @@ public void GrantShouldWork() var builder = ServiceProvider.GetRequiredService(); - var configuration = builder.Build(); + var rootCommand = builder.Build(); var args = new[] { @@ -179,7 +179,7 @@ public void GrantShouldWork() Privilege2 }; - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); @@ -226,7 +226,7 @@ public void RevokeAllShouldWork() var builder = ServiceProvider.GetRequiredService(); - var configuration = builder.Build(); + var rootCommand = builder.Build(); var args = new[] { @@ -235,7 +235,7 @@ public void RevokeAllShouldWork() "--revoke-all" }; - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); Assert.Empty(policy.LsaEnumerateAccountRights(PrincipalSid1)); @@ -281,7 +281,7 @@ public void RevokeShouldWork() var builder = ServiceProvider.GetRequiredService(); - var configuration = builder.Build(); + var rootCommand = builder.Build(); var args = new[] { @@ -291,7 +291,7 @@ public void RevokeShouldWork() Privilege2 }; - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); diff --git a/src/Tests.Cli/PrincipalSyntaxTests.cs b/src/Tests.Cli/PrincipalSyntaxTests.cs index b8223f0..926b3b8 100644 --- a/src/Tests.Cli/PrincipalSyntaxTests.cs +++ b/src/Tests.Cli/PrincipalSyntaxTests.cs @@ -31,9 +31,9 @@ public PrincipalSyntaxTests() public void GrantAndRevokeShouldWork() { var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--revoke", "SeBatchLogonRight" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -45,9 +45,9 @@ public void GrantAndRevokeShouldWork() public void GrantMultipleShouldWork() { var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--grant", "SeBatchLogonRight" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -59,9 +59,9 @@ public void GrantMultipleShouldWork() public void GrantShouldWork() { var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -74,7 +74,7 @@ public void GrantShouldWork() [InlineData("principal", "DOMAIN\\UserOrGroup", "--grant", "")] [InlineData("principal", "DOMAIN\\UserOrGroup", "--grant", " ")] public void GrantWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); + => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures granting a privilege and revoking all other privileges is rejected. @@ -83,9 +83,9 @@ public void GrantWithInvalidStringThrowsException(params string[] args) public void GrantWithRevokeAllThrowsException() { var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--revoke-all" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); + Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// @@ -95,9 +95,9 @@ public void GrantWithRevokeAllThrowsException() public void NoOptionsThrowsException() { var args = new[] { "principal" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); + Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// @@ -109,7 +109,7 @@ public void NoOptionsThrowsException() [InlineData("principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--grant", "SeServiceLogonRight")] [InlineData("principal", "DOMAIN\\UserOrGroup", "--revoke", "SeServiceLogonRight", "--revoke", "SeServiceLogonRight")] public void OverlappingGrantsAndRevokesThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); + => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures an empty or whitespace principal is rejected. @@ -119,7 +119,7 @@ public void OverlappingGrantsAndRevokesThrowsException(params string[] args) [InlineData("principal", "", "--grant", "SeServiceLogonRight")] [InlineData("principal", " ", "--grant", "SeServiceLogonRight")] public void PrincipalWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); + => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures revoking all privileges is accepted. @@ -128,9 +128,9 @@ public void PrincipalWithInvalidStringThrowsException(params string[] args) public void RevokeAllShouldWork() { var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-all" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -142,9 +142,9 @@ public void RevokeAllShouldWork() public void RevokeAllWithGrantsThrowsException() { var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-all", "--grant", "SeServiceLogonRight" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); + Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// @@ -154,9 +154,9 @@ public void RevokeAllWithGrantsThrowsException() public void RevokeAllWithRevocationsThrowsException() { var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-all", "--revoke", "SeServiceLogonRight" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); + Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// @@ -166,9 +166,9 @@ public void RevokeAllWithRevocationsThrowsException() public void RevokeAllWithRevokeOthersThrowsException() { var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-all", "--revoke-others" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); + Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// @@ -178,9 +178,9 @@ public void RevokeAllWithRevokeOthersThrowsException() public void RevokeMultipleShouldWork() { var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke", "SeServiceLogonRight", "--revoke", "SeBatchLogonRight" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -193,7 +193,7 @@ public void RevokeOthersWithOutGrantsThrowsException() { var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-others" }; - Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); + Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); } /// @@ -203,9 +203,9 @@ public void RevokeOthersWithOutGrantsThrowsException() public void RevokeOthersWithRevocationsThrowsException() { var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-others", "--revoke", "SeServiceLogonRight" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); + Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// @@ -215,9 +215,9 @@ public void RevokeOthersWithRevocationsThrowsException() public void RevokeShouldWork() { var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke", "SeServiceLogonRight" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -230,7 +230,7 @@ public void RevokeShouldWork() [InlineData("principal", "DOMAIN\\UserOrGroup", "--revoke", "")] [InlineData("principal", "DOMAIN\\UserOrGroup", "--revoke", " ")] public void RevokeWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); + => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures an empty or whitespace system name is rejected. @@ -240,5 +240,5 @@ public void RevokeWithInvalidStringThrowsException(params string[] args) [InlineData("principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--system-name", "")] [InlineData("principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--system-name", " ")] public void SystemNameWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); + => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); } \ No newline at end of file diff --git a/src/Tests.Cli/PrivilegeCommandTests.cs b/src/Tests.Cli/PrivilegeCommandTests.cs index ae144f7..1628d69 100644 --- a/src/Tests.Cli/PrivilegeCommandTests.cs +++ b/src/Tests.Cli/PrivilegeCommandTests.cs @@ -57,7 +57,7 @@ public void GrantAndRevokeOthersShouldWork() var builder = ServiceProvider.GetRequiredService(); - var configuration = builder.Build(); + var rootCommand = builder.Build(); var args = new[] { @@ -68,7 +68,7 @@ public void GrantAndRevokeOthersShouldWork() "--revoke-others" }; - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); @@ -116,7 +116,7 @@ public void GrantAndRevokePasses() var builder = ServiceProvider.GetRequiredService(); - var configuration = builder.Build(); + var rootCommand = builder.Build(); var args = new[] { @@ -128,7 +128,7 @@ public void GrantAndRevokePasses() PrincipalName1 }; - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); @@ -179,7 +179,7 @@ public void GrantAndRevokePatternPasses() var builder = ServiceProvider.GetRequiredService(); - var configuration = builder.Build(); + var rootCommand = builder.Build(); var args = new[] { @@ -191,7 +191,7 @@ public void GrantAndRevokePatternPasses() "^S-1-5-21" }; - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); Assert.Equal(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); @@ -237,7 +237,7 @@ public void GrantPasses() var builder = ServiceProvider.GetRequiredService(); - var configuration = builder.Build(); + var rootCommand = builder.Build(); var args = new[] { @@ -247,7 +247,7 @@ public void GrantPasses() PrincipalName1 }; - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); @@ -296,7 +296,7 @@ public void RevokeAllPasses() var builder = ServiceProvider.GetRequiredService(); - var configuration = builder.Build(); + var rootCommand = builder.Build(); var args = new[] { @@ -305,7 +305,7 @@ public void RevokeAllPasses() "--revoke-all" }; - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); @@ -353,7 +353,7 @@ public void RevokePasses() var builder = ServiceProvider.GetRequiredService(); - var configuration = builder.Build(); + var rootCommand = builder.Build(); var args = new[] { @@ -363,7 +363,7 @@ public void RevokePasses() PrincipalName2 }; - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); @@ -414,7 +414,7 @@ public void RevokePatternForAllButBuiltinAndVirtualPasses() var builder = ServiceProvider.GetRequiredService(); - var configuration = builder.Build(); + var rootCommand = builder.Build(); var args = new[] { @@ -424,7 +424,7 @@ public void RevokePatternForAllButBuiltinAndVirtualPasses() "^S-1-5-21" }; - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); Assert.Equal(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); diff --git a/src/Tests.Cli/PrivilegeSyntaxTests.cs b/src/Tests.Cli/PrivilegeSyntaxTests.cs index 9c4b5db..960b4a2 100644 --- a/src/Tests.Cli/PrivilegeSyntaxTests.cs +++ b/src/Tests.Cli/PrivilegeSyntaxTests.cs @@ -31,9 +31,9 @@ public PrivilegeSyntaxTests() public void GrantAndRevokeShouldWork() { var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\User", "--revoke", "DOMAIN\\Group" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -45,9 +45,9 @@ public void GrantAndRevokeShouldWork() public void GrantMultipleShouldWork() { var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\User", "--grant", "DOMAIN\\Group" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -59,9 +59,9 @@ public void GrantMultipleShouldWork() public void GrantShouldWork() { var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\User" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -74,7 +74,7 @@ public void GrantShouldWork() [InlineData("privilege", "SeServiceLogonRight", "--grant", "")] [InlineData("privilege", "SeServiceLogonRight", "--grant", " ")] public void GrantWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); + => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures granting a context and revoking all contexts is rejected. @@ -83,9 +83,9 @@ public void GrantWithInvalidStringThrowsException(params string[] args) public void GrantWithRevokeAllThrowsException() { var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\User", "--revoke-all" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); + Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// @@ -95,9 +95,9 @@ public void GrantWithRevokeAllThrowsException() public void NoOptionsThrowsException() { var args = new[] { "privilege" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); + Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// @@ -109,7 +109,7 @@ public void NoOptionsThrowsException() [InlineData("privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--grant", "DOMAIN\\UserOrGroup")] [InlineData("privilege", "SeServiceLogonRight", "--revoke", "DOMAIN\\UserOrGroup", "--revoke", "DOMAIN\\UserOrGroup")] public void OverlappingGrantsAndRevokesThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); + => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures an empty or whitespace principal is rejected. @@ -119,7 +119,7 @@ public void OverlappingGrantsAndRevokesThrowsException(params string[] args) [InlineData("privilege", "", "--grant", "DOMAIN\\UserOrGroup")] [InlineData("privilege", " ", "--grant", "DOMAIN\\UserOrGroup")] public void PrivilegeWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); + => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures revoking all contexts is accepted. @@ -128,10 +128,10 @@ public void PrivilegeWithInvalidStringThrowsException(params string[] args) public void RevokeAllShouldWork() { var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-all" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); int? rc = null; - var exception = Record.Exception(() => rc = configuration.Parse(args).Validate().Invoke()); + var exception = Record.Exception(() => rc = rootCommand.Parse(args).ThrowIfInvalid().Run()); Assert.Null(exception); Assert.Equal(0, rc); @@ -144,9 +144,9 @@ public void RevokeAllShouldWork() public void RevokeAllWithGrantsThrowsException() { var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-all", "--grant", "DOMAIN\\UserOrGroup" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); + Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// @@ -156,9 +156,9 @@ public void RevokeAllWithGrantsThrowsException() public void RevokeAllWithRevocationsThrowsException() { var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-all", "--revoke", "DOMAIN\\UserOrGroup" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); + Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// @@ -168,9 +168,9 @@ public void RevokeAllWithRevocationsThrowsException() public void RevokeAllWithRevokeOthersThrowsException() { var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-all", "--revoke-others" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); + Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// @@ -180,9 +180,9 @@ public void RevokeAllWithRevokeOthersThrowsException() public void RevokeMultipleShouldWork() { var args = new[] { "privilege", "SeServiceLogonRight", "--revoke", "DOMAIN\\User", "--revoke", "DOMAIN\\Group" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -194,9 +194,9 @@ public void RevokeMultipleShouldWork() public void RevokeOthersWithOutGrantsThrowsException() { var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-others" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); + Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// @@ -206,9 +206,9 @@ public void RevokeOthersWithOutGrantsThrowsException() public void RevokeOthersWithRevocationsThrowsException() { var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-others", "--revoke", "DOMAIN\\UserOrGroup" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); + Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// @@ -218,9 +218,9 @@ public void RevokeOthersWithRevocationsThrowsException() public void RevokePatternShouldWork() { var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -232,9 +232,9 @@ public void RevokePatternShouldWork() public void RevokePatternWithGrantShouldWork() { var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--revoke-pattern", "^S-1-5-21-" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -249,9 +249,9 @@ public void RevokePatternWithGrantShouldWork() [InlineData("privilege", "SeServiceLogonRight", "--revoke-pattern", "(?i)^[A-Z]+")] public void RevokePatternWithValidRegexShouldWork(params string[] args) { - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -263,9 +263,9 @@ public void RevokePatternWithValidRegexShouldWork(params string[] args) public void RevokePatternWithRevokeAllThrowsException() { var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-", "--revoke-all" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); + Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// @@ -275,9 +275,9 @@ public void RevokePatternWithRevokeAllThrowsException() public void RevokePatternWithRevokeOthersThrowsException() { var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-", "--revoke-others" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); + Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// @@ -287,9 +287,9 @@ public void RevokePatternWithRevokeOthersThrowsException() public void RevokePatternWithRevokeThrowsException() { var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-", "--revoke", "DOMAIN\\UserOrGroup" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); + Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// @@ -300,7 +300,7 @@ public void RevokePatternWithRevokeThrowsException() [InlineData("privilege", "SeServiceLogonRight", "--revoke-pattern", "[0-9]{3,1}")] [InlineData("privilege", "SeServiceLogonRight", "--revoke-pattern", "^[S-1-5-21-")] public void RevokePatternWithInvalidRegexThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); + => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures an empty or whitespace revocation pattern is rejected. @@ -310,7 +310,7 @@ public void RevokePatternWithInvalidRegexThrowsException(params string[] args) [InlineData("privilege", "SeServiceLogonRight", "--revoke-pattern", "")] [InlineData("privilege", "SeServiceLogonRight", "--revoke-pattern", " ")] public void RevokePatternWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); + => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures revoking a context is accepted. @@ -319,9 +319,9 @@ public void RevokePatternWithInvalidStringThrowsException(params string[] args) public void RevokeShouldWork() { var args = new[] { "privilege", "SeServiceLogonRight", "--revoke", "DOMAIN\\UserOrGroup" }; - var configuration = _builder.Build(); + var rootCommand = _builder.Build(); - var rc = configuration.Parse(args).Validate().Invoke(); + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); Assert.Equal(0, rc); } @@ -334,7 +334,7 @@ public void RevokeShouldWork() [InlineData("privilege", "SeServiceLogonRight", "--revoke", "")] [InlineData("privilege", "SeServiceLogonRight", "--revoke", " ")] public void RevokeWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); + => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures an empty or whitespace system name is rejected. @@ -344,5 +344,5 @@ public void RevokeWithInvalidStringThrowsException(params string[] args) [InlineData("privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--system-name", "")] [InlineData("privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--system-name", " ")] public void SystemNameWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); + => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); } \ No newline at end of file diff --git a/src/Tests.Cli/Tests.Cli.csproj b/src/Tests.Cli/Tests.Cli.csproj index dcf8996..d7c13bb 100644 --- a/src/Tests.Cli/Tests.Cli.csproj +++ b/src/Tests.Cli/Tests.Cli.csproj @@ -19,7 +19,6 @@ - diff --git a/src/UserRights.Cli/CliBuilder.cs b/src/UserRights.Cli/CliBuilder.cs index eeb1ae9..55028b4 100644 --- a/src/UserRights.Cli/CliBuilder.cs +++ b/src/UserRights.Cli/CliBuilder.cs @@ -9,6 +9,7 @@ namespace UserRights.Cli; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -39,10 +40,10 @@ public CliBuilder(ILogger logger, ILsaUserRights policy, IUserRights } /// - /// Builds the command line parser. + /// Builds the command line parser root command. /// - /// A configured command line parser. - public CommandLineConfiguration Build() + /// A configured command line parser root command. + public RootCommand Build() { var rootCommand = new RootCommand("Windows User Rights Assignment Utility") { @@ -62,13 +63,7 @@ public CommandLineConfiguration Build() } } - var configuration = new CommandLineConfiguration(rootCommand) - { - // Disable the default exception handler to allow logging errors to the event log. - EnableDefaultExceptionHandler = false - }; - - return configuration; + return rootCommand; } /// @@ -90,7 +85,9 @@ private Command BuildListCommand() // Ensure the path is a valid string. pathOption.Validators.Add(result => { - if (string.IsNullOrWhiteSpace(result.GetValue(pathOption))) + var path = result.GetValue(pathOption); + + if (string.IsNullOrWhiteSpace(path)) { result.AddError("Path cannot be empty or whitespace."); } @@ -104,7 +101,9 @@ private Command BuildListCommand() // Ensure the system name is a valid string. systemNameOption.Validators.Add(result => { - if (string.IsNullOrWhiteSpace(result.GetValue(systemNameOption))) + var systemName = result.GetValue(systemNameOption); + + if (string.IsNullOrWhiteSpace(systemName)) { result.AddError("System name cannot be empty or whitespace."); } @@ -128,8 +127,11 @@ private Command BuildListCommand() var results = _manager.GetUserRights(_policy); + Func writeAsync = json ? results.ToJson : results.ToCsv; + if (string.IsNullOrWhiteSpace(path)) { + // The invocation configuration's output stream in the parse result is unusable because its encoding cannot be changed. var stream = Console.OpenStandardOutput(); var encoding = Console.OutputEncoding; @@ -137,14 +139,7 @@ private Command BuildListCommand() { Console.OutputEncoding = Encoding.UTF8; - if (json) - { - await results.ToJson(stream, cancellationToken).ConfigureAwait(false); - } - else - { - await results.ToCsv(stream, cancellationToken).ConfigureAwait(false); - } + await writeAsync(stream, cancellationToken).ConfigureAwait(false); } finally { @@ -153,17 +148,10 @@ private Command BuildListCommand() } else { - var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true); + var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: true); await using (stream.ConfigureAwait(false)) { - if (json) - { - await results.ToJson(stream, cancellationToken).ConfigureAwait(false); - } - else - { - await results.ToCsv(stream, cancellationToken).ConfigureAwait(false); - } + await writeAsync(stream, cancellationToken).ConfigureAwait(false); } } }); @@ -215,7 +203,9 @@ private Command BuildPrincipalCommand() // Ensure the principal is a valid string. principalArgument.Validators.Add(result => { - if (string.IsNullOrWhiteSpace(result.GetValue(principalArgument))) + var principal = result.GetValue(principalArgument); + + if (string.IsNullOrWhiteSpace(principal)) { result.AddError("Principal cannot be empty or whitespace."); } @@ -224,9 +214,11 @@ private Command BuildPrincipalCommand() // Ensure principal mode is used with at least one of grant, revoke, or revoke all. principalArgument.Validators.Add(result => { - if (result.GetValue(grantsOption) is not { Length: > 0 } - && result.GetValue(revocationsOption) is not { Length: > 0 } - && !result.GetValue(revokeAllOption)) + var grants = result.GetValue(grantsOption); + var revocations = result.GetValue(revocationsOption); + var revokeAll = result.GetValue(revokeAllOption); + + if (grants is not { Length: > 0 } && revocations is not { Length: > 0 } && !revokeAll) { result.AddError("At least one option is required."); } @@ -235,8 +227,9 @@ private Command BuildPrincipalCommand() // Ensure the grants are valid strings. grantsOption.Validators.Add(result => { - var grantsCollection = result.GetValue(grantsOption) ?? []; - if (grantsCollection.Any(string.IsNullOrWhiteSpace)) + var grantsCollection = result.GetValue(grantsOption); + + if (grantsCollection?.Any(string.IsNullOrWhiteSpace) is true) { result.AddError("Grants cannot be empty or whitespace."); } @@ -264,7 +257,9 @@ private Command BuildPrincipalCommand() // Ensure the revocations are valid strings. revocationsOption.Validators.Add(result => { - if (result.Tokens.Any(p => string.IsNullOrWhiteSpace(p.Value))) + var revocations = result.GetValue(revocationsOption); + + if (revocations?.Any(string.IsNullOrWhiteSpace) is true) { result.AddError("Revocations cannot be empty or whitespace."); } @@ -292,9 +287,12 @@ private Command BuildPrincipalCommand() // Ensure revoke all is not used with any other option. revokeAllOption.Validators.Add(result => { - if (result.GetValue(revokeOthersOption) - || result.GetValue(grantsOption) is { Length: > 0 } - || result.GetValue(revocationsOption) is { Length: > 0 }) + var grants = result.GetValue(grantsOption); + var revocations = result.GetValue(revocationsOption); + var revokeAll = result.GetValue(revokeAllOption); + var revokeOthers = result.GetValue(revokeOthersOption); + + if (revokeAll && (revokeOthers || grants is { Length: > 0 } || revocations is { Length: > 0 })) { result.AddError("Revoke all cannot be used with any other option."); } @@ -303,9 +301,12 @@ private Command BuildPrincipalCommand() // Ensure revoke others is only used with grant. revokeOthersOption.Validators.Add(result => { - if (result.GetValue(revokeAllOption) - || result.GetValue(grantsOption) is not { Length: > 0 } - || result.GetValue(revocationsOption) is { Length: > 0 }) + var grants = result.GetValue(grantsOption); + var revocations = result.GetValue(revocationsOption); + var revokeAll = result.GetValue(revokeAllOption); + var revokeOthers = result.GetValue(revokeOthersOption); + + if (revokeOthers && (revokeAll || grants is not { Length: > 0 } || revocations is { Length: > 0 })) { result.AddError("Revoke others is only valid with grants."); } @@ -314,7 +315,9 @@ private Command BuildPrincipalCommand() // Ensure the system name is a valid string. systemNameOption.Validators.Add(result => { - if (string.IsNullOrWhiteSpace(result.GetValue(systemNameOption))) + var systemName = result.GetValue(systemNameOption); + + if (string.IsNullOrWhiteSpace(systemName)) { result.AddError("System name cannot be empty or whitespace."); } @@ -409,7 +412,9 @@ private Command BuildPrivilegeCommand() // Ensure the principal is a valid string. privilegeArgument.Validators.Add(result => { - if (string.IsNullOrWhiteSpace(result.GetValue(privilegeArgument))) + var privilege = result.GetValue(privilegeArgument); + + if (string.IsNullOrWhiteSpace(privilege)) { result.AddError("Privilege cannot be empty or whitespace."); } @@ -418,10 +423,12 @@ private Command BuildPrivilegeCommand() // Ensure privilege mode is used with at least one of grant, revoke, revoke all, or revoke pattern. privilegeArgument.Validators.Add(result => { - if (result.GetValue(grantsOption) is not { Length: > 0 } - && result.GetValue(revocationsOption) is not { Length: > 0 } - && !result.GetValue(revokeAllOption) - && result.GetValue(revokePatternOption) is null) + var grants = result.GetValue(grantsOption); + var revocations = result.GetValue(revocationsOption); + var revokeAll = result.GetValue(revokeAllOption); + var revokePattern = result.GetValue(revokePatternOption); + + if (grants is not { Length: > 0 } && revocations is not { Length: > 0 } && !revokeAll && revokePattern is null) { result.AddError("At least one option is required."); } @@ -430,7 +437,9 @@ private Command BuildPrivilegeCommand() // Ensure the grants are valid strings. grantsOption.Validators.Add(result => { - if (result.Tokens.Any(p => string.IsNullOrWhiteSpace(p.Value))) + var grants = result.GetValue(grantsOption); + + if (grants?.Any(string.IsNullOrWhiteSpace) is true) { result.AddError("Grants cannot be empty or whitespace."); } @@ -458,7 +467,9 @@ private Command BuildPrivilegeCommand() // Ensure the revocations are valid strings. revocationsOption.Validators.Add(result => { - if (result.Tokens.Any(p => string.IsNullOrWhiteSpace(p.Value))) + var revocations = result.GetValue(revocationsOption); + + if (revocations?.Any(string.IsNullOrWhiteSpace) is true) { result.AddError("Revocations cannot be empty or whitespace."); } @@ -486,10 +497,13 @@ private Command BuildPrivilegeCommand() // Ensure revoke all is not used with any other option. revokeAllOption.Validators.Add(result => { - if (result.GetValue(grantsOption) is { Length: > 0 } - || result.GetValue(revocationsOption) is { Length: > 0 } - || result.GetValue(revokeOthersOption) - || result.GetValue(revokePatternOption) is not null) + var grants = result.GetValue(grantsOption); + var revocations = result.GetValue(revocationsOption); + var revokeAll = result.GetValue(revokeAllOption); + var revokeOthers = result.GetValue(revokeOthersOption); + var revokePattern = result.GetValue(revokePatternOption); + + if (revokeAll && (grants is { Length: > 0 } || revocations is { Length: > 0 } || revokeOthers || revokePattern is not null)) { result.AddError("Revoke all cannot be used with any other option."); } @@ -498,10 +512,13 @@ private Command BuildPrivilegeCommand() // Ensure revoke others is only used with grant. revokeOthersOption.Validators.Add(result => { - if (result.GetValue(grantsOption) is not { Length: > 0 } - || result.GetValue(revocationsOption) is { Length: > 0 } - || result.GetValue(revokeAllOption) - || result.GetValue(revokePatternOption) is not null) + var grants = result.GetValue(grantsOption); + var revocations = result.GetValue(revocationsOption); + var revokeAll = result.GetValue(revokeAllOption); + var revokeOthers = result.GetValue(revokeOthersOption); + var revokePattern = result.GetValue(revokePatternOption); + + if (revokeOthers && (grants is not { Length: > 0 } || revocations is { Length: > 0 } || revokeAll || revokePattern is not null)) { result.AddError("Revoke others is only valid when used with grants."); } @@ -511,6 +528,7 @@ private Command BuildPrivilegeCommand() revokePatternOption.Validators.Add(result => { var revokePattern = result.GetValue(revokePatternOption); + if (string.IsNullOrWhiteSpace(revokePattern)) { result.AddError("Revoke pattern cannot be empty or whitespace."); @@ -531,9 +549,11 @@ private Command BuildPrivilegeCommand() // Ensure revoke pattern is not used with revoke, revoke all, or revoke others. revokePatternOption.Validators.Add(result => { - if (result.GetValue(revocationsOption) is { Length: > 0 } - || result.GetValue(revokeAllOption) - || result.GetValue(revokeOthersOption)) + var revocations = result.GetValue(revocationsOption); + var revokeAll = result.GetValue(revokeAllOption); + var revokeOthers = result.GetValue(revokeOthersOption); + + if (revocations is { Length: > 0 } || revokeAll || revokeOthers) { result.AddError("Revoke pattern is only valid when used alone or with grants."); } @@ -542,7 +562,9 @@ private Command BuildPrivilegeCommand() // Ensure the system name is a valid string. systemNameOption.Validators.Add(result => { - if (string.IsNullOrWhiteSpace(result.GetValue(systemNameOption))) + var systemName = result.GetValue(systemNameOption); + + if (string.IsNullOrWhiteSpace(systemName)) { result.AddError("System name cannot be empty or whitespace."); } diff --git a/src/UserRights.Cli/CliExtensions.cs b/src/UserRights.Cli/CliExtensions.cs index cc4d384..3ccecb9 100644 --- a/src/UserRights.Cli/CliExtensions.cs +++ b/src/UserRights.Cli/CliExtensions.cs @@ -4,6 +4,8 @@ using System.CommandLine; using System.Globalization; using System.Text; +using System.Threading; +using System.Threading.Tasks; /// /// Represents extensions for parsing and invoking command line arguments. @@ -11,12 +13,49 @@ public static class CliExtensions { /// - /// Validates the parse result for any errors. + /// Invokes the appropriate command handler for a parsed command line input. /// /// The command line input parsing results. - /// The validated command line input parsing results. + /// A token that can be used to cancel an invocation. + /// A task whose result can be used as a process exit code. + public static async Task RunAsync(this ParseResult parseResult, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(parseResult); + + var invocationConfiguration = new InvocationConfiguration + { + // Disable the default exception handler to allow logging errors to the event log. + EnableDefaultExceptionHandler = false + }; + + return await parseResult.InvokeAsync(invocationConfiguration, cancellationToken).ConfigureAwait(false); + } + + /// + /// Invokes the appropriate command handler for a parsed command line input. + /// + /// The command line input parsing results. + /// A value that can be used as a process exit code. + public static int Run(this ParseResult parseResult) + { + ArgumentNullException.ThrowIfNull(parseResult); + + var invocationConfiguration = new InvocationConfiguration + { + // Disable the default exception handler to allow logging errors to the event log. + EnableDefaultExceptionHandler = false + }; + + return parseResult.Invoke(invocationConfiguration); + } + + /// + /// Throws a if contains any errors. + /// + /// The command line input parsing results. + /// The same command line input parsing results. /// Thrown when the parse results contain any errors. - public static ParseResult Validate(this ParseResult parseResult) + public static ParseResult ThrowIfInvalid(this ParseResult parseResult) { ArgumentNullException.ThrowIfNull(parseResult); diff --git a/src/UserRights.Cli/HelpExamplesAction.cs b/src/UserRights.Cli/HelpExamplesAction.cs index add3481..1a34c48 100644 --- a/src/UserRights.Cli/HelpExamplesAction.cs +++ b/src/UserRights.Cli/HelpExamplesAction.cs @@ -25,6 +25,9 @@ public HelpExamplesAction(HelpAction helpAction) _helpAction = helpAction; } + /// + public override bool ClearsParseErrors => true; + /// public override int Invoke(ParseResult parseResult) { @@ -97,6 +100,6 @@ private static void GenerateExamples(ParseResult parseResult) stringBuilder.AppendLine(CultureInfo.InvariantCulture, $" {ProgramInfo.Program} {example}"); } - parseResult.Configuration.Output.WriteLine(stringBuilder.ToString().TrimEnd()); + parseResult.InvocationConfiguration.Output.WriteLine(stringBuilder.ToString().TrimEnd()); } } \ No newline at end of file diff --git a/src/UserRights/Program.cs b/src/UserRights/Program.cs index fb75897..1ac132c 100644 --- a/src/UserRights/Program.cs +++ b/src/UserRights/Program.cs @@ -6,12 +6,14 @@ namespace UserRights; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; + using Serilog; using Serilog.Context; using Serilog.Core; using Serilog.Events; using Serilog.Templates; using Serilog.Templates.Themes; + using UserRights.Application; using UserRights.Cli; @@ -144,14 +146,15 @@ private static async Task Run(string[] args) var builder = serviceProvider.GetRequiredService(); - var configuration = builder.Build(); + var rootCommand = builder.Build(); + + var parseResult = rootCommand.Parse(args).ThrowIfInvalid(); - var parseResult = configuration.Parse(args); if (!string.Equals(parseResult.CommandResult.Command.Name, "list", StringComparison.Ordinal)) { levelSwitch.MinimumLevel = LogEventLevel.Verbose; } - return await parseResult.Validate().InvokeAsync().ConfigureAwait(false); + return await parseResult.RunAsync().ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/UserRights/UserRights.csproj b/src/UserRights/UserRights.csproj index 457d03d..59c4c78 100644 --- a/src/UserRights/UserRights.csproj +++ b/src/UserRights/UserRights.csproj @@ -18,7 +18,6 @@ - From e89291c6c7bc27adae375c8e26a548f403727392 Mon Sep 17 00:00:00 2001 From: "Joseph L. Casale" <9957114+jcasale@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:30:49 -0600 Subject: [PATCH 07/15] Updates .NET SDK to 10.0.100. --- .github/workflows/main.yml | 3 ++- src/Directory.Packages.props | 10 +++++----- src/Tests.Application/Tests.Application.csproj | 3 +-- src/Tests.Cli/Tests.Cli.csproj | 3 +-- src/Tests/Tests.csproj | 2 +- .../UserRights.Application.csproj | 2 +- src/UserRights.Cli/UserRights.Cli.csproj | 2 +- src/UserRights.Extensions/UserRights.Extensions.csproj | 2 +- src/UserRights/UserRights.csproj | 2 +- src/global.json | 2 +- 10 files changed, 15 insertions(+), 16 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cafedee..611bc6d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,8 @@ jobs: - name: Set dotnet version id: dotnet-version run: | - $version = [version]::new('${{ steps.setup-dotnet.outputs.dotnet-version }}') + $str = '${{ steps.setup-dotnet.outputs.dotnet-version }}'.Split('-')[0] + $version = [version]::new($str) $label = 'net{0}{1}' -f $version.Major, $version.Minor echo $label diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index e44f49a..30e4c1e 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -6,11 +6,11 @@ - - - - - + + + + + diff --git a/src/Tests.Application/Tests.Application.csproj b/src/Tests.Application/Tests.Application.csproj index fab47e4..e5ea1b8 100644 --- a/src/Tests.Application/Tests.Application.csproj +++ b/src/Tests.Application/Tests.Application.csproj @@ -1,7 +1,7 @@  - net9.0-windows + net10.0-windows enable latest latest-All @@ -21,7 +21,6 @@ - all diff --git a/src/Tests.Cli/Tests.Cli.csproj b/src/Tests.Cli/Tests.Cli.csproj index d7c13bb..7aecd4d 100644 --- a/src/Tests.Cli/Tests.Cli.csproj +++ b/src/Tests.Cli/Tests.Cli.csproj @@ -1,7 +1,7 @@  - net9.0-windows + net10.0-windows enable latest latest-All @@ -19,7 +19,6 @@ - all diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj index 548839b..2c87cb9 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -1,7 +1,7 @@ - net9.0-windows + net10.0-windows enable latest latest-All diff --git a/src/UserRights.Application/UserRights.Application.csproj b/src/UserRights.Application/UserRights.Application.csproj index 62a3825..7eb6ad2 100644 --- a/src/UserRights.Application/UserRights.Application.csproj +++ b/src/UserRights.Application/UserRights.Application.csproj @@ -1,7 +1,7 @@ - net9.0-windows + net10.0-windows enable latest latest-All diff --git a/src/UserRights.Cli/UserRights.Cli.csproj b/src/UserRights.Cli/UserRights.Cli.csproj index 40a1f4f..8d1df3e 100644 --- a/src/UserRights.Cli/UserRights.Cli.csproj +++ b/src/UserRights.Cli/UserRights.Cli.csproj @@ -1,7 +1,7 @@ - net9.0-windows + net10.0-windows enable latest latest-All diff --git a/src/UserRights.Extensions/UserRights.Extensions.csproj b/src/UserRights.Extensions/UserRights.Extensions.csproj index b1bd7d9..87a459d 100644 --- a/src/UserRights.Extensions/UserRights.Extensions.csproj +++ b/src/UserRights.Extensions/UserRights.Extensions.csproj @@ -1,7 +1,7 @@ - net9.0-windows + net10.0-windows enable latest latest-All diff --git a/src/UserRights/UserRights.csproj b/src/UserRights/UserRights.csproj index 59c4c78..5685e22 100644 --- a/src/UserRights/UserRights.csproj +++ b/src/UserRights/UserRights.csproj @@ -2,7 +2,7 @@ Exe - net9.0-windows + net10.0-windows enable latest latest-All diff --git a/src/global.json b/src/global.json index 918e756..5f2cbb2 100644 --- a/src/global.json +++ b/src/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.305", + "version": "10.0.100", "rollForward": "latestFeature" } } \ No newline at end of file From 6ae603e63310a0e0be48586e722680483fa4a9de Mon Sep 17 00:00:00 2001 From: "Joseph L. Casale" <9957114+jcasale@users.noreply.github.com> Date: Sun, 2 Nov 2025 08:34:42 -0700 Subject: [PATCH 08/15] Adds missing format provider. --- src/UserRights/EventIdProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UserRights/EventIdProvider.cs b/src/UserRights/EventIdProvider.cs index 2c4effd..6e0ca49 100644 --- a/src/UserRights/EventIdProvider.cs +++ b/src/UserRights/EventIdProvider.cs @@ -25,10 +25,10 @@ public ushort ComputeEventId(LogEvent logEvent) var id = property switch { // The EventId property was provided by Serilog directly. - ScalarValue scalar => scalar.ToString(), + ScalarValue scalar => scalar.ToString("G", NumberFormatInfo.InvariantInfo), // The EventId property was provided by Microsoft.Extensions.Logging. - StructureValue structure => structure.Properties.FirstOrDefault(p => string.Equals("Id", p.Name, StringComparison.OrdinalIgnoreCase))?.Value.ToString(), + StructureValue structure => structure.Properties.FirstOrDefault(p => string.Equals("Id", p.Name, StringComparison.OrdinalIgnoreCase))?.Value.ToString("G", NumberFormatInfo.InvariantInfo), _ => null }; From 89e2863b7f6b3692e4308189dd582b5a562ed048 Mon Sep 17 00:00:00 2001 From: "Joseph L. Casale" <9957114+jcasale@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:02:10 -0700 Subject: [PATCH 09/15] Enables implicit usings. --- .../LsaUserRightsConnectTests.cs | 2 - .../LsaUserRightsGetPrincipalsTests.cs | 2 - .../LsaUserRightsGrantPrivilegeTests.cs | 1 - .../LsaUserRightsRevokePrivilegeTests.cs | 1 - .../LsaUserRightsTestBase.cs | 4 -- .../Tests.Application.csproj | 1 + .../UserRightsManagerListTests.cs | 5 --- .../UserRightsManagerPrincipalTests.cs | 3 -- .../UserRightsManagerPrivilegeTests.cs | 3 -- .../UserRightsManagerTestBase.cs | 2 - src/Tests.Cli/CliTestBase.cs | 2 - src/Tests.Cli/ListCommandTests.cs | 5 --- src/Tests.Cli/PrincipalCommandTests.cs | 3 -- src/Tests.Cli/PrivilegeCommandTests.cs | 3 -- src/Tests.Cli/Tests.Cli.csproj | 1 + src/Tests/MockLsaUserRights.cs | 3 -- src/Tests/MockUserRightsManager.cs | 1 - src/Tests/TestData.cs | 1 - src/Tests/Tests.csproj | 1 + src/Tests/UserRightEntryEqualityComparer.cs | 3 -- .../IUserRightsManager.cs | 1 - src/UserRights.Application/LsaUserRights.cs | 1 - src/UserRights.Application/UserRightEntry.cs | 2 - .../UserRights.Application.csproj | 1 + .../UserRightsManager.cs | 3 -- src/UserRights.Cli/CliBuilder.cs | 6 --- src/UserRights.Cli/CliExtensions.cs | 3 -- src/UserRights.Cli/HelpExamplesAction.cs | 1 - src/UserRights.Cli/ProgramInfo.cs | 1 - src/UserRights.Cli/SyntaxException.cs | 2 - src/UserRights.Cli/UserRights.Cli.csproj | 1 + .../Security/SecurityExtensions.cs | 1 - .../Security/SecurityTranslationException.cs | 2 - .../Serialization/SerializationException.cs | 2 - .../Serialization/SerializationExtensions.cs | 45 +++++++++---------- .../UserRights.Extensions.csproj | 1 + src/UserRights/ConsoleExceptionEnricher.cs | 1 - src/UserRights/EventIdProvider.cs | 2 - src/UserRights/Program.cs | 2 - src/UserRights/UserRights.csproj | 1 + 40 files changed, 27 insertions(+), 99 deletions(-) diff --git a/src/Tests.Application/LsaUserRightsConnectTests.cs b/src/Tests.Application/LsaUserRightsConnectTests.cs index 1a8c063..68a4ad3 100644 --- a/src/Tests.Application/LsaUserRightsConnectTests.cs +++ b/src/Tests.Application/LsaUserRightsConnectTests.cs @@ -1,7 +1,5 @@ namespace Tests.Application; -using System; - using UserRights.Application; using Xunit; diff --git a/src/Tests.Application/LsaUserRightsGetPrincipalsTests.cs b/src/Tests.Application/LsaUserRightsGetPrincipalsTests.cs index 6db92cc..c837472 100644 --- a/src/Tests.Application/LsaUserRightsGetPrincipalsTests.cs +++ b/src/Tests.Application/LsaUserRightsGetPrincipalsTests.cs @@ -1,7 +1,5 @@ namespace Tests.Application; -using System; -using System.Linq; using System.Security.Principal; using UserRights.Application; diff --git a/src/Tests.Application/LsaUserRightsGrantPrivilegeTests.cs b/src/Tests.Application/LsaUserRightsGrantPrivilegeTests.cs index 4392c17..63ada34 100644 --- a/src/Tests.Application/LsaUserRightsGrantPrivilegeTests.cs +++ b/src/Tests.Application/LsaUserRightsGrantPrivilegeTests.cs @@ -1,6 +1,5 @@ namespace Tests.Application; -using System; using System.Security.Principal; using UserRights.Application; diff --git a/src/Tests.Application/LsaUserRightsRevokePrivilegeTests.cs b/src/Tests.Application/LsaUserRightsRevokePrivilegeTests.cs index f6fa5a0..1984c28 100644 --- a/src/Tests.Application/LsaUserRightsRevokePrivilegeTests.cs +++ b/src/Tests.Application/LsaUserRightsRevokePrivilegeTests.cs @@ -1,6 +1,5 @@ namespace Tests.Application; -using System; using System.Security.Principal; using UserRights.Application; diff --git a/src/Tests.Application/LsaUserRightsTestBase.cs b/src/Tests.Application/LsaUserRightsTestBase.cs index 2b89597..44139b7 100644 --- a/src/Tests.Application/LsaUserRightsTestBase.cs +++ b/src/Tests.Application/LsaUserRightsTestBase.cs @@ -1,12 +1,8 @@ namespace Tests.Application; -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; -using System.IO; -using System.Linq; using System.Reflection; using System.Security.Principal; using System.Text; diff --git a/src/Tests.Application/Tests.Application.csproj b/src/Tests.Application/Tests.Application.csproj index e5ea1b8..a291750 100644 --- a/src/Tests.Application/Tests.Application.csproj +++ b/src/Tests.Application/Tests.Application.csproj @@ -2,6 +2,7 @@ net10.0-windows + enable enable latest latest-All diff --git a/src/Tests.Application/UserRightsManagerListTests.cs b/src/Tests.Application/UserRightsManagerListTests.cs index 530c783..c2bf744 100644 --- a/src/Tests.Application/UserRightsManagerListTests.cs +++ b/src/Tests.Application/UserRightsManagerListTests.cs @@ -1,14 +1,9 @@ namespace Tests.Application; -using System; -using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Linq; using System.Security.Principal; using System.Text; using System.Text.Json; -using System.Threading.Tasks; using CsvHelper; using CsvHelper.Configuration; diff --git a/src/Tests.Application/UserRightsManagerPrincipalTests.cs b/src/Tests.Application/UserRightsManagerPrincipalTests.cs index 3e5fd15..5ce9c04 100644 --- a/src/Tests.Application/UserRightsManagerPrincipalTests.cs +++ b/src/Tests.Application/UserRightsManagerPrincipalTests.cs @@ -1,8 +1,5 @@ namespace Tests.Application; -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Principal; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Tests.Application/UserRightsManagerPrivilegeTests.cs b/src/Tests.Application/UserRightsManagerPrivilegeTests.cs index 3ec99b4..95634ff 100644 --- a/src/Tests.Application/UserRightsManagerPrivilegeTests.cs +++ b/src/Tests.Application/UserRightsManagerPrivilegeTests.cs @@ -1,8 +1,5 @@ namespace Tests.Application; -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Principal; using System.Text.RegularExpressions; diff --git a/src/Tests.Application/UserRightsManagerTestBase.cs b/src/Tests.Application/UserRightsManagerTestBase.cs index 2473187..9b0fe1d 100644 --- a/src/Tests.Application/UserRightsManagerTestBase.cs +++ b/src/Tests.Application/UserRightsManagerTestBase.cs @@ -1,7 +1,5 @@ namespace Tests.Application; -using System; - using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using UserRights.Application; diff --git a/src/Tests.Cli/CliTestBase.cs b/src/Tests.Cli/CliTestBase.cs index 33a8b12..7233f07 100644 --- a/src/Tests.Cli/CliTestBase.cs +++ b/src/Tests.Cli/CliTestBase.cs @@ -1,7 +1,5 @@ namespace Tests.Cli; -using System; - using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/src/Tests.Cli/ListCommandTests.cs b/src/Tests.Cli/ListCommandTests.cs index 77e44ea..bece5d0 100644 --- a/src/Tests.Cli/ListCommandTests.cs +++ b/src/Tests.Cli/ListCommandTests.cs @@ -1,13 +1,8 @@ namespace Tests.Cli; -using System; -using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Linq; using System.Security.Principal; using System.Text.Json; -using System.Threading.Tasks; using CsvHelper; using CsvHelper.Configuration; diff --git a/src/Tests.Cli/PrincipalCommandTests.cs b/src/Tests.Cli/PrincipalCommandTests.cs index 31f2bf1..7c819d8 100644 --- a/src/Tests.Cli/PrincipalCommandTests.cs +++ b/src/Tests.Cli/PrincipalCommandTests.cs @@ -1,8 +1,5 @@ namespace Tests.Cli; -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Principal; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Tests.Cli/PrivilegeCommandTests.cs b/src/Tests.Cli/PrivilegeCommandTests.cs index 1628d69..451559d 100644 --- a/src/Tests.Cli/PrivilegeCommandTests.cs +++ b/src/Tests.Cli/PrivilegeCommandTests.cs @@ -1,8 +1,5 @@ namespace Tests.Cli; -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Principal; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Tests.Cli/Tests.Cli.csproj b/src/Tests.Cli/Tests.Cli.csproj index 7aecd4d..2ce29e5 100644 --- a/src/Tests.Cli/Tests.Cli.csproj +++ b/src/Tests.Cli/Tests.Cli.csproj @@ -2,6 +2,7 @@ net10.0-windows + enable enable latest latest-All diff --git a/src/Tests/MockLsaUserRights.cs b/src/Tests/MockLsaUserRights.cs index f6d6b9e..8ead010 100644 --- a/src/Tests/MockLsaUserRights.cs +++ b/src/Tests/MockLsaUserRights.cs @@ -1,8 +1,5 @@ namespace Tests; -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Principal; using UserRights.Application; diff --git a/src/Tests/MockUserRightsManager.cs b/src/Tests/MockUserRightsManager.cs index 38ed591..df92593 100644 --- a/src/Tests/MockUserRightsManager.cs +++ b/src/Tests/MockUserRightsManager.cs @@ -1,6 +1,5 @@ namespace Tests; -using System.Collections.Generic; using System.Text.RegularExpressions; using UserRights.Application; diff --git a/src/Tests/TestData.cs b/src/Tests/TestData.cs index 2acca78..16bd207 100644 --- a/src/Tests/TestData.cs +++ b/src/Tests/TestData.cs @@ -1,6 +1,5 @@ namespace Tests; -using System; using System.Security.Principal; /// diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj index 2c87cb9..fb01afb 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -2,6 +2,7 @@ net10.0-windows + enable enable latest latest-All diff --git a/src/Tests/UserRightEntryEqualityComparer.cs b/src/Tests/UserRightEntryEqualityComparer.cs index 91f19e4..ab50afd 100644 --- a/src/Tests/UserRightEntryEqualityComparer.cs +++ b/src/Tests/UserRightEntryEqualityComparer.cs @@ -1,8 +1,5 @@ namespace Tests; -using System; -using System.Collections.Generic; - using UserRights.Application; /// diff --git a/src/UserRights.Application/IUserRightsManager.cs b/src/UserRights.Application/IUserRightsManager.cs index d125503..fa513e3 100644 --- a/src/UserRights.Application/IUserRightsManager.cs +++ b/src/UserRights.Application/IUserRightsManager.cs @@ -1,6 +1,5 @@ namespace UserRights.Application; -using System.Collections.Generic; using System.Text.RegularExpressions; /// diff --git a/src/UserRights.Application/LsaUserRights.cs b/src/UserRights.Application/LsaUserRights.cs index 1369f4e..17bf93b 100644 --- a/src/UserRights.Application/LsaUserRights.cs +++ b/src/UserRights.Application/LsaUserRights.cs @@ -1,6 +1,5 @@ namespace UserRights.Application; -using System; using System.ComponentModel; using System.Runtime.InteropServices; using System.Security.Principal; diff --git a/src/UserRights.Application/UserRightEntry.cs b/src/UserRights.Application/UserRightEntry.cs index 6ea5c0b..121fb4c 100644 --- a/src/UserRights.Application/UserRightEntry.cs +++ b/src/UserRights.Application/UserRightEntry.cs @@ -1,7 +1,5 @@ namespace UserRights.Application; -using System; - /// /// Represents an entry in the local security database. /// diff --git a/src/UserRights.Application/UserRights.Application.csproj b/src/UserRights.Application/UserRights.Application.csproj index 7eb6ad2..ea1454b 100644 --- a/src/UserRights.Application/UserRights.Application.csproj +++ b/src/UserRights.Application/UserRights.Application.csproj @@ -2,6 +2,7 @@ net10.0-windows + enable enable latest latest-All diff --git a/src/UserRights.Application/UserRightsManager.cs b/src/UserRights.Application/UserRightsManager.cs index 30915b4..2248ab9 100644 --- a/src/UserRights.Application/UserRightsManager.cs +++ b/src/UserRights.Application/UserRightsManager.cs @@ -1,8 +1,5 @@ namespace UserRights.Application; -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Principal; using System.Text.RegularExpressions; diff --git a/src/UserRights.Cli/CliBuilder.cs b/src/UserRights.Cli/CliBuilder.cs index 55028b4..080d586 100644 --- a/src/UserRights.Cli/CliBuilder.cs +++ b/src/UserRights.Cli/CliBuilder.cs @@ -1,16 +1,10 @@ namespace UserRights.Cli; -using System; -using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Help; using System.Globalization; -using System.IO; -using System.Linq; using System.Text; using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; diff --git a/src/UserRights.Cli/CliExtensions.cs b/src/UserRights.Cli/CliExtensions.cs index 3ccecb9..75905e8 100644 --- a/src/UserRights.Cli/CliExtensions.cs +++ b/src/UserRights.Cli/CliExtensions.cs @@ -1,11 +1,8 @@ namespace UserRights.Cli; -using System; using System.CommandLine; using System.Globalization; using System.Text; -using System.Threading; -using System.Threading.Tasks; /// /// Represents extensions for parsing and invoking command line arguments. diff --git a/src/UserRights.Cli/HelpExamplesAction.cs b/src/UserRights.Cli/HelpExamplesAction.cs index 1a34c48..8bfa050 100644 --- a/src/UserRights.Cli/HelpExamplesAction.cs +++ b/src/UserRights.Cli/HelpExamplesAction.cs @@ -1,6 +1,5 @@ namespace UserRights.Cli; -using System; using System.CommandLine; using System.CommandLine.Help; using System.CommandLine.Invocation; diff --git a/src/UserRights.Cli/ProgramInfo.cs b/src/UserRights.Cli/ProgramInfo.cs index 7255e25..ac22d93 100644 --- a/src/UserRights.Cli/ProgramInfo.cs +++ b/src/UserRights.Cli/ProgramInfo.cs @@ -1,6 +1,5 @@ namespace UserRights.Cli; -using System; using System.Reflection; /// diff --git a/src/UserRights.Cli/SyntaxException.cs b/src/UserRights.Cli/SyntaxException.cs index b0f8a70..0445c80 100644 --- a/src/UserRights.Cli/SyntaxException.cs +++ b/src/UserRights.Cli/SyntaxException.cs @@ -1,7 +1,5 @@ namespace UserRights.Cli; -using System; - /// /// Represents the exception that is thrown when a syntax error occurs. /// diff --git a/src/UserRights.Cli/UserRights.Cli.csproj b/src/UserRights.Cli/UserRights.Cli.csproj index 8d1df3e..29dbafb 100644 --- a/src/UserRights.Cli/UserRights.Cli.csproj +++ b/src/UserRights.Cli/UserRights.Cli.csproj @@ -2,6 +2,7 @@ net10.0-windows + enable enable latest latest-All diff --git a/src/UserRights.Extensions/Security/SecurityExtensions.cs b/src/UserRights.Extensions/Security/SecurityExtensions.cs index 7f8f7d1..21c3607 100644 --- a/src/UserRights.Extensions/Security/SecurityExtensions.cs +++ b/src/UserRights.Extensions/Security/SecurityExtensions.cs @@ -1,6 +1,5 @@ namespace UserRights.Extensions.Security; -using System; using System.Security.Principal; /// diff --git a/src/UserRights.Extensions/Security/SecurityTranslationException.cs b/src/UserRights.Extensions/Security/SecurityTranslationException.cs index f1af5f4..e5ed89a 100644 --- a/src/UserRights.Extensions/Security/SecurityTranslationException.cs +++ b/src/UserRights.Extensions/Security/SecurityTranslationException.cs @@ -1,7 +1,5 @@ namespace UserRights.Extensions.Security; -using System; - /// /// Represents the exception thrown when an error occurs translating security contexts. /// diff --git a/src/UserRights.Extensions/Serialization/SerializationException.cs b/src/UserRights.Extensions/Serialization/SerializationException.cs index 283a1a4..cca891e 100644 --- a/src/UserRights.Extensions/Serialization/SerializationException.cs +++ b/src/UserRights.Extensions/Serialization/SerializationException.cs @@ -1,7 +1,5 @@ namespace UserRights.Extensions.Serialization; -using System; - /// /// Represents the exception thrown when an error occurs serializing data. /// diff --git a/src/UserRights.Extensions/Serialization/SerializationExtensions.cs b/src/UserRights.Extensions/Serialization/SerializationExtensions.cs index 46c7c52..2aa7a68 100644 --- a/src/UserRights.Extensions/Serialization/SerializationExtensions.cs +++ b/src/UserRights.Extensions/Serialization/SerializationExtensions.cs @@ -1,13 +1,8 @@ namespace UserRights.Extensions.Serialization; -using System; -using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Text; using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; using CsvHelper; using CsvHelper.Configuration; @@ -18,14 +13,14 @@ namespace UserRights.Extensions.Serialization; public static class SerializationExtensions { /// - /// Configures the JSON serializer options to format output as indented. - /// - private static readonly JsonSerializerOptions Options = new() - { - WriteIndented = true - }; - - /// + /// Configures the JSON serializer options to format output as indented. + /// + private static readonly JsonSerializerOptions Options = new() + { + WriteIndented = true + }; + + /// /// Serializes data to a string in CSV format. /// /// The type of data. @@ -45,15 +40,15 @@ public static async Task ToCsv(this IEnumerable data, Stream stream, Cance try { - var writer = new StreamWriter(stream, new UTF8Encoding(false), leaveOpen: true); - await using (writer.ConfigureAwait(false)) - { - var csv = new CsvWriter(writer, csvConfiguration, leaveOpen: true); - await using (csv.ConfigureAwait(false)) - { - await csv.WriteRecordsAsync(data, cancellationToken).ConfigureAwait(false); - } - } + var writer = new StreamWriter(stream, new UTF8Encoding(false), leaveOpen: true); + await using (writer.ConfigureAwait(false)) + { + var csv = new CsvWriter(writer, csvConfiguration, leaveOpen: true); + await using (csv.ConfigureAwait(false)) + { + await csv.WriteRecordsAsync(data, cancellationToken).ConfigureAwait(false); + } + } } catch (Exception e) { @@ -66,8 +61,8 @@ public static async Task ToCsv(this IEnumerable data, Stream stream, Cance /// /// The type of data. /// The data to serialize. - /// The UTF-8 stream to write to. - /// The that can be used to cancel the write operation. + /// The UTF-8 stream to write to. + /// The that can be used to cancel the write operation. /// A task that represents the asynchronous write operation. public static async Task ToJson(this T data, Stream stream, CancellationToken cancellationToken = default) { @@ -76,7 +71,7 @@ public static async Task ToJson(this T data, Stream stream, CancellationToken try { - await JsonSerializer.SerializeAsync(stream, data, Options, cancellationToken).ConfigureAwait(false); + await JsonSerializer.SerializeAsync(stream, data, Options, cancellationToken).ConfigureAwait(false); } catch (Exception e) { diff --git a/src/UserRights.Extensions/UserRights.Extensions.csproj b/src/UserRights.Extensions/UserRights.Extensions.csproj index 87a459d..001702e 100644 --- a/src/UserRights.Extensions/UserRights.Extensions.csproj +++ b/src/UserRights.Extensions/UserRights.Extensions.csproj @@ -2,6 +2,7 @@ net10.0-windows + enable enable latest latest-All diff --git a/src/UserRights/ConsoleExceptionEnricher.cs b/src/UserRights/ConsoleExceptionEnricher.cs index 746c63c..24d4e44 100644 --- a/src/UserRights/ConsoleExceptionEnricher.cs +++ b/src/UserRights/ConsoleExceptionEnricher.cs @@ -1,6 +1,5 @@ namespace UserRights; -using System; using System.Globalization; using System.Text; diff --git a/src/UserRights/EventIdProvider.cs b/src/UserRights/EventIdProvider.cs index 6e0ca49..e2500c6 100644 --- a/src/UserRights/EventIdProvider.cs +++ b/src/UserRights/EventIdProvider.cs @@ -1,8 +1,6 @@ namespace UserRights; -using System; using System.Globalization; -using System.Linq; using Serilog.Events; using Serilog.Sinks.EventLog; diff --git a/src/UserRights/Program.cs b/src/UserRights/Program.cs index 1ac132c..2e315b9 100644 --- a/src/UserRights/Program.cs +++ b/src/UserRights/Program.cs @@ -1,9 +1,7 @@ namespace UserRights; -using System; using System.Globalization; using System.Text; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; diff --git a/src/UserRights/UserRights.csproj b/src/UserRights/UserRights.csproj index 5685e22..b14aba5 100644 --- a/src/UserRights/UserRights.csproj +++ b/src/UserRights/UserRights.csproj @@ -3,6 +3,7 @@ Exe net10.0-windows + enable enable latest latest-All From 5fe7c6e3ebc54508b8d23b478c761b3ed203704a Mon Sep 17 00:00:00 2001 From: "Joseph L. Casale" <9957114+jcasale@users.noreply.github.com> Date: Tue, 4 Nov 2025 03:16:36 -0700 Subject: [PATCH 10/15] Updates argument validation. --- src/UserRights.Application/LsaUserRights.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/UserRights.Application/LsaUserRights.cs b/src/UserRights.Application/LsaUserRights.cs index 17bf93b..59a291d 100644 --- a/src/UserRights.Application/LsaUserRights.cs +++ b/src/UserRights.Application/LsaUserRights.cs @@ -55,11 +55,7 @@ public unsafe void LsaAddAccountRights(SecurityIdentifier accountSid, params str ArgumentNullException.ThrowIfNull(accountSid); ArgumentNullException.ThrowIfNull(userRights); - - if (userRights.Length == 0) - { - throw new ArgumentOutOfRangeException(nameof(userRights), "Value cannot be an empty collection."); - } + ArgumentOutOfRangeException.ThrowIfZero(userRights.Length, nameof(userRights)); var bytes = new byte[accountSid.BinaryLength]; accountSid.GetBinaryForm(bytes, 0); @@ -230,11 +226,7 @@ public unsafe void LsaRemoveAccountRights(SecurityIdentifier accountSid, params ArgumentNullException.ThrowIfNull(accountSid); ArgumentNullException.ThrowIfNull(userRights); - - if (userRights.Length == 0) - { - throw new ArgumentOutOfRangeException(nameof(userRights), "Value cannot be an empty collection."); - } + ArgumentOutOfRangeException.ThrowIfZero(userRights.Length, nameof(userRights)); var bytes = new byte[accountSid.BinaryLength]; accountSid.GetBinaryForm(bytes, 0); From 6be5a57404ee790d5da462db3bfba0f883dc0042 Mon Sep 17 00:00:00 2001 From: "Joseph L. Casale" <9957114+jcasale@users.noreply.github.com> Date: Tue, 4 Nov 2025 03:17:26 -0700 Subject: [PATCH 11/15] Fixes disposal pattern. --- src/UserRights.Application/LsaUserRights.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/UserRights.Application/LsaUserRights.cs b/src/UserRights.Application/LsaUserRights.cs index 59a291d..3e9393c 100644 --- a/src/UserRights.Application/LsaUserRights.cs +++ b/src/UserRights.Application/LsaUserRights.cs @@ -277,8 +277,9 @@ protected virtual void Dispose(bool disposing) if (disposing) { _handle?.Dispose(); - _disposed = true; } + + _disposed = true; } /// From fab1a53715c46dea6ffa72e53660558b155dc617 Mon Sep 17 00:00:00 2001 From: "Joseph L. Casale" <9957114+jcasale@users.noreply.github.com> Date: Tue, 4 Nov 2025 03:21:26 -0700 Subject: [PATCH 12/15] Updates Microsoft.Windows.CsWin32. --- src/Directory.Packages.props | 2 +- src/UserRights.Application/LsaUserRights.cs | 51 ++++++++------------- src/UserRights.Cli/CliBuilder.cs | 7 +-- 3 files changed, 23 insertions(+), 37 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 30e4c1e..1bc4e3a 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -12,7 +12,7 @@ - + diff --git a/src/UserRights.Application/LsaUserRights.cs b/src/UserRights.Application/LsaUserRights.cs index 3e9393c..b5de3ac 100644 --- a/src/UserRights.Application/LsaUserRights.cs +++ b/src/UserRights.Application/LsaUserRights.cs @@ -62,8 +62,6 @@ public unsafe void LsaAddAccountRights(SecurityIdentifier accountSid, params str fixed (byte* b = bytes) { var psid = new PSID(b); - using var ssid = new LsaCloseSafeHandle(psid); - Span rights = stackalloc LSA_UNICODE_STRING[userRights.Length]; for (var i = 0; i < userRights.Length; i++) { @@ -82,12 +80,11 @@ public unsafe void LsaAddAccountRights(SecurityIdentifier accountSid, params str } } - var status = PInvoke.LsaAddAccountRights(_handle, ssid, rights); - var error = PInvoke.LsaNtStatusToWinError(status); - - if ((WIN32_ERROR)error != WIN32_ERROR.ERROR_SUCCESS) + var status = PInvoke.LsaAddAccountRights(_handle, psid, rights); + if (status != NTSTATUS.STATUS_SUCCESS) { - throw new Win32Exception((int)error); + var error = PInvoke.LsaNtStatusToWinError(status); + throw new Win32Exception(unchecked((int)error), "Failed to add the account right."); } } } @@ -109,17 +106,15 @@ public unsafe string[] LsaEnumerateAccountRights(SecurityIdentifier accountSid) fixed (byte* b = bytes) { var psid = new PSID(b); - using var ssid = new LsaCloseSafeHandle(psid); LSA_UNICODE_STRING* userRights = null; try { - var status = PInvoke.LsaEnumerateAccountRights(_handle, ssid, out userRights, out var count); - var error = (WIN32_ERROR)PInvoke.LsaNtStatusToWinError(status); - - if (error != WIN32_ERROR.ERROR_SUCCESS) + var status = PInvoke.LsaEnumerateAccountRights(_handle, psid, out userRights, out var count); + if (status != NTSTATUS.STATUS_SUCCESS) { - throw new Win32Exception((int)error); + var error = PInvoke.LsaNtStatusToWinError(status); + throw new Win32Exception(unchecked((int)error), "Failed to enumerate account rights."); } var results = new string[count]; @@ -137,10 +132,7 @@ public unsafe string[] LsaEnumerateAccountRights(SecurityIdentifier accountSid) } finally { - if (userRights is not null) - { - PInvoke.LsaFreeMemory(userRights); - } + PInvoke.LsaFreeMemory(userRights); } } } @@ -188,7 +180,7 @@ SecurityIdentifier[] Method(LSA_UNICODE_STRING right) if (error != WIN32_ERROR.ERROR_SUCCESS) { - throw new Win32Exception((int)error); + throw new Win32Exception(unchecked((int)error), "Failed to enumerate accounts with right."); } var results = new SecurityIdentifier[count]; @@ -206,10 +198,7 @@ SecurityIdentifier[] Method(LSA_UNICODE_STRING right) } finally { - if (buffer is not null) - { - PInvoke.LsaFreeMemory(buffer); - } + PInvoke.LsaFreeMemory(buffer); } } } @@ -233,8 +222,6 @@ public unsafe void LsaRemoveAccountRights(SecurityIdentifier accountSid, params fixed (byte* b = bytes) { var psid = new PSID(b); - using var ssid = new LsaCloseSafeHandle(psid); - Span rights = stackalloc LSA_UNICODE_STRING[userRights.Length]; for (var i = 0; i < userRights.Length; i++) { @@ -253,12 +240,11 @@ public unsafe void LsaRemoveAccountRights(SecurityIdentifier accountSid, params } } - var status = PInvoke.LsaRemoveAccountRights(_handle, ssid, false, rights); - var error = PInvoke.LsaNtStatusToWinError(status); - - if ((WIN32_ERROR)error != WIN32_ERROR.ERROR_SUCCESS) + var status = PInvoke.LsaRemoveAccountRights(_handle, psid, false, rights); + if (status != NTSTATUS.STATUS_SUCCESS) { - throw new Win32Exception((int)error); + var error = PInvoke.LsaNtStatusToWinError(status); + throw new Win32Exception(unchecked((int)error), "Failed to remove the account right."); } } } @@ -319,11 +305,10 @@ private unsafe LsaCloseSafeHandle LsaOpenPolicy(ref LSA_OBJECT_ATTRIBUTES object static LsaCloseSafeHandle Method(LSA_UNICODE_STRING name, ref LSA_OBJECT_ATTRIBUTES attributes, uint access) { var status = PInvoke.LsaOpenPolicy(name, attributes, access, out var policyHandle); - var error = PInvoke.LsaNtStatusToWinError(status); - - if ((WIN32_ERROR)error != WIN32_ERROR.ERROR_SUCCESS) + if (status != NTSTATUS.STATUS_SUCCESS) { - throw new Win32Exception((int)error); + var error = PInvoke.LsaNtStatusToWinError(status); + throw new Win32Exception(unchecked((int)error), "Failed to open the policy database."); } return policyHandle; diff --git a/src/UserRights.Cli/CliBuilder.cs b/src/UserRights.Cli/CliBuilder.cs index 080d586..198b30c 100644 --- a/src/UserRights.Cli/CliBuilder.cs +++ b/src/UserRights.Cli/CliBuilder.cs @@ -259,7 +259,7 @@ private Command BuildPrincipalCommand() } }); - // Ensure the revocations do not overlap with revocations or contain duplicates. + // Ensure the revocations do not overlap with grants or contain duplicates. revocationsOption.Validators.Add(result => { var grantsCollection = result.GetValue(grantsOption) ?? []; @@ -469,7 +469,7 @@ private Command BuildPrivilegeCommand() } }); - // Ensure the revocations do not overlap with revocations or contain duplicates. + // Ensure the revocations do not overlap with grants or contain duplicates. revocationsOption.Validators.Add(result => { var grantsCollection = result.GetValue(grantsOption) ?? []; @@ -535,7 +535,8 @@ private Command BuildPrivilegeCommand() } catch (RegexParseException e) { - result.AddError(string.Format(CultureInfo.InvariantCulture, "Revoke pattern must be a valid regular expression. {0}", e.Message)); + var error = string.Create(CultureInfo.InvariantCulture, $"Revoke pattern must be a valid regular expression. {e.Message}"); + result.AddError(error); } } }); From f3dbe42340a9238e506666ed43bab331e2dc2f69 Mon Sep 17 00:00:00 2001 From: "Joseph L. Casale" <9957114+jcasale@users.noreply.github.com> Date: Tue, 4 Nov 2025 04:06:20 -0700 Subject: [PATCH 13/15] Converts tests from xUnit to MSTest. --- .github/workflows/main.yml | 13 +- .github/workflows/unit-tests.yml | 97 ++++++ .gitignore | 1 + src/Directory.Packages.props | 6 - src/Tests.Application/.editorconfig | 9 +- .../AdminOnlyFactAttribute.cs | 25 -- .../LsaUserRightsConnectTests.cs | 8 +- .../LsaUserRightsDisposeTests.cs | 19 +- .../LsaUserRightsGetPrincipalsTests.cs | 38 ++- .../LsaUserRightsGrantPrivilegeTests.cs | 28 +- .../LsaUserRightsRevokePrivilegeTests.cs | 29 +- src/Tests.Application/MSTestSettings.cs | 1 + .../Tests.Application.csproj | 26 +- .../UserRightsManagerListTests.cs | 125 +------- .../UserRightsManagerPrincipalTests.cs | 213 +++++++------ .../UserRightsManagerPrivilegeTests.cs | 296 ++++++++++-------- .../UserRightsManagerTestBase.cs | 87 ----- src/Tests.Cli/CliTestBase.cs | 80 ----- src/Tests.Cli/ListCommandTests.cs | 85 +++-- src/Tests.Cli/ListSyntaxTests.cs | 74 +++-- src/Tests.Cli/MSTestSettings.cs | 1 + src/Tests.Cli/PrincipalCommandTests.cs | 185 +++++------ src/Tests.Cli/PrincipalSyntaxTests.cs | 164 +++++----- src/Tests.Cli/PrivilegeCommandTests.cs | 285 +++++++---------- src/Tests.Cli/PrivilegeSyntaxTests.cs | 231 ++++++++------ src/Tests.Cli/Tests.Cli.csproj | 21 +- src/Tests/.editorconfig | 2 + src/Tests/CliBuilderFixture.cs | 126 ++++++++ src/Tests/IUserRightsSerializable.cs | 12 - .../LsaUserRightsSnapshotFixture.cs} | 50 ++- src/Tests/MockLsaUserRights.cs | 43 +-- src/Tests/MockUserRightsManager.cs | 2 +- src/Tests/RunWhenElevatedAttribute.cs | 32 ++ src/Tests/TestData.cs | 4 +- src/Tests/Tests.csproj | 12 +- src/Tests/UserRightEntryEqualityComparer.cs | 56 ---- src/Tests/UserRightsManagerFixture.cs | 66 ++++ src/UserRights.Application/UserRightEntry.cs | 21 ++ src/global.json | 6 + src/testconfig.json | 22 ++ 40 files changed, 1294 insertions(+), 1307 deletions(-) create mode 100644 .github/workflows/unit-tests.yml delete mode 100644 src/Tests.Application/AdminOnlyFactAttribute.cs create mode 100644 src/Tests.Application/MSTestSettings.cs delete mode 100644 src/Tests.Application/UserRightsManagerTestBase.cs delete mode 100644 src/Tests.Cli/CliTestBase.cs create mode 100644 src/Tests.Cli/MSTestSettings.cs create mode 100644 src/Tests/.editorconfig create mode 100644 src/Tests/CliBuilderFixture.cs delete mode 100644 src/Tests/IUserRightsSerializable.cs rename src/{Tests.Application/LsaUserRightsTestBase.cs => Tests/LsaUserRightsSnapshotFixture.cs} (88%) create mode 100644 src/Tests/RunWhenElevatedAttribute.cs delete mode 100644 src/Tests/UserRightEntryEqualityComparer.cs create mode 100644 src/Tests/UserRightsManagerFixture.cs create mode 100644 src/testconfig.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 611bc6d..636eef3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,11 +37,18 @@ jobs: "label=$label" >> $env:GITHUB_OUTPUT - - name: Run tests - run: dotnet.exe test .\src\UserRights.slnx --configuration Release --runtime win-x64 + - name: Run CLI tests + run: | + cd .\src + dotnet.exe test --project .\Tests.Cli\Tests.Cli.csproj --configuration Debug --runtime win-x64 --output Detailed + + - name: Run Application tests + run: | + cd .\src + dotnet.exe test --project .\Tests.Application\Tests.Application.csproj --configuration Debug --runtime win-x64 --output Detailed --max-parallel-test-modules 1 - name: Clean solution - run: dotnet.exe clean .\src\UserRights.slnx --configuration Release + run: dotnet.exe clean .\src\UserRights.slnx --configuration Debug - name: Publish runtime-dependent release run: dotnet.exe publish .\src\UserRights\UserRights.csproj --configuration Release --runtime win-x64 --no-self-contained --output .\publish diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..c17f6cb --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,97 @@ +name: Unit Tests +on: + push: + branches-ignore: [master] + pull_request: + branches: [master] + workflow_dispatch: + +env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_GENERATE_ASPNET_CERTIFICATE: false + DOTNET_NOLOGO: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + +jobs: + unit-tests: + runs-on: windows-latest + permissions: + pull-requests: write + contents: read + + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Setup dotnet + id: setup-dotnet + uses: actions/setup-dotnet@v5 + with: + global-json-file: src/global.json + + - name: Setup report generator + run: dotnet.exe tool install -g dotnet-reportgenerator-globaltool + + - name: Run CLI tests + run: | + cd src + dotnet.exe test ` + --project .\Tests.Cli\Tests.Cli.csproj ` + --configuration Debug ` + --runtime win-x64 ` + --output Detailed ` + --diagnostic ` + --diagnostic-output-directory diags ` + --report-trx ` + --coverage ` + --coverage-settings .\testconfig.json + + - name: Run Application tests + run: | + cd src + dotnet.exe test ` + --project .\Tests.Application\Tests.Application.csproj ` + --configuration Debug ` + --runtime win-x64 ` + --output Detailed ` + --diagnostic ` + --diagnostic-output-directory diags ` + --report-trx ` + --coverage ` + --coverage-settings .\testconfig.json ` + --max-parallel-test-modules 1 + + - name: Run report generator + if: always() + run: | + reportgenerator.exe ` + -reports:**/*.cobertura.xml ` + -targetdir:${{ github.workspace }}/coveragereport ` + '-reporttypes:Cobertura;HtmlInline;MarkdownSummary' ` + '-title:Code Coverage' ` + -tag:${{ github.run_number }}_${{ github.run_id }} + + - name: Upload test result artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results + path: | + ${{ github.workspace }}/**/diags/**/* + ${{ github.workspace }}/**/TestResults/**/* + if-no-files-found: error + + - name: Upload coverage report artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage-report + path: ${{ github.workspace }}/**/coveragereport/**/* + if-no-files-found: error + + - name: Add coverage pr comment + uses: marocchino/sticky-pull-request-comment@v2 + if: github.event_name == 'pull_request' + with: + path: ${{ github.workspace }}/coveragereport/Summary.md diff --git a/.gitignore b/.gitignore index 5a65f81..7c9526f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea/ .vs/ +src/TestResults/** **/bin/ **/obj/ *.user \ No newline at end of file diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 1bc4e3a..a8209bf 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -3,7 +3,6 @@ true - @@ -11,7 +10,6 @@ - @@ -23,9 +21,5 @@ - - - - \ No newline at end of file diff --git a/src/Tests.Application/.editorconfig b/src/Tests.Application/.editorconfig index c36ad37..250fa43 100644 --- a/src/Tests.Application/.editorconfig +++ b/src/Tests.Application/.editorconfig @@ -1,2 +1,9 @@ -[UserRightsManagerPrivilegeTests.cs] +[LsaUserRightsDisposeTests.cs] +dotnet_diagnostic.CA1031.severity = none # Do not catch general exception types. + +[UserRightsManagerPrincipalTests.cs] +dotnet_diagnostic.SA1515.severity = none # Single-line comment should be preceded by blank line. + +[UserRightsManagerPrivilegeTests.cs] +dotnet_diagnostic.SA1515.severity = none # Single-line comment should be preceded by blank line. dotnet_diagnostic.MA0110.severity = none # Use the Regex source generator. \ No newline at end of file diff --git a/src/Tests.Application/AdminOnlyFactAttribute.cs b/src/Tests.Application/AdminOnlyFactAttribute.cs deleted file mode 100644 index fc1924c..0000000 --- a/src/Tests.Application/AdminOnlyFactAttribute.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Tests.Application; - -using Xunit; - -/// -/// Represents a test fact that signals the runner to skip the test for non administrative users. -/// -public sealed class AdminOnlyFactAttribute : FactAttribute -{ - /// - /// Initializes a new instance of the class. - /// - public AdminOnlyFactAttribute() - { - using var identity = System.Security.Principal.WindowsIdentity.GetCurrent(); - var principal = new System.Security.Principal.WindowsPrincipal(identity); - - if (principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator)) - { - return; - } - - Skip = "Current user is not an administrator."; - } -} \ No newline at end of file diff --git a/src/Tests.Application/LsaUserRightsConnectTests.cs b/src/Tests.Application/LsaUserRightsConnectTests.cs index 68a4ad3..73a1411 100644 --- a/src/Tests.Application/LsaUserRightsConnectTests.cs +++ b/src/Tests.Application/LsaUserRightsConnectTests.cs @@ -1,23 +1,25 @@ namespace Tests.Application; using UserRights.Application; -using Xunit; /// /// Represents tests for connection functionality. /// -[Collection("lsa")] +[TestClass] public class LsaUserRightsConnectTests { /// /// Tests that only a single connection to the local security authority is allowed. /// - [AdminOnlyFact] + [TestMethod] + [RunWhenElevated] public void MultipleConnectionsThrowsException() { + // Arrange. using var policy = new LsaUserRights(); policy.Connect(); + // Act & Assert. Assert.Throws(() => policy.Connect()); } } \ No newline at end of file diff --git a/src/Tests.Application/LsaUserRightsDisposeTests.cs b/src/Tests.Application/LsaUserRightsDisposeTests.cs index 818ebc6..cd2a8db 100644 --- a/src/Tests.Application/LsaUserRightsDisposeTests.cs +++ b/src/Tests.Application/LsaUserRightsDisposeTests.cs @@ -1,26 +1,33 @@ namespace Tests.Application; using UserRights.Application; -using Xunit; /// /// Represents tests for disposal functionality. /// -[Collection("lsa")] +[TestClass] public class LsaUserRightsDisposeTests { /// /// Tests whether dispose can be successfully called multiple times. /// - [Fact] + [TestMethod] + [RunWhenElevated] public void CanBeDisposedMultipleTimes() { + // Arrange. var policy = new LsaUserRights(); policy.Dispose(); - var exception = Record.Exception(policy.Dispose); - - Assert.Null(exception); + // Act & Assert. + try + { + policy.Dispose(); + } + catch (Exception e) + { + Assert.Fail($"Multiple calls to Dispose() should not fail: {e}"); + } } } \ No newline at end of file diff --git a/src/Tests.Application/LsaUserRightsGetPrincipalsTests.cs b/src/Tests.Application/LsaUserRightsGetPrincipalsTests.cs index c837472..05ef7a9 100644 --- a/src/Tests.Application/LsaUserRightsGetPrincipalsTests.cs +++ b/src/Tests.Application/LsaUserRightsGetPrincipalsTests.cs @@ -3,7 +3,6 @@ namespace Tests.Application; using System.Security.Principal; using UserRights.Application; -using Xunit; using static Tests.PrivilegeConstants; using static Tests.SecurityIdentifierConstants; @@ -11,15 +10,18 @@ namespace Tests.Application; /// /// Represents tests for list functionality. /// -[Collection("lsa")] -public sealed class LsaUserRightsGetPrincipalsTests : LsaUserRightsTestBase +[TestClass] +[DoNotParallelize] +public sealed class LsaUserRightsGetPrincipalsTests : LsaUserRightsSnapshotFixture { /// /// Tests listing all the principals assigned to all privileges. /// - [AdminOnlyFact] + [TestMethod] + [RunWhenElevated] public void GetPrincipalsShouldWork() { + // Arrange. var expected = InitialState.Values .SelectMany(p => p) .Distinct() @@ -29,51 +31,63 @@ public void GetPrincipalsShouldWork() using var policy = new LsaUserRights(); policy.Connect(); + // Act. var actual = policy.LsaEnumerateAccountsWithUserRight() .Order() .ToArray(); - Assert.Equal(expected, actual); + // Assert. + CollectionAssert.AreEqual(expected, actual); } /// /// Tests listing the principals assigned to a single privilege. /// /// - /// We assume the BUILTIN\Administrators group is granted the SeTakeOwnershipPrivilege privilege. + /// The test verifies that the BUILTIN\Administrators group is assigned the SeTakeOwnershipPrivilege user right. /// - [AdminOnlyFact] + [TestMethod] + [RunWhenElevated] public void GetPrincipalsSinglePrivilegeShouldWork() { + // Arrange. var securityIdentifier = new SecurityIdentifier(Administrators); using var policy = new LsaUserRights(); policy.Connect(); - var collection = policy.LsaEnumerateAccountsWithUserRight(SeTakeOwnershipPrivilege).ToArray(); + // Act. + var collection = policy.LsaEnumerateAccountsWithUserRight(SeTakeOwnershipPrivilege); + // Assert. Assert.Contains(securityIdentifier, collection); } /// /// Tests listing all the principals assigned to all privileges without connecting throws an exception. /// - [AdminOnlyFact] + [TestMethod] + [RunWhenElevated] public void GetPrincipalsWithoutConnectingThrowsException() { + // Arrange. using var policy = new LsaUserRights(); - Assert.Throws(() => policy.LsaEnumerateAccountsWithUserRight().ToArray()); + // Act & Assert. + Assert.Throws(() => policy.LsaEnumerateAccountsWithUserRight()); } /// /// Tests listing the principals assigned to a single privilege without connecting throws an exception. /// - [AdminOnlyFact] + [TestMethod] + [RunWhenElevated] public void GetPrincipalsSinglePrivilegeWithoutConnectingThrowsException() { + // Arrange. using var policy = new LsaUserRights(); - Assert.Throws(() => policy.LsaEnumerateAccountsWithUserRight(SeTakeOwnershipPrivilege).ToArray()); + // Act & Assert. + Assert.Throws(() => policy.LsaEnumerateAccountsWithUserRight(SeTakeOwnershipPrivilege)); } } \ No newline at end of file diff --git a/src/Tests.Application/LsaUserRightsGrantPrivilegeTests.cs b/src/Tests.Application/LsaUserRightsGrantPrivilegeTests.cs index 63ada34..efb678b 100644 --- a/src/Tests.Application/LsaUserRightsGrantPrivilegeTests.cs +++ b/src/Tests.Application/LsaUserRightsGrantPrivilegeTests.cs @@ -3,7 +3,6 @@ namespace Tests.Application; using System.Security.Principal; using UserRights.Application; -using Xunit; using static Tests.PrivilegeConstants; using static Tests.SecurityIdentifierConstants; @@ -11,25 +10,27 @@ namespace Tests.Application; /// /// Represents tests for grant functionality. /// -[Collection("lsa")] -public sealed class LsaUserRightsGrantPrivilegeTests : LsaUserRightsTestBase +[TestClass] +[DoNotParallelize] +public sealed class LsaUserRightsGrantPrivilegeTests : LsaUserRightsSnapshotFixture { /// /// Tests granting a privilege. /// /// - /// We assume the BUILTIN\Users group is not granted the SeMachineAccountPrivilege privilege. + /// The test verifies that the BUILTIN\Users group is not assigned the SeTakeOwnershipPrivilege user right. /// - [AdminOnlyFact] + [TestMethod] + [RunWhenElevated] public void GrantPrivilegeShouldWork() { + // Arrange. var securityIdentifier = new SecurityIdentifier(Users); - if (InitialState.TryGetValue(SeMachineAccountPrivilege, out var initial)) - { - Assert.DoesNotContain(securityIdentifier, initial); - } + InitialState.TryGetValue(SeMachineAccountPrivilege, out var initial); + Assert.DoesNotContain(securityIdentifier, initial ?? []); + // Act. using var policy = new LsaUserRights(); policy.Connect(); policy.LsaAddAccountRights(securityIdentifier, SeMachineAccountPrivilege); @@ -38,20 +39,25 @@ public void GrantPrivilegeShouldWork() current.TryGetValue(SeMachineAccountPrivilege, out var collection); - Assert.NotNull(collection); + // Assert. + Assert.IsNotNull(collection); Assert.Contains(securityIdentifier, collection); } /// /// Tests granting a privilege without connecting throws an exception. /// - [AdminOnlyFact] + [TestMethod] + [RunWhenElevated] public void GrantPrivilegeWithoutConnectingThrowsException() { + // Arrange. var securityIdentifier = new SecurityIdentifier(Users); + // Act. using var policy = new LsaUserRights(); + // Assert. Assert.Throws(() => policy.LsaAddAccountRights(securityIdentifier, SeMachineAccountPrivilege)); } } \ No newline at end of file diff --git a/src/Tests.Application/LsaUserRightsRevokePrivilegeTests.cs b/src/Tests.Application/LsaUserRightsRevokePrivilegeTests.cs index 1984c28..dc91360 100644 --- a/src/Tests.Application/LsaUserRightsRevokePrivilegeTests.cs +++ b/src/Tests.Application/LsaUserRightsRevokePrivilegeTests.cs @@ -3,7 +3,6 @@ namespace Tests.Application; using System.Security.Principal; using UserRights.Application; -using Xunit; using static Tests.PrivilegeConstants; using static Tests.SecurityIdentifierConstants; @@ -11,47 +10,55 @@ namespace Tests.Application; /// /// Represents tests for revoke functionality. /// -[Collection("lsa")] -public sealed class LsaUserRightsRevokePrivilegeTests : LsaUserRightsTestBase +[TestClass] +[DoNotParallelize] +public sealed class LsaUserRightsRevokePrivilegeTests : LsaUserRightsSnapshotFixture { /// /// Tests revoking a privilege. /// /// - /// We assume the BUILTIN\Backup Operators is granted the SeBackupPrivilege privilege. + /// The test requires that the BUILTIN\Backup Operators group is assigned the SeBackupPrivilege user right. /// - [AdminOnlyFact] + [TestMethod] + [RunWhenElevated] public void RevokePrivilegeShouldWork() { + // Arrange. var securityIdentifier = new SecurityIdentifier(BackupOperators); InitialState.TryGetValue(SeBackupPrivilege, out var initial); - Assert.NotNull(initial); + Assert.IsNotNull(initial); Assert.Contains(securityIdentifier, initial); + // Act. using var policy = new LsaUserRights(); policy.Connect(); policy.LsaRemoveAccountRights(securityIdentifier, SeBackupPrivilege); var current = GetCurrentState(); - if (current.TryGetValue(SeBackupPrivilege, out var collection)) - { - Assert.DoesNotContain(securityIdentifier, collection); - } + current.TryGetValue(SeBackupPrivilege, out var collection); + + // Assert. + Assert.DoesNotContain(securityIdentifier, collection ?? []); } /// /// Tests revoking a privilege without connecting throws an exception. /// - [AdminOnlyFact] + [TestMethod] + [RunWhenElevated] public void RevokePrivilegeWithoutConnectingThrowsException() { + // Arrange. var securityIdentifier = new SecurityIdentifier(BackupOperators); + // Act. using var policy = new LsaUserRights(); + // Assert. Assert.Throws(() => policy.LsaRemoveAccountRights(securityIdentifier, SeBackupPrivilege)); } } \ No newline at end of file diff --git a/src/Tests.Application/MSTestSettings.cs b/src/Tests.Application/MSTestSettings.cs new file mode 100644 index 0000000..a41798c --- /dev/null +++ b/src/Tests.Application/MSTestSettings.cs @@ -0,0 +1 @@ +[assembly: DoNotParallelize] \ No newline at end of file diff --git a/src/Tests.Application/Tests.Application.csproj b/src/Tests.Application/Tests.Application.csproj index a291750..960b8a5 100644 --- a/src/Tests.Application/Tests.Application.csproj +++ b/src/Tests.Application/Tests.Application.csproj @@ -1,38 +1,18 @@ - + + Exe net10.0-windows enable enable latest latest-All - + All false - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - \ No newline at end of file diff --git a/src/Tests.Application/UserRightsManagerListTests.cs b/src/Tests.Application/UserRightsManagerListTests.cs index c2bf744..3dd57b4 100644 --- a/src/Tests.Application/UserRightsManagerListTests.cs +++ b/src/Tests.Application/UserRightsManagerListTests.cs @@ -1,43 +1,25 @@ namespace Tests.Application; -using System.Globalization; using System.Security.Principal; -using System.Text; -using System.Text.Json; -using CsvHelper; -using CsvHelper.Configuration; -using Microsoft.Extensions.DependencyInjection; using UserRights.Application; using UserRights.Extensions.Security; -using UserRights.Extensions.Serialization; -using Xunit; using static Tests.TestData; /// /// Represents tests for list functionality. /// -public sealed class UserRightsManagerListTests : UserRightsManagerTestBase +[TestClass] +public class UserRightsManagerListTests { /// - /// Verifies invalid arguments throw an instance of . + /// Verifies listing user rights. /// - [Fact] - public void InvalidArgumentsThrowsException() - { - var manager = ServiceProvider.GetRequiredService(); - - Assert.ThrowsAny(() => manager.GetUserRights(null!)); - } - - /// - /// Verifies listing user rights and serializing the output to a CSV. - /// - /// A task that represents the asynchronous write operation. - [Fact] - public async Task SerializingToCsvShouldWork() + [TestMethod] + public void SerializingToCsvShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -58,100 +40,21 @@ public async Task SerializingToCsvShouldWork() var expected = database .SelectMany(kvp => kvp.Value.Select(p => new UserRightEntry(kvp.Key, p.Value, p.ToAccount().Value))) - .OrderBy(p => p.Privilege, StringComparer.OrdinalIgnoreCase) - .ThenBy(p => p.SecurityId, StringComparer.OrdinalIgnoreCase) .ToArray(); var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal([PrincipalSid1, PrincipalSid2], policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal([Privilege1, Privilege2], policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal([Privilege1, Privilege2], policy.LsaEnumerateAccountRights(PrincipalSid2)); - - var manager = ServiceProvider.GetRequiredService(); - var userRights = manager.GetUserRights(policy).ToArray(); - - Assert.Equal(expected, userRights, new UserRightEntryEqualityComparer()); - - using var stream = new MemoryStream(); - await userRights.ToCsv(stream).ConfigureAwait(true); - stream.Position = 0; - using var reader = new StreamReader(stream, Encoding.UTF8); - var serialized = await reader.ReadToEndAsync().ConfigureAwait(true); - - var configuration = new CsvConfiguration(CultureInfo.InvariantCulture) - { - PrepareHeaderForMatch = a => a.Header.ToUpperInvariant() ?? throw new InvalidOperationException() - }; - - using var stringReader = new StringReader(serialized); - using var csvReader = new CsvReader(stringReader, configuration); - - var records = csvReader.GetRecordsAsync(); - var actual = await records - .OrderBy(p => p.Privilege, StringComparer.OrdinalIgnoreCase) - .ThenBy(p => p.SecurityId, StringComparer.OrdinalIgnoreCase) - .ToArrayAsync() - .ConfigureAwait(true); - - Assert.Equal(expected, actual, new UserRightEntryEqualityComparer()); - } - - /// - /// Verifies listing user rights and serializing the output to a JSON. - /// - /// A task that represents the asynchronous write operation. - [Fact] - public async Task SerializingToJsonShouldWork() - { - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var expected = database - .SelectMany(kvp => kvp.Value.Select(p => new UserRightEntry(kvp.Key, p.Value, p.ToAccount().Value))) - .OrderBy(p => p.Privilege, StringComparer.OrdinalIgnoreCase) - .ThenBy(p => p.SecurityId, StringComparer.OrdinalIgnoreCase) - .ToArray(); - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - Assert.Equal([PrincipalSid1, PrincipalSid2], policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - - var manager = ServiceProvider.GetRequiredService(); - var userRights = manager.GetUserRights(policy).ToArray(); - - Assert.Equal(expected, userRights, new UserRightEntryEqualityComparer()); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - using var stream = new MemoryStream(); - await userRights.ToJson(stream).ConfigureAwait(true); - stream.Position = 0; - using var reader = new StreamReader(stream, Encoding.UTF8); - var serialized = await reader.ReadToEndAsync().ConfigureAwait(true); + using var fixture = new UserRightsManagerFixture(); - var actual = JsonSerializer.Deserialize(serialized) - ?.OrderBy(p => p.Privilege, StringComparer.OrdinalIgnoreCase) - .ThenBy(p => p.SecurityId, StringComparer.OrdinalIgnoreCase) - .ToArray() ?? []; + // Act. + var actual = fixture.UserRightsManager.GetUserRights(policy).ToArray(); - Assert.Equal(expected, actual, new UserRightEntryEqualityComparer()); + // Assert. + CollectionAssert.AreEquivalent(expected, actual); } } \ No newline at end of file diff --git a/src/Tests.Application/UserRightsManagerPrincipalTests.cs b/src/Tests.Application/UserRightsManagerPrincipalTests.cs index 5ce9c04..7874fba 100644 --- a/src/Tests.Application/UserRightsManagerPrincipalTests.cs +++ b/src/Tests.Application/UserRightsManagerPrincipalTests.cs @@ -2,70 +2,72 @@ namespace Tests.Application; using System.Security.Principal; -using Microsoft.Extensions.DependencyInjection; using UserRights.Application; -using Xunit; using static Tests.TestData; /// /// Represents tests for modify principal functionality. /// -public sealed class UserRightsManagerPrincipalTests : UserRightsManagerTestBase +[TestClass] +public class UserRightsManagerPrincipalTests { /// - /// Generates invalid method arguments for the method. + /// Gets invalid method arguments for the unit test. /// - /// A sequence of method arguments. - public static TheoryData InvalidArguments() + public static IEnumerable<(ILsaUserRights Policy, string Principal, string[] Grants, string[] Revocations, bool RevokeAll, bool RevokeOthers, bool DryRun)> InvalidArgumentData { - var policy = new MockLsaUserRights( - new Dictionary>(StringComparer.InvariantCultureIgnoreCase) - { - { "joey", new List { PrincipalSid1 } } - }); - - return new() + get { - // Verify null policy instance. - { null!, PrincipalName1, [Privilege1], [], false, false, false }, - - // Verify null or empty principal. - { policy, null!, [Privilege1], [], false, false, false }, - { policy, string.Empty, [Privilege1], [], false, false, false }, - - // Verify null grant collection. - { policy, PrincipalName1, null!, [Privilege1], false, false, false }, - - // Verify null revocation collection. - { policy, PrincipalName1, [Privilege1], null!, false, false, false }, - - // Verify RevokeAll requirements. - { policy, PrincipalName1, [Privilege1], [], true, false, false }, - { policy, PrincipalName1, [], [Privilege1], true, false, false }, - { policy, PrincipalName1, [], [], true, true, false }, - - // Verify RevokeOthers requirements. - { policy, PrincipalName1, [Privilege1], [], true, true, false }, - { policy, PrincipalName1, [], [], false, true, false }, - { policy, PrincipalName1, [Privilege1], [Privilege2], false, true, false }, - - // Verify remaining requirements. - { policy, PrincipalName1, [], [], false, false, false }, - - // Verify grant and revocation set restrictions. - { policy, PrincipalName1, [Privilege1], [Privilege1], false, false, false }, - { policy, PrincipalName1, [Privilege1, Privilege1], [], false, false, false }, - { policy, PrincipalName1, [], [Privilege1, Privilege1], false, false, false } - }; + var policy = new MockLsaUserRights( + new Dictionary>(StringComparer.InvariantCultureIgnoreCase) + { + { "joey", new List { PrincipalSid1 } } + }); + + return + [ + // Verify null policy instance. + new(null!, PrincipalName1, [Privilege1], [], false, false, false), + + // Verify null or empty principal. + new(policy, null!, [Privilege1], [], false, false, false), + new(policy, string.Empty, [Privilege1], [], false, false, false), + + // Verify null grant collection. + new(policy, PrincipalName1, null!, [Privilege1], false, false, false), + + // Verify null revocation collection. + new(policy, PrincipalName1, [Privilege1], null!, false, false, false), + + // Verify RevokeAll requirements. + new(policy, PrincipalName1, [Privilege1], [], true, false, false), + new(policy, PrincipalName1, [], [Privilege1], true, false, false), + new(policy, PrincipalName1, [], [], true, true, false), + + // Verify RevokeOthers requirements. + new(policy, PrincipalName1, [Privilege1], [], true, true, false), + new(policy, PrincipalName1, [], [], false, true, false), + new(policy, PrincipalName1, [Privilege1], [Privilege2], false, true, false), + + // Verify remaining requirements. + new(policy, PrincipalName1, [], [], false, false, false), + + // Verify grant and revocation set restrictions. + new(policy, PrincipalName1, [Privilege1], [Privilege1], false, false, false), + new(policy, PrincipalName1, [Privilege1, Privilege1], [], false, false, false), + new(policy, PrincipalName1, [], [Privilege1, Privilege1], false, false, false) + ]; + } } /// - /// Verifies a granting a privilege to a principal and revoking their other privileges is successful and does not modify other assignments. + /// Verifies granting a privilege to a principal and revoking their other privileges is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void GrantAndRevokeOthersShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1 @@ -85,24 +87,28 @@ public void GrantAndRevokeOthersShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal([PrincipalSid1, PrincipalSid2], policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - var manager = ServiceProvider.GetRequiredService(); - manager.ModifyPrincipal(policy, PrincipalName1, [Privilege2], [], false, true, false); + using var fixture = new UserRightsManagerFixture(); - Assert.Equal([PrincipalSid1, PrincipalSid2], policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + // Act. + fixture.UserRightsManager.ModifyPrincipal(policy, PrincipalName1, [Privilege2], [], false, true, false); + + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); } /// /// Verifies a single grant with a single revoke is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void GrantAndRevokeShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -123,24 +129,28 @@ public void GrantAndRevokeShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + + using var fixture = new UserRightsManagerFixture(); - var manager = ServiceProvider.GetRequiredService(); - manager.ModifyPrincipal(policy, PrincipalName1, [Privilege2], [Privilege1], false, false, false); + // Act. + fixture.UserRightsManager.ModifyPrincipal(policy, PrincipalName1, [Privilege2], [Privilege1], false, false, false); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); } /// /// Verifies a single grant is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void GrantShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -161,16 +171,19 @@ public void GrantShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - var manager = ServiceProvider.GetRequiredService(); - manager.ModifyPrincipal(policy, PrincipalName1, [Privilege2], [], false, false, false); + using var fixture = new UserRightsManagerFixture(); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid1).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + // Act. + fixture.UserRightsManager.ModifyPrincipal(policy, PrincipalName1, [Privilege2], [], false, false, false); + + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); } /// @@ -183,21 +196,24 @@ public void GrantShouldWork() /// Revokes all privileges from the principal. /// Revokes all privileges from the principal excluding those being granted. /// Enables dry-run mode. - [Theory] - [MemberData(nameof(InvalidArguments))] - public void InvalidArgumentsThrowsException(IUserRightsSerializable policy, string principal, string[] grants, string[] revocations, bool revokeAll, bool revokeOthers, bool dryRun) + [TestMethod] + [DynamicData(nameof(InvalidArgumentData))] + public void InvalidArgumentsThrowsException(ILsaUserRights policy, string principal, string[] grants, string[] revocations, bool revokeAll, bool revokeOthers, bool dryRun) { - var manager = ServiceProvider.GetRequiredService(); + // Arrange. + using var fixture = new UserRightsManagerFixture(); - Assert.ThrowsAny(() => manager.ModifyPrincipal(policy, principal, grants, revocations, revokeAll, revokeOthers, dryRun)); + // Act & Assert. + Assert.Throws(() => fixture.UserRightsManager.ModifyPrincipal(policy, principal, grants, revocations, revokeAll, revokeOthers, dryRun)); } /// /// Verifies a revoking all privileges for a principal is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void RevokeAllShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -219,24 +235,28 @@ public void RevokeAllShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid1).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + + using var fixture = new UserRightsManagerFixture(); - var manager = ServiceProvider.GetRequiredService(); - manager.ModifyPrincipal(policy, PrincipalName1, [], [], true, false, false); + // Act. + fixture.UserRightsManager.ModifyPrincipal(policy, PrincipalName1, [], [], true, false, false); - Assert.Empty(policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal([PrincipalSid2], policy.LsaEnumerateAccountsWithUserRight()); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + // Assert. + Assert.IsEmpty(policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); } /// /// Verifies a single revocation is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void RevokeShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -257,15 +277,18 @@ public void RevokeShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + + using var fixture = new UserRightsManagerFixture(); - var manager = ServiceProvider.GetRequiredService(); - manager.ModifyPrincipal(policy, PrincipalName2, [], [Privilege2], false, false, false); + // Act. + fixture.UserRightsManager.ModifyPrincipal(policy, PrincipalName2, [], [Privilege2], false, false, false); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); } } \ No newline at end of file diff --git a/src/Tests.Application/UserRightsManagerPrivilegeTests.cs b/src/Tests.Application/UserRightsManagerPrivilegeTests.cs index 95634ff..9a962a5 100644 --- a/src/Tests.Application/UserRightsManagerPrivilegeTests.cs +++ b/src/Tests.Application/UserRightsManagerPrivilegeTests.cs @@ -3,76 +3,77 @@ namespace Tests.Application; using System.Security.Principal; using System.Text.RegularExpressions; -using Microsoft.Extensions.DependencyInjection; - using UserRights.Application; -using Xunit; - using static Tests.TestData; /// -/// Represents integration tests for modify privilege functionality. +/// Represents tests for modify privilege functionality. /// -public sealed class UserRightsManagerPrivilegeTests : UserRightsManagerTestBase +[TestClass] +public class UserRightsManagerPrivilegeTests { /// - /// Generates invalid method arguments for the method. + /// Gets invalid method arguments for the unit test. /// /// A sequence of method arguments. - public static TheoryData InvalidArguments() + public static IEnumerable<(ILsaUserRights Policy, string Privilege, string[] Grants, string[] Revocations, bool RevokeAll, bool RevokeOthers, string RevokePattern, bool DryRun)> InvalidArgumentData { - var policy = new MockLsaUserRights(); - const string pattern = ".*"; - - return new() + get { - // Verify null policy instance. - { null!, Privilege1, [PrincipalName1], [], false, false, null!, false }, - - // Verify null or empty privilege. - { policy, null!, [PrincipalName1], [], false, false, null!, false }, - { policy, string.Empty, [PrincipalName1], [], false, false, null!, false }, - - // Verify null grant collection. - { policy, Privilege1, null!, [PrincipalName1], false, false, null!, false }, - - // Verify null revocation collection. - { policy, Privilege1, [PrincipalName1], null!, false, false, null!, false }, - - // Verify RevokeAll requirements. - { policy, Privilege1, [PrincipalName1], [], true, false, null!, false }, - { policy, Privilege1, [], [PrincipalName1], true, false, null!, false }, - { policy, Privilege1, [], [], true, true, null!, false }, - { policy, Privilege1, [], [], true, false, pattern, false }, - - // Verify RevokeOthers requirements. - { policy, Privilege1, [], [], false, true, null!, false }, - { policy, Privilege1, [PrincipalName1], [PrincipalName2], false, true, null!, false }, - { policy, Privilege2, [], [], true, true, null!, false }, - { policy, Privilege1, [], [], false, true, pattern, false }, - - // Verify RevokePattern requirements. - { policy, Privilege1, [], [PrincipalName1], false, false, pattern, false }, - { policy, Privilege2, [], [], true, false, pattern, false }, - { policy, Privilege2, [], [], false, true, pattern, false }, - - // Verify remaining requirements. - { policy, Privilege1, [], [], false, false, null!, false }, - - // Verify grant and revocation set restrictions. - { policy, Privilege1, [PrincipalName1], [PrincipalName1], false, false, null!, false }, - { policy, Privilege1, [PrincipalName1, PrincipalName1], [], false, false, null!, false }, - { policy, Privilege1, [], [PrincipalName1, PrincipalName1], false, false, null!, false } - }; + var policy = new MockLsaUserRights(); + const string pattern = ".*"; + + return + [ + // Verify null policy instance. + new(null!, Privilege1, [PrincipalName1], [], false, false, null!, new(false)), + + // Verify null or empty privilege. + new(policy, null!, [PrincipalName1], [], false, false, null!, new(false)), + new(policy, string.Empty, [PrincipalName1], [], false, false, null!, new(false)), + + // Verify null grant collection. + new(policy, Privilege1, null!, [PrincipalName1], false, false, null!, new(false)), + + // Verify null revocation collection. + new(policy, Privilege1, [PrincipalName1], null!, false, false, null!, new(false)), + + // Verify RevokeAll requirements. + new(policy, Privilege1, [PrincipalName1], [], true, false, null!, new(false)), + new(policy, Privilege1, [], [PrincipalName1], true, false, null!, new(false)), + new(policy, Privilege1, [], [], true, true, null!, new(false)), + new(policy, Privilege1, [], [], true, false, pattern, new(false)), + + // Verify RevokeOthers requirements. + new(policy, Privilege1, [], [], false, true, null!, new(false)), + new(policy, Privilege1, [PrincipalName1], [PrincipalName2], false, true, null!, new(false)), + new(policy, Privilege2, [], [], true, true, null!, new(false)), + new(policy, Privilege1, [], [], false, true, pattern, new(false)), + + // Verify RevokePattern requirements. + new(policy, Privilege1, [], [PrincipalName1], false, false, pattern, new(false)), + new(policy, Privilege2, [], [], true, false, pattern, new(false)), + new(policy, Privilege2, [], [], false, true, pattern, new(false)), + + // Verify remaining requirements. + new(policy, Privilege1, [], [], false, false, null!, new(false)), + + // Verify grant and revocation set restrictions. + new(policy, Privilege1, [PrincipalName1], [PrincipalName1], false, false, null!, new(false)), + new(policy, Privilege1, [PrincipalName1, PrincipalName1], [], false, false, null!, new(false)), + new(policy, Privilege1, [], [PrincipalName1, PrincipalName1], false, false, null!, new(false)) + ]; + } } /// /// Verifies granting a principal to a privilege and revoking its other principals is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void GrantAndRevokeOthersShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -93,28 +94,32 @@ public void GrantAndRevokeOthersShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight(Privilege1).Order()); - Assert.Equal([PrincipalSid2], policy.LsaEnumerateAccountsWithUserRight(Privilege2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege1)); + CollectionAssert.AreEqual(new[] { PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege2)); + + using var fixture = new UserRightsManagerFixture(); - var manager = ServiceProvider.GetRequiredService(); - manager.ModifyPrivilege(policy, Privilege2, [PrincipalName1], [], false, true, null!, false); + // Act. + fixture.UserRightsManager.ModifyPrivilege(policy, Privilege2, [PrincipalName1], [], false, true, null!, false); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid1).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal([Privilege1], policy.LsaEnumerateAccountRights(PrincipalSid2)); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight(Privilege1).Order()); - Assert.Equal([PrincipalSid1], policy.LsaEnumerateAccountsWithUserRight(Privilege2)); + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege1)); + CollectionAssert.AreEqual(new[] { PrincipalSid1 }, policy.LsaEnumerateAccountsWithUserRight(Privilege2)); } /// /// Verifies a single grant with a single revoke is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void GrantAndRevokeShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1 @@ -135,24 +140,28 @@ public void GrantAndRevokeShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid1).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + + using var fixture = new UserRightsManagerFixture(); - var manager = ServiceProvider.GetRequiredService(); - manager.ModifyPrivilege(policy, Privilege1, [PrincipalName2], [PrincipalName1], false, false, null!, false); + // Act. + fixture.UserRightsManager.ModifyPrivilege(policy, Privilege1, [PrincipalName2], [PrincipalName1], false, false, null!, false); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); } /// /// Verifies granting a principal to a privilege and revoking all principals matching a pattern is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void GrantAndRevokePatternShouldWork() { + // Arrange. var principals1 = new List { PrincipalSidCurrent, @@ -176,28 +185,32 @@ public void GrantAndRevokePatternShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSidCurrent, PrincipalSid1, PrincipalSid2, PrincipalSid3 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSidCurrent)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid3).Order(StringComparer.OrdinalIgnoreCase)); + CollectionAssert.AreEquivalent(new[] { PrincipalSidCurrent, PrincipalSid1, PrincipalSid2, PrincipalSid3 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSidCurrent)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid3)); - var manager = ServiceProvider.GetRequiredService(); + using var fixture = new UserRightsManagerFixture(); var pattern = new Regex("^S-1-5-21", RegexOptions.None, TimeSpan.FromSeconds(1)); - manager.ModifyPrivilege(policy, Privilege1, [PrincipalName1], [], false, false, pattern, false); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid1).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal([Privilege1, Privilege2], policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal([Privilege1, Privilege2], policy.LsaEnumerateAccountRights(PrincipalSid3).Order(StringComparer.OrdinalIgnoreCase)); + // Act. + fixture.UserRightsManager.ModifyPrivilege(policy, Privilege1, [PrincipalName1], [], false, false, pattern, false); + + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid3)); } /// /// Verifies a single grant is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void GrantShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1 @@ -217,16 +230,19 @@ public void GrantShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + + using var fixture = new UserRightsManagerFixture(); - var manager = ServiceProvider.GetRequiredService(); - manager.ModifyPrivilege(policy, Privilege2, [PrincipalName1], [], false, false, null!, false); + // Act. + fixture.UserRightsManager.ModifyPrivilege(policy, Privilege2, [PrincipalName1], [], false, false, null!, false); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid1).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); } /// @@ -240,22 +256,25 @@ public void GrantShouldWork() /// Revokes all principals from the privilege excluding those being granted. /// Revokes all principals whose SID matches the regular expression excluding those being granted. /// Enables dry-run mode. - [Theory] - [MemberData(nameof(InvalidArguments))] - public void InvalidArgumentsThrowsException(IUserRightsSerializable policy, string privilege, string[] grants, string[] revocations, bool revokeAll, bool revokeOthers, string revokePattern, bool dryRun) + [TestMethod] + [DynamicData(nameof(InvalidArgumentData))] + public void InvalidArgumentsThrowsException(ILsaUserRights policy, string privilege, string[] grants, string[] revocations, bool revokeAll, bool revokeOthers, string revokePattern, bool dryRun) { - var manager = ServiceProvider.GetRequiredService(); + // Arrange. + using var fixture = new UserRightsManagerFixture(); var regex = string.IsNullOrWhiteSpace(revokePattern) ? null : new Regex(revokePattern, RegexOptions.None, TimeSpan.FromSeconds(1)); - Assert.ThrowsAny(() => manager.ModifyPrivilege(policy, privilege, grants, revocations, revokeAll, revokeOthers, regex, dryRun)); + // Act & Assert. + Assert.Throws(() => fixture.UserRightsManager.ModifyPrivilege(policy, privilege, grants, revocations, revokeAll, revokeOthers, regex, dryRun)); } /// /// Verifies revoking all principals for a privilege is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void RevokeAllShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -277,28 +296,32 @@ public void RevokeAllShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid1).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight(Privilege1).Order()); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight(Privilege2).Order()); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege1)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege2)); + + using var fixture = new UserRightsManagerFixture(); - var manager = ServiceProvider.GetRequiredService(); - manager.ModifyPrivilege(policy, Privilege1, [], [], true, false, null!, false); + // Act. + fixture.UserRightsManager.ModifyPrivilege(policy, Privilege1, [], [], true, false, null!, false); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - Assert.Empty(policy.LsaEnumerateAccountsWithUserRight(Privilege1)); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight(Privilege2).Order()); + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + Assert.IsEmpty(policy.LsaEnumerateAccountsWithUserRight(Privilege1)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege2)); } /// /// Verifies a single revocation is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void RevokeShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -319,24 +342,28 @@ public void RevokeShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - var manager = ServiceProvider.GetRequiredService(); - manager.ModifyPrivilege(policy, Privilege1, [], [PrincipalName2], false, false, null!, false); + using var fixture = new UserRightsManagerFixture(); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + // Act. + fixture.UserRightsManager.ModifyPrivilege(policy, Privilege1, [], [PrincipalName2], false, false, null!, false); + + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); } /// - /// Verifies revoking all non builtin and virtual principals from a privilege is successful. + /// Verifies revoking all non-builtin and virtual principals from a privilege is successful. /// - [Fact] + [TestMethod] public void RevokePatternForAllButBuiltinAndVirtualShouldWork() { + // Arrange. var principals1 = new List { PrincipalSidCurrent, @@ -360,19 +387,22 @@ public void RevokePatternForAllButBuiltinAndVirtualShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSidCurrent, PrincipalSid1, PrincipalSid2, PrincipalSid3 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal([Privilege1], policy.LsaEnumerateAccountRights(PrincipalSidCurrent)); - Assert.Equal([Privilege2], policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid3).Order(StringComparer.OrdinalIgnoreCase)); + CollectionAssert.AreEquivalent(new[] { PrincipalSidCurrent, PrincipalSid1, PrincipalSid2, PrincipalSid3 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSidCurrent)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid3)); - var manager = ServiceProvider.GetRequiredService(); + using var fixture = new UserRightsManagerFixture(); var pattern = new Regex("^S-1-5-21", RegexOptions.None, TimeSpan.FromSeconds(1)); - manager.ModifyPrivilege(policy, Privilege1, [], [], false, false, pattern, false); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal([Privilege2], policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal([Privilege1, Privilege2], policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal([Privilege1, Privilege2], policy.LsaEnumerateAccountRights(PrincipalSid3).Order(StringComparer.OrdinalIgnoreCase)); + // Act. + fixture.UserRightsManager.ModifyPrivilege(policy, Privilege1, [], [], false, false, pattern, false); + + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid3)); } } \ No newline at end of file diff --git a/src/Tests.Application/UserRightsManagerTestBase.cs b/src/Tests.Application/UserRightsManagerTestBase.cs deleted file mode 100644 index 9b0fe1d..0000000 --- a/src/Tests.Application/UserRightsManagerTestBase.cs +++ /dev/null @@ -1,87 +0,0 @@ -namespace Tests.Application; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using UserRights.Application; - -/// -/// Represents the test base for application. -/// -public abstract class UserRightsManagerTestBase : IDisposable -{ - private readonly IServiceCollection _serviceCollection; - private readonly Lazy _serviceProvider; - - private bool _disposed; - - /// - /// Initializes a new instance of the class. - /// - protected UserRightsManagerTestBase() - { - _serviceCollection = new ServiceCollection() - .AddSingleton() - .AddLogging(builder => builder - .ClearProviders() - .SetMinimumLevel(LogLevel.Trace) - .AddDebug()); - - // Defer the creation until the instance is accessed to allow inheritors to modify the service collection. - _serviceProvider = new(_serviceCollection.BuildServiceProvider); - } - - /// - /// Gets the service collection. - /// - protected IServiceCollection ServiceCollection - { - get - { - ObjectDisposedException.ThrowIf(_disposed, this); - - return _serviceCollection; - } - } - - /// - /// Gets the service provider. - /// - protected ServiceProvider ServiceProvider - { - get - { - ObjectDisposedException.ThrowIf(_disposed, this); - - return _serviceProvider.Value; - } - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases resources when they are no longer required. - /// - /// A value indicating whether the method call comes from a dispose method (its value is ) or from a finalizer (its value is ). - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - if (_serviceProvider.IsValueCreated) - { - _serviceProvider.Value.Dispose(); - } - - _disposed = true; - } - } -} \ No newline at end of file diff --git a/src/Tests.Cli/CliTestBase.cs b/src/Tests.Cli/CliTestBase.cs deleted file mode 100644 index 7233f07..0000000 --- a/src/Tests.Cli/CliTestBase.cs +++ /dev/null @@ -1,80 +0,0 @@ -namespace Tests.Cli; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -/// -/// Represents a test configuration that provides command line infrastructure. -/// -public abstract class CliTestBase : IDisposable -{ - private readonly IServiceCollection _serviceCollection; - private readonly Lazy _serviceProvider; - - private bool _disposed; - - /// - /// Initializes a new instance of the class. - /// - protected CliTestBase() - { - _serviceCollection = new ServiceCollection() - .AddLogging(builder => builder - .ClearProviders() - .SetMinimumLevel(LogLevel.Trace) - .AddDebug()); - - _serviceProvider = new(_serviceCollection.BuildServiceProvider); - } - - /// - /// Gets the service collection. - /// - protected IServiceCollection ServiceCollection - { - get - { - ObjectDisposedException.ThrowIf(_disposed, this); - - return _serviceCollection; - } - } - - /// - /// Gets the service provider. - /// - protected ServiceProvider ServiceProvider - { - get - { - ObjectDisposedException.ThrowIf(_disposed, this); - - return _serviceProvider.Value; - } - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases resources when they are no longer required. - /// - /// A value indicating whether the method call comes from a dispose method (its value is ) or from a finalizer (its value is ). - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - _serviceProvider.Value.Dispose(); - _disposed = true; - } - } -} \ No newline at end of file diff --git a/src/Tests.Cli/ListCommandTests.cs b/src/Tests.Cli/ListCommandTests.cs index bece5d0..400f45c 100644 --- a/src/Tests.Cli/ListCommandTests.cs +++ b/src/Tests.Cli/ListCommandTests.cs @@ -6,26 +6,32 @@ namespace Tests.Cli; using CsvHelper; using CsvHelper.Configuration; -using Microsoft.Extensions.DependencyInjection; + using UserRights.Application; using UserRights.Cli; using UserRights.Extensions.Security; -using Xunit; using static Tests.TestData; /// /// Represents integration tests for list functionality. /// -public sealed class ListCommandTests : CliTestBase +[TestClass] +public class ListCommandTests { + /// + /// Gets or sets the unit test context. + /// + public required TestContext TestContext { get; set; } + /// /// Verifies listing user rights to a JSON file. /// /// A task representing the asynchronous operation. - [Fact] + [TestMethod] public async Task PathAndJsonShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -52,53 +58,48 @@ public async Task PathAndJsonShouldWork() var policy = new MockLsaUserRights(database); - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); + using var fixture = new CliBuilderFixture(policy); - var builder = ServiceProvider.GetRequiredService(); - - var rootCommand = builder.Build(); + var rootCommand = fixture.CliBuilder.Build(); var file = Path.GetTempFileName(); - var args = new[] - { - "list", - "--json", - "--path", - file - }; + var args = new[] { "list", "--json", "--path", file }; + // Act. int rc; UserRightEntry[] actual; try { - rc = await rootCommand.Parse(args).ThrowIfInvalid().RunAsync(); + rc = await rootCommand.Parse(args).ThrowIfInvalid().RunAsync(TestContext.CancellationToken).ConfigureAwait(false); - await using var stream = File.OpenRead(file); - - var results = await JsonSerializer.DeserializeAsync(stream); - actual = results - ?.OrderBy(p => p.Privilege, StringComparer.OrdinalIgnoreCase) - .ThenBy(p => p.SecurityId, StringComparer.OrdinalIgnoreCase) - .ToArray() ?? []; + var stream = File.OpenRead(file); + await using (stream.ConfigureAwait(false)) + { + var results = await JsonSerializer.DeserializeAsync(stream, cancellationToken: TestContext.CancellationToken).ConfigureAwait(false); + actual = results + ?.OrderBy(p => p.Privilege, StringComparer.OrdinalIgnoreCase) + .ThenBy(p => p.SecurityId, StringComparer.OrdinalIgnoreCase) + .ToArray() ?? []; + } } finally { File.Delete(file); } - Assert.Equal(0, rc); - Assert.Equal(expected, actual, new UserRightEntryEqualityComparer()); + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEqual(expected, actual); } /// /// Verifies listing user rights to a CSV file. /// /// A task representing the asynchronous operation. - [Fact] + [TestMethod] public async Task PathShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -124,27 +125,19 @@ public async Task PathShouldWork() .ThenBy(p => p.SecurityId, StringComparer.OrdinalIgnoreCase) .ToArray(); - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); + using var fixture = new CliBuilderFixture(policy); - var builder = ServiceProvider.GetRequiredService(); - - var rootCommand = builder.Build(); + var rootCommand = fixture.CliBuilder.Build(); var file = Path.GetTempFileName(); - var args = new[] - { - "list", - "--path", - file - }; + var args = new[] { "list", "--path", file }; + // Act. int rc; UserRightEntry[] actual; try { - rc = await rootCommand.Parse(args).ThrowIfInvalid().RunAsync(); + rc = await rootCommand.Parse(args).ThrowIfInvalid().RunAsync(TestContext.CancellationToken).ConfigureAwait(false); var csvConfiguration = new CsvConfiguration(CultureInfo.InvariantCulture) { @@ -154,17 +147,19 @@ public async Task PathShouldWork() using var streamReader = new StreamReader(file); using var csvReader = new CsvReader(streamReader, csvConfiguration); - actual = await csvReader.GetRecordsAsync() + actual = await csvReader.GetRecordsAsync(TestContext.CancellationToken) .OrderBy(p => p.Privilege, StringComparer.OrdinalIgnoreCase) .ThenBy(p => p.SecurityId, StringComparer.OrdinalIgnoreCase) - .ToArrayAsync(); + .ToArrayAsync(TestContext.CancellationToken) + .ConfigureAwait(false); } finally { File.Delete(file); } - Assert.Equal(0, rc); - Assert.Equal(expected, actual, new UserRightEntryEqualityComparer()); + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEqual(expected, actual); } } \ No newline at end of file diff --git a/src/Tests.Cli/ListSyntaxTests.cs b/src/Tests.Cli/ListSyntaxTests.cs index 9853605..8988373 100644 --- a/src/Tests.Cli/ListSyntaxTests.cs +++ b/src/Tests.Cli/ListSyntaxTests.cs @@ -1,102 +1,98 @@ namespace Tests.Cli; -using Microsoft.Extensions.DependencyInjection; -using UserRights.Application; using UserRights.Cli; -using Xunit; /// /// Represents syntax tests for list functionality. /// -public sealed class ListSyntaxTests : CliTestBase +[TestClass] +public sealed class ListSyntaxTests : CliBuilderFixture { - private readonly CliBuilder _builder; - - /// - /// Initializes a new instance of the class. - /// - public ListSyntaxTests() - { - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - _builder = ServiceProvider.GetRequiredService(); - } - /// /// Verifies list mode with CSV formatted output sent to STDOUT is parsed successfully. /// - [Fact] + [TestMethod] public void CsvToStdoutShouldWork() { + // Arrange. var args = new[] { "list" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Verifies list mode with CSV formatted output sent to a file is parsed successfully. /// - [Fact] + [TestMethod] public void CsvToPathShouldWork() { + // Arrange. var args = new[] { "list", "--path", "file.csv" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Ensures an empty or whitespace path is rejected. /// /// The test arguments. - [Theory] - [InlineData("list", "--path", "")] - [InlineData("list", "--path", " ")] + [TestMethod] + [DataRow("list", "--path", "")] + [DataRow("list", "--path", " ")] public void PathWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); + => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures an empty or whitespace system name is rejected. /// /// The test arguments. - [Theory] - [InlineData("list", "--system-name", "")] - [InlineData("list", "--system-name", " ")] + [TestMethod] + [DataRow("list", "--system-name", "")] + [DataRow("list", "--system-name", " ")] public void SystemNameWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); + => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Verifies list mode with JSON formatted output sent to STDOUT is parsed successfully. /// - [Fact] + [TestMethod] public void JsonToStdoutShouldWork() { + // Arrange. var args = new[] { "list", "--json" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Verifies list mode with JSON formatted output sent to a file is parsed successfully. /// - [Fact] + [TestMethod] public void JsonToPathShouldWork() { - var args = new[] { "list", "--json", "--path", "file.csv" }; - var rootCommand = _builder.Build(); + // Arrange. + var args = new[] { "list", "--json", "--path", "file.json" }; + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } } \ No newline at end of file diff --git a/src/Tests.Cli/MSTestSettings.cs b/src/Tests.Cli/MSTestSettings.cs new file mode 100644 index 0000000..6b35270 --- /dev/null +++ b/src/Tests.Cli/MSTestSettings.cs @@ -0,0 +1 @@ +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] \ No newline at end of file diff --git a/src/Tests.Cli/PrincipalCommandTests.cs b/src/Tests.Cli/PrincipalCommandTests.cs index 7c819d8..ebd1eaa 100644 --- a/src/Tests.Cli/PrincipalCommandTests.cs +++ b/src/Tests.Cli/PrincipalCommandTests.cs @@ -2,24 +2,23 @@ namespace Tests.Cli; using System.Security.Principal; -using Microsoft.Extensions.DependencyInjection; -using UserRights.Application; using UserRights.Cli; -using Xunit; using static Tests.TestData; /// /// Represents integration tests for modify principal functionality. /// -public sealed class PrincipalCommandTests : CliTestBase +[TestClass] +public class PrincipalCommandTests { /// - /// Verifies a granting a privilege to a principal and revoking their other privileges is successful and does not modify other assignments. + /// Verifies granting a privilege to a principal and revoking their other privileges is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void GrantAndRevokeOthersShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1 @@ -39,43 +38,35 @@ public void GrantAndRevokeOthersShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal([PrincipalSid1, PrincipalSid2], policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); policy.ResetConnection(); - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); + using var fixture = new CliBuilderFixture(policy); - var builder = ServiceProvider.GetRequiredService(); + var rootCommand = fixture.CliBuilder.Build(); - var rootCommand = builder.Build(); - - var args = new[] - { - "principal", - PrincipalName1, - "--grant", - Privilege2, - "--revoke-others" - }; + var args = new[] { "principal", PrincipalName1, "--grant", Privilege2, "--revoke-others" }; + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); - Assert.Equal([PrincipalSid1, PrincipalSid2], policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); } /// /// Verifies a single grant with a single revoke is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void GrantAndRevokeShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -96,44 +87,35 @@ public void GrantAndRevokeShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); policy.ResetConnection(); - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); + using var fixture = new CliBuilderFixture(policy); - var rootCommand = builder.Build(); + var rootCommand = fixture.CliBuilder.Build(); - var args = new[] - { - "principal", - PrincipalName1, - "--grant", - Privilege2, - "--revoke", - Privilege1 - }; + var args = new[] { "principal", PrincipalName1, "--grant", Privilege2, "--revoke", Privilege1 }; + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); } /// /// Verifies a single grant is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void GrantShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -154,42 +136,35 @@ public void GrantShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); policy.ResetConnection(); - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); + using var fixture = new CliBuilderFixture(policy); - var builder = ServiceProvider.GetRequiredService(); + var rootCommand = fixture.CliBuilder.Build(); - var rootCommand = builder.Build(); - - var args = new[] - { - "principal", - PrincipalName1, - "--grant", - Privilege2 - }; + var args = new[] { "principal", PrincipalName1, "--grant", Privilege2 }; + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid1).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); } /// /// Verifies a revoking all privileges for a principal is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void RevokeAllShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -211,41 +186,35 @@ public void RevokeAllShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid1).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); policy.ResetConnection(); - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); + using var fixture = new CliBuilderFixture(policy); - var rootCommand = builder.Build(); + var rootCommand = fixture.CliBuilder.Build(); - var args = new[] - { - "principal", - PrincipalName1, - "--revoke-all" - }; + var args = new[] { "principal", PrincipalName1, "--revoke-all" }; + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); - Assert.Empty(policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal([PrincipalSid2], policy.LsaEnumerateAccountsWithUserRight()); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + // Assert. + Assert.AreEqual(0, rc); + Assert.IsEmpty(policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); } /// /// Verifies a single revocation is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void RevokeShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -266,33 +235,25 @@ public void RevokeShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); policy.ResetConnection(); - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); + using var fixture = new CliBuilderFixture(policy); - var builder = ServiceProvider.GetRequiredService(); + var rootCommand = fixture.CliBuilder.Build(); - var rootCommand = builder.Build(); - - var args = new[] - { - "principal", - PrincipalName2, - "--revoke", - Privilege2 - }; + var args = new[] { "principal", PrincipalName2, "--revoke", Privilege2 }; + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); } } \ No newline at end of file diff --git a/src/Tests.Cli/PrincipalSyntaxTests.cs b/src/Tests.Cli/PrincipalSyntaxTests.cs index 926b3b8..37087da 100644 --- a/src/Tests.Cli/PrincipalSyntaxTests.cs +++ b/src/Tests.Cli/PrincipalSyntaxTests.cs @@ -1,244 +1,260 @@ namespace Tests.Cli; -using Microsoft.Extensions.DependencyInjection; -using UserRights.Application; using UserRights.Cli; -using Xunit; /// /// Represents syntax tests for principal functionality. /// -public sealed class PrincipalSyntaxTests : CliTestBase +[TestClass] +public sealed class PrincipalSyntaxTests : CliBuilderFixture { - private readonly CliBuilder _builder; - - /// - /// Initializes a new instance of the class. - /// - public PrincipalSyntaxTests() - { - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - _builder = ServiceProvider.GetRequiredService(); - } - /// /// Ensures granting a privilege and revoking a different privilege is accepted. /// - [Fact] + [TestMethod] public void GrantAndRevokeShouldWork() { + // Arrange. var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--revoke", "SeBatchLogonRight" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Ensures granting multiple privileges is accepted. /// - [Fact] + [TestMethod] public void GrantMultipleShouldWork() { + // Arrange. var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--grant", "SeBatchLogonRight" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Ensures granting a privilege is accepted. /// - [Fact] + [TestMethod] public void GrantShouldWork() { + // Arrange. var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Ensures an empty or whitespace grant is rejected. /// /// The test arguments. - [Theory] - [InlineData("principal", "DOMAIN\\UserOrGroup", "--grant", "")] - [InlineData("principal", "DOMAIN\\UserOrGroup", "--grant", " ")] + [TestMethod] + [DataRow("principal", "DOMAIN\\UserOrGroup", "--grant", "")] + [DataRow("principal", "DOMAIN\\UserOrGroup", "--grant", " ")] public void GrantWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); + => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures granting a privilege and revoking all other privileges is rejected. /// - [Fact] + [TestMethod] public void GrantWithRevokeAllThrowsException() { + // Arrange. var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--revoke-all" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act & Assert. Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// /// Ensures specifying no options is rejected. /// - [Fact] + [TestMethod] public void NoOptionsThrowsException() { + // Arrange. var args = new[] { "principal" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act & Assert. Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// - /// Ensures overlapping or duplicate privileges is rejected. + /// Ensures overlapping or duplicate privileges are rejected. /// /// The test arguments. - [Theory] - [InlineData("principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--revoke", "SeServiceLogonRight")] - [InlineData("principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--grant", "SeServiceLogonRight")] - [InlineData("principal", "DOMAIN\\UserOrGroup", "--revoke", "SeServiceLogonRight", "--revoke", "SeServiceLogonRight")] + [TestMethod] + [DataRow("principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--revoke", "SeServiceLogonRight")] + [DataRow("principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--grant", "SeServiceLogonRight")] + [DataRow("principal", "DOMAIN\\UserOrGroup", "--revoke", "SeServiceLogonRight", "--revoke", "SeServiceLogonRight")] public void OverlappingGrantsAndRevokesThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); + => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures an empty or whitespace principal is rejected. /// /// The test arguments. - [Theory] - [InlineData("principal", "", "--grant", "SeServiceLogonRight")] - [InlineData("principal", " ", "--grant", "SeServiceLogonRight")] + [TestMethod] + [DataRow("principal", "", "--grant", "SeServiceLogonRight")] + [DataRow("principal", " ", "--grant", "SeServiceLogonRight")] public void PrincipalWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); + => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures revoking all privileges is accepted. /// - [Fact] + [TestMethod] public void RevokeAllShouldWork() { + // Arrange. var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-all" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// - /// Ensures granting a privilege and granting all privileges is rejected. + /// Ensures granting a privilege and revoking all privileges is rejected. /// - [Fact] + [TestMethod] public void RevokeAllWithGrantsThrowsException() { + // Arrange. var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-all", "--grant", "SeServiceLogonRight" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act & Assert. Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// /// Ensures revoking a privilege and revoking all privileges is rejected. /// - [Fact] + [TestMethod] public void RevokeAllWithRevocationsThrowsException() { + // Arrange. var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-all", "--revoke", "SeServiceLogonRight" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act & Assert. Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// /// Ensures revoking all privileges and revoking other privileges is rejected. /// - [Fact] + [TestMethod] public void RevokeAllWithRevokeOthersThrowsException() { + // Arrange. var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-all", "--revoke-others" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act & Assert. Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// /// Ensures revoking multiple privileges is accepted. /// - [Fact] + [TestMethod] public void RevokeMultipleShouldWork() { + // Arrange. var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke", "SeServiceLogonRight", "--revoke", "SeBatchLogonRight" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Ensures revoke other privileges without granting a privilege is rejected. /// - [Fact] + [TestMethod] public void RevokeOthersWithOutGrantsThrowsException() { + // Arrange. var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-others" }; - Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); + // Act & Assert. + Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); } /// - /// Ensures revoke other privileges with revoking a privilege is rejected. + /// Ensures revoke other privileges while also revoking a privilege is rejected. /// - [Fact] + [TestMethod] public void RevokeOthersWithRevocationsThrowsException() { + // Arrange. var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-others", "--revoke", "SeServiceLogonRight" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act & Assert. Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// /// Ensures revoking a privilege is accepted. /// - [Fact] + [TestMethod] public void RevokeShouldWork() { + // Arrange. var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke", "SeServiceLogonRight" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Ensures an empty or whitespace revocation is rejected. /// /// The test arguments. - [Theory] - [InlineData("principal", "DOMAIN\\UserOrGroup", "--revoke", "")] - [InlineData("principal", "DOMAIN\\UserOrGroup", "--revoke", " ")] + [TestMethod] + [DataRow("principal", "DOMAIN\\UserOrGroup", "--revoke", "")] + [DataRow("principal", "DOMAIN\\UserOrGroup", "--revoke", " ")] public void RevokeWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); + => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures an empty or whitespace system name is rejected. /// /// The test arguments. - [Theory] - [InlineData("principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--system-name", "")] - [InlineData("principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--system-name", " ")] + [TestMethod] + [DataRow("principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--system-name", "")] + [DataRow("principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--system-name", " ")] public void SystemNameWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); + => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); } \ No newline at end of file diff --git a/src/Tests.Cli/PrivilegeCommandTests.cs b/src/Tests.Cli/PrivilegeCommandTests.cs index 451559d..a1f459c 100644 --- a/src/Tests.Cli/PrivilegeCommandTests.cs +++ b/src/Tests.Cli/PrivilegeCommandTests.cs @@ -2,24 +2,23 @@ namespace Tests.Cli; using System.Security.Principal; -using Microsoft.Extensions.DependencyInjection; -using UserRights.Application; using UserRights.Cli; -using Xunit; using static Tests.TestData; /// /// Represents integration tests for modify privilege functionality. /// -public sealed class PrivilegeCommandTests : CliTestBase +[TestClass] +public class PrivilegeCommandTests { /// /// Verifies granting a principal to a privilege and revoking its other principals is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void GrantAndRevokeOthersShouldWork() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -40,47 +39,39 @@ public void GrantAndRevokeOthersShouldWork() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight(Privilege1).Order()); - Assert.Equal([PrincipalSid2], policy.LsaEnumerateAccountsWithUserRight(Privilege2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege1)); + CollectionAssert.AreEqual(new[] { PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege2)); policy.ResetConnection(); - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); + using var fixture = new CliBuilderFixture(policy); - var builder = ServiceProvider.GetRequiredService(); + var rootCommand = fixture.CliBuilder.Build(); - var rootCommand = builder.Build(); - - var args = new[] - { - "privilege", - Privilege2, - "--grant", - PrincipalName1, - "--revoke-others" - }; + var args = new[] { "privilege", Privilege2, "--grant", PrincipalName1, "--revoke-others" }; + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid1).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight(Privilege1).Order()); - Assert.Equal([PrincipalSid1], policy.LsaEnumerateAccountsWithUserRight(Privilege2)); + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege1)); + CollectionAssert.AreEqual(new[] { PrincipalSid1 }, policy.LsaEnumerateAccountsWithUserRight(Privilege2)); } /// /// Verifies a single grant with a single revoke is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void GrantAndRevokePasses() { + // Arrange. var principals1 = new List { PrincipalSid1 @@ -101,44 +92,35 @@ public void GrantAndRevokePasses() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid1).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); policy.ResetConnection(); - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); + using var fixture = new CliBuilderFixture(policy); - var builder = ServiceProvider.GetRequiredService(); + var rootCommand = fixture.CliBuilder.Build(); - var rootCommand = builder.Build(); - - var args = new[] - { - "privilege", - Privilege1, - "--grant", - PrincipalName2, - "--revoke", - PrincipalName1 - }; + var args = new[] { "privilege", Privilege1, "--grant", PrincipalName2, "--revoke", PrincipalName1 }; + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); } /// /// Verifies granting a principal to a privilege and revoking all principals matching a pattern is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void GrantAndRevokePatternPasses() { + // Arrange. var principals1 = new List { PrincipalSidCurrent, @@ -162,47 +144,38 @@ public void GrantAndRevokePatternPasses() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSidCurrent, PrincipalSid1, PrincipalSid2, PrincipalSid3 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSidCurrent)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid3).Order(StringComparer.OrdinalIgnoreCase)); + CollectionAssert.AreEquivalent(new[] { PrincipalSidCurrent, PrincipalSid1, PrincipalSid2, PrincipalSid3 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSidCurrent)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid3)); policy.ResetConnection(); - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); + using var fixture = new CliBuilderFixture(policy); - var builder = ServiceProvider.GetRequiredService(); + var rootCommand = fixture.CliBuilder.Build(); - var rootCommand = builder.Build(); - - var args = new[] - { - "privilege", - Privilege1, - "--grant", - PrincipalName1, - "--revoke-pattern", - "^S-1-5-21" - }; + var args = new[] { "privilege", Privilege1, "--grant", PrincipalName1, "--revoke-pattern", "^S-1-5-21" }; + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid1).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal([Privilege1, Privilege2], policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal([Privilege1, Privilege2], policy.LsaEnumerateAccountRights(PrincipalSid3).Order(StringComparer.OrdinalIgnoreCase)); + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid3)); } /// /// Verifies a single grant is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void GrantPasses() { + // Arrange. var principals1 = new List { PrincipalSid1 @@ -222,42 +195,35 @@ public void GrantPasses() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); policy.ResetConnection(); - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); + using var fixture = new CliBuilderFixture(policy); - var builder = ServiceProvider.GetRequiredService(); + var rootCommand = fixture.CliBuilder.Build(); - var rootCommand = builder.Build(); - - var args = new[] - { - "privilege", - Privilege2, - "--grant", - PrincipalName1 - }; + var args = new[] { "privilege", Privilege2, "--grant", PrincipalName1 }; + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid1).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); } /// /// Verifies revoking all principals for a privilege is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void RevokeAllPasses() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -279,45 +245,39 @@ public void RevokeAllPasses() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid1).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight(Privilege1).Order()); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight(Privilege2).Order()); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege1)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege2)); policy.ResetConnection(); - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); + using var fixture = new CliBuilderFixture(policy); - var builder = ServiceProvider.GetRequiredService(); + var rootCommand = fixture.CliBuilder.Build(); - var rootCommand = builder.Build(); - - var args = new[] - { - "privilege", - Privilege1, - "--revoke-all" - }; + var args = new[] { "privilege", Privilege1, "--revoke-all" }; + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - Assert.Empty(policy.LsaEnumerateAccountsWithUserRight(Privilege1)); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight(Privilege2).Order()); + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + Assert.IsEmpty(policy.LsaEnumerateAccountsWithUserRight(Privilege1)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege2)); } /// /// Verifies a single revocation is successful and does not modify other assignments. /// - [Fact] + [TestMethod] public void RevokePasses() { + // Arrange. var principals1 = new List { PrincipalSid1, @@ -338,42 +298,35 @@ public void RevokePasses() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); policy.ResetConnection(); - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); + using var fixture = new CliBuilderFixture(policy); - var rootCommand = builder.Build(); + var rootCommand = fixture.CliBuilder.Build(); - var args = new[] - { - "privilege", - Privilege1, - "--revoke", - PrincipalName2 - }; + var args = new[] { "privilege", Privilege1, "--revoke", PrincipalName2 }; + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); } /// - /// Verifies revoking all non builtin and virtual principals from a privilege is successful. + /// Verifies revoking all non-builtin and virtual principals from a privilege is successful. /// - [Fact] + [TestMethod] public void RevokePatternForAllButBuiltinAndVirtualPasses() { + // Arrange. var principals1 = new List { PrincipalSidCurrent, @@ -397,36 +350,28 @@ public void RevokePatternForAllButBuiltinAndVirtualPasses() var policy = new MockLsaUserRights(database); policy.Connect("SystemName"); - Assert.Equal(new[] { PrincipalSidCurrent, PrincipalSid1, PrincipalSid2, PrincipalSid3 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSidCurrent)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid3).Order(StringComparer.OrdinalIgnoreCase)); + CollectionAssert.AreEquivalent(new[] { PrincipalSidCurrent, PrincipalSid1, PrincipalSid2, PrincipalSid3 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSidCurrent)); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid3)); policy.ResetConnection(); - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); + using var fixture = new CliBuilderFixture(policy); - var rootCommand = builder.Build(); + var rootCommand = fixture.CliBuilder.Build(); - var args = new[] - { - "privilege", - Privilege1, - "--revoke-pattern", - "^S-1-5-21" - }; + var args = new[] { "privilege", Privilege1, "--revoke-pattern", "^S-1-5-21" }; + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal([Privilege1, Privilege2], policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); - Assert.Equal([Privilege1, Privilege2], policy.LsaEnumerateAccountRights(PrincipalSid3).Order(StringComparer.OrdinalIgnoreCase)); + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }, policy.LsaEnumerateAccountsWithUserRight()); + CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid3)); } } \ No newline at end of file diff --git a/src/Tests.Cli/PrivilegeSyntaxTests.cs b/src/Tests.Cli/PrivilegeSyntaxTests.cs index 960b4a2..44b52e5 100644 --- a/src/Tests.Cli/PrivilegeSyntaxTests.cs +++ b/src/Tests.Cli/PrivilegeSyntaxTests.cs @@ -1,294 +1,320 @@ namespace Tests.Cli; -using Microsoft.Extensions.DependencyInjection; -using UserRights.Application; using UserRights.Cli; -using Xunit; /// /// Represents syntax tests for privilege functionality. /// -public sealed class PrivilegeSyntaxTests : CliTestBase +[TestClass] +public sealed class PrivilegeSyntaxTests : CliBuilderFixture { - private readonly CliBuilder _builder; - - /// - /// Initializes a new instance of the class. - /// - public PrivilegeSyntaxTests() - { - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - _builder = ServiceProvider.GetRequiredService(); - } - /// /// Ensures granting a context and revoking a different context is accepted. /// - [Fact] + [TestMethod] public void GrantAndRevokeShouldWork() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\User", "--revoke", "DOMAIN\\Group" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Ensures granting multiple contexts is accepted. /// - [Fact] + [TestMethod] public void GrantMultipleShouldWork() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\User", "--grant", "DOMAIN\\Group" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Ensures granting a context is accepted. /// - [Fact] + [TestMethod] public void GrantShouldWork() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\User" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Ensures an empty or whitespace grant is rejected. /// /// The test arguments. - [Theory] - [InlineData("privilege", "SeServiceLogonRight", "--grant", "")] - [InlineData("privilege", "SeServiceLogonRight", "--grant", " ")] + [TestMethod] + [DataRow("privilege", "SeServiceLogonRight", "--grant", "")] + [DataRow("privilege", "SeServiceLogonRight", "--grant", " ")] public void GrantWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); + => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures granting a context and revoking all contexts is rejected. /// - [Fact] + [TestMethod] public void GrantWithRevokeAllThrowsException() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\User", "--revoke-all" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act & Assert. Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// /// Ensures specifying no options is rejected. /// - [Fact] + [TestMethod] public void NoOptionsThrowsException() { + // Arrange. var args = new[] { "privilege" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act & Assert. Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// - /// Ensures overlapping or duplicate contexts is rejected. + /// Ensures overlapping or duplicate contexts are rejected. /// /// The test arguments. - [Theory] - [InlineData("privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--revoke", "DOMAIN\\UserOrGroup")] - [InlineData("privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--grant", "DOMAIN\\UserOrGroup")] - [InlineData("privilege", "SeServiceLogonRight", "--revoke", "DOMAIN\\UserOrGroup", "--revoke", "DOMAIN\\UserOrGroup")] + [TestMethod] + [DataRow("privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--revoke", "DOMAIN\\UserOrGroup")] + [DataRow("privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--grant", "DOMAIN\\UserOrGroup")] + [DataRow("privilege", "SeServiceLogonRight", "--revoke", "DOMAIN\\UserOrGroup", "--revoke", "DOMAIN\\UserOrGroup")] public void OverlappingGrantsAndRevokesThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); + => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures an empty or whitespace principal is rejected. /// /// The test arguments. - [Theory] - [InlineData("privilege", "", "--grant", "DOMAIN\\UserOrGroup")] - [InlineData("privilege", " ", "--grant", "DOMAIN\\UserOrGroup")] + [TestMethod] + [DataRow("privilege", "", "--grant", "DOMAIN\\UserOrGroup")] + [DataRow("privilege", " ", "--grant", "DOMAIN\\UserOrGroup")] public void PrivilegeWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); + => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures revoking all contexts is accepted. /// - [Fact] + [TestMethod] public void RevokeAllShouldWork() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-all" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); - int? rc = null; - var exception = Record.Exception(() => rc = rootCommand.Parse(args).ThrowIfInvalid().Run()); + // Act. + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Null(exception); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Ensures granting a context and revoking all contexts is rejected. /// - [Fact] + [TestMethod] public void RevokeAllWithGrantsThrowsException() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-all", "--grant", "DOMAIN\\UserOrGroup" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act & Assert. Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// /// Ensures revoking a context and revoking all contexts is rejected. /// - [Fact] + [TestMethod] public void RevokeAllWithRevocationsThrowsException() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-all", "--revoke", "DOMAIN\\UserOrGroup" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act & Assert. Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// /// Ensures revoking all contexts and revoking other contexts is rejected. /// - [Fact] + [TestMethod] public void RevokeAllWithRevokeOthersThrowsException() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-all", "--revoke-others" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act & Assert. Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// /// Ensures revoking multiple contexts is accepted. /// - [Fact] + [TestMethod] public void RevokeMultipleShouldWork() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--revoke", "DOMAIN\\User", "--revoke", "DOMAIN\\Group" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Ensures revoke other contexts without granting a context is rejected. /// - [Fact] + [TestMethod] public void RevokeOthersWithOutGrantsThrowsException() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-others" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act & Assert. Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// /// Ensures revoke other contexts with revoking a context is rejected. /// - [Fact] + [TestMethod] public void RevokeOthersWithRevocationsThrowsException() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-others", "--revoke", "DOMAIN\\UserOrGroup" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act & Assert. Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// /// Ensures revoking a valid pattern is accepted. /// - [Fact] + [TestMethod] public void RevokePatternShouldWork() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Ensures granting a context and revoking a valid pattern is accepted. /// - [Fact] + [TestMethod] public void RevokePatternWithGrantShouldWork() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--revoke-pattern", "^S-1-5-21-" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Ensures revoking a valid regex is accepted. /// /// The test arguments. - [Theory] - [InlineData("privilege", "SeServiceLogonRight", "--revoke-pattern", "^xyz.*")] - [InlineData("privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-")] - [InlineData("privilege", "SeServiceLogonRight", "--revoke-pattern", "(?i)^[A-Z]+")] + [TestMethod] + [DataRow("privilege", "SeServiceLogonRight", "--revoke-pattern", "^xyz.*")] + [DataRow("privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-")] + [DataRow("privilege", "SeServiceLogonRight", "--revoke-pattern", "(?i)^[A-Z]+")] public void RevokePatternWithValidRegexShouldWork(params string[] args) { - var rootCommand = _builder.Build(); + // Arrange. + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Ensures revoking a pattern and revoking all contexts is rejected. /// - [Fact] + [TestMethod] public void RevokePatternWithRevokeAllThrowsException() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-", "--revoke-all" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act & Assert. Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// /// Ensures revoking a pattern and revoking other contexts is rejected. /// - [Fact] + [TestMethod] public void RevokePatternWithRevokeOthersThrowsException() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-", "--revoke-others" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act & Assert. Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } /// /// Ensures revoking a pattern and revoking a context is rejected. /// - [Fact] + [TestMethod] public void RevokePatternWithRevokeThrowsException() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-", "--revoke", "DOMAIN\\UserOrGroup" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act & Assert. Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); } @@ -296,53 +322,56 @@ public void RevokePatternWithRevokeThrowsException() /// Ensures revoking an invalid regex is rejected. /// /// The test arguments. - [Theory] - [InlineData("privilege", "SeServiceLogonRight", "--revoke-pattern", "[0-9]{3,1}")] - [InlineData("privilege", "SeServiceLogonRight", "--revoke-pattern", "^[S-1-5-21-")] + [TestMethod] + [DataRow("privilege", "SeServiceLogonRight", "--revoke-pattern", "[0-9]{3,1}")] + [DataRow("privilege", "SeServiceLogonRight", "--revoke-pattern", "^[S-1-5-21-")] public void RevokePatternWithInvalidRegexThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); + => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures an empty or whitespace revocation pattern is rejected. /// /// The test arguments. - [Theory] - [InlineData("privilege", "SeServiceLogonRight", "--revoke-pattern", "")] - [InlineData("privilege", "SeServiceLogonRight", "--revoke-pattern", " ")] + [TestMethod] + [DataRow("privilege", "SeServiceLogonRight", "--revoke-pattern", "")] + [DataRow("privilege", "SeServiceLogonRight", "--revoke-pattern", " ")] public void RevokePatternWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); + => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures revoking a context is accepted. /// - [Fact] + [TestMethod] public void RevokeShouldWork() { + // Arrange. var args = new[] { "privilege", "SeServiceLogonRight", "--revoke", "DOMAIN\\UserOrGroup" }; - var rootCommand = _builder.Build(); + var rootCommand = CliBuilder.Build(); + // Act. var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - Assert.Equal(0, rc); + // Assert. + Assert.AreEqual(0, rc); } /// /// Ensures an empty or whitespace revocation is rejected. /// /// The test arguments. - [Theory] - [InlineData("privilege", "SeServiceLogonRight", "--revoke", "")] - [InlineData("privilege", "SeServiceLogonRight", "--revoke", " ")] + [TestMethod] + [DataRow("privilege", "SeServiceLogonRight", "--revoke", "")] + [DataRow("privilege", "SeServiceLogonRight", "--revoke", " ")] public void RevokeWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); + => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); /// /// Ensures an empty or whitespace system name is rejected. /// /// The test arguments. - [Theory] - [InlineData("privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--system-name", "")] - [InlineData("privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--system-name", " ")] + [TestMethod] + [DataRow("privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--system-name", "")] + [DataRow("privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--system-name", " ")] public void SystemNameWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).ThrowIfInvalid().Run()); + => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); } \ No newline at end of file diff --git a/src/Tests.Cli/Tests.Cli.csproj b/src/Tests.Cli/Tests.Cli.csproj index 2ce29e5..67f6e28 100644 --- a/src/Tests.Cli/Tests.Cli.csproj +++ b/src/Tests.Cli/Tests.Cli.csproj @@ -1,37 +1,22 @@ - + + Exe net10.0-windows enable enable latest latest-All - + All false - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - \ No newline at end of file diff --git a/src/Tests/.editorconfig b/src/Tests/.editorconfig new file mode 100644 index 0000000..68c93aa --- /dev/null +++ b/src/Tests/.editorconfig @@ -0,0 +1,2 @@ +[*.cs] +dotnet_diagnostic.MSTEST0004.severity = none # Public types should be test classes. \ No newline at end of file diff --git a/src/Tests/CliBuilderFixture.cs b/src/Tests/CliBuilderFixture.cs new file mode 100644 index 0000000..9aa438a --- /dev/null +++ b/src/Tests/CliBuilderFixture.cs @@ -0,0 +1,126 @@ +namespace Tests; + +using System.Security.Principal; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using UserRights.Application; +using UserRights.Cli; + +/// +/// Represents a test fixture with a implementation for testing the CLI. +/// +public class CliBuilderFixture : IDisposable +{ + private readonly ServiceProvider _serviceProvider; + + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a fixture with an empty, mock implementation, and a mock implementation. + /// + public CliBuilderFixture() + { + var serviceCollection = new ServiceCollection() + .AddLogging(builder => builder + .ClearProviders() + .SetMinimumLevel(LogLevel.Trace) + .AddDebug()); + + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + + _serviceProvider = serviceCollection.BuildServiceProvider(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The existing LSA user rights implementation. + /// + /// Creates a fixture with a user-supplied implementation, and a complete instance of a implementation. + /// + public CliBuilderFixture(ILsaUserRights policy) + { + ArgumentNullException.ThrowIfNull(policy); + + var serviceCollection = new ServiceCollection() + .AddLogging(builder => builder + .ClearProviders() + .SetMinimumLevel(LogLevel.Trace) + .AddDebug()); + + serviceCollection.AddSingleton(policy); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + + _serviceProvider = serviceCollection.BuildServiceProvider(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The entries to include in the mock policy database. + /// The remote system name to execute the task on (default localhost). + /// + /// Creates a fixture with user-supplied, existing policy entries, and mock instances of a and implementations. + /// + public CliBuilderFixture(IDictionary> database, string? systemName = null) + { + ArgumentNullException.ThrowIfNull(database); + + var serviceCollection = new ServiceCollection() + .AddLogging(builder => builder + .ClearProviders() + .SetMinimumLevel(LogLevel.Trace) + .AddDebug()); + + var policy = new MockLsaUserRights(database); + policy.Connect(systemName); + + serviceCollection.AddSingleton(policy); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + + _serviceProvider = serviceCollection.BuildServiceProvider(); + } + + /// + /// Gets a CLI builder with a mock implementation of . + /// + public CliBuilder CliBuilder => + _disposed + ? throw new ObjectDisposedException(GetType().FullName) + : _serviceProvider.GetRequiredService(); + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases resources when they are no longer required. + /// + /// A value indicating whether the method call comes from a dispose method (its value is ) or from a finalizer (its value is ). + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _serviceProvider.Dispose(); + } + + _disposed = true; + } +} \ No newline at end of file diff --git a/src/Tests/IUserRightsSerializable.cs b/src/Tests/IUserRightsSerializable.cs deleted file mode 100644 index 03ad48e..0000000 --- a/src/Tests/IUserRightsSerializable.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Tests; - -using UserRights.Application; - -using Xunit.Abstractions; - -/// -/// Represents the interface to the local security authority user right functions with support for serialization in xUnit.net. -/// -public interface IUserRightsSerializable : IUserRights, IXunitSerializable -{ -} \ No newline at end of file diff --git a/src/Tests.Application/LsaUserRightsTestBase.cs b/src/Tests/LsaUserRightsSnapshotFixture.cs similarity index 88% rename from src/Tests.Application/LsaUserRightsTestBase.cs rename to src/Tests/LsaUserRightsSnapshotFixture.cs index 44139b7..94e36d7 100644 --- a/src/Tests.Application/LsaUserRightsTestBase.cs +++ b/src/Tests/LsaUserRightsSnapshotFixture.cs @@ -1,4 +1,4 @@ -namespace Tests.Application; +namespace Tests; using System.Collections.ObjectModel; using System.Diagnostics; @@ -8,13 +8,16 @@ namespace Tests.Application; using System.Text; using Microsoft.Extensions.Configuration; -using UserRights.Application; + using UserRights.Extensions.Security; /// -/// Represents the test base for application. +/// Represents a test fixture for preserving the state of the local security authority (LSA) database during test execution. /// -public abstract class LsaUserRightsTestBase : IDisposable +/// +/// This fixture creates a temporary directory and backs up the security database when instantiated, then restores it during disposal. +/// +public abstract class LsaUserRightsSnapshotFixture : IDisposable { private const string ExportSecurityTemplateName = "export.ini"; private const string ExportSecurityLogName = "export.log"; @@ -28,9 +31,9 @@ public abstract class LsaUserRightsTestBase : IDisposable private bool _disposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - protected LsaUserRightsTestBase() + protected LsaUserRightsSnapshotFixture() { try { @@ -90,9 +93,9 @@ protected virtual void Dispose(bool disposing) _directory.Delete(true); } - - _disposed = true; } + + _disposed = true; } /// @@ -149,7 +152,7 @@ private static void CreateRestoreTemplate(string workingDirectory, IReadOnlyDict continue; } - var entry = string.Format(CultureInfo.InvariantCulture, "{0} =", privilege); + var entry = string.Create(CultureInfo.InvariantCulture, $"{privilege} ="); lines.Insert(index + 1, entry); } @@ -167,11 +170,9 @@ private static void CreateSecurityDatabaseBackup(string workingDirectory) { ArgumentException.ThrowIfNullOrWhiteSpace(workingDirectory); - var arguments = string.Format( + var arguments = string.Create( CultureInfo.InvariantCulture, - "/export /cfg {0} /areas user_rights /log {1}", - ExportSecurityTemplateName, - ExportSecurityLogName); + $"/export /cfg {ExportSecurityTemplateName} /areas user_rights /log {ExportSecurityLogName}"); var stringBuilder = new StringBuilder(); @@ -190,15 +191,13 @@ private static void CreateSecurityDatabaseBackup(string workingDirectory) process.BeginErrorReadLine(); - process.WaitForExit(5000); + process.WaitForExit(); if (process.ExitCode != 0) { - var message = string.Format( + var message = string.Create( CultureInfo.InvariantCulture, - "Failed to export the security database, exit code: {0}\r\n{1}", - process.ExitCode, - stringBuilder); + $"Failed to export the security database, exit code: {process.ExitCode}\r\n{stringBuilder}"); throw new InvalidOperationException(message); } @@ -277,12 +276,9 @@ private static void RestoreSecurityDatabaseBackup(string workingDirectory) { ArgumentException.ThrowIfNullOrWhiteSpace(workingDirectory); - var arguments = string.Format( + var arguments = string.Create( CultureInfo.InvariantCulture, - "/configure /db {0} /cfg {1} /areas user_rights /log {2}", - RestoreSecurityDatabaseName, - RestoreSecurityTemplateName, - RestoreSecurityLogName); + $"/configure /db {RestoreSecurityDatabaseName} /cfg {RestoreSecurityTemplateName} /areas user_rights /log {RestoreSecurityLogName}"); var stringBuilder = new StringBuilder(); @@ -301,15 +297,13 @@ private static void RestoreSecurityDatabaseBackup(string workingDirectory) process.BeginErrorReadLine(); - process.WaitForExit(5000); + process.WaitForExit(); if (process.ExitCode != 0) { - var message = string.Format( + var message = string.Create( CultureInfo.InvariantCulture, - "Failed to restore the security database, exit code: {0}\r\n{1}", - process.ExitCode, - stringBuilder); + $"Failed to restore the security database, exit code: {process.ExitCode}\r\n{stringBuilder}"); throw new InvalidOperationException(message); } diff --git a/src/Tests/MockLsaUserRights.cs b/src/Tests/MockLsaUserRights.cs index 8ead010..2bb8f12 100644 --- a/src/Tests/MockLsaUserRights.cs +++ b/src/Tests/MockLsaUserRights.cs @@ -4,12 +4,10 @@ namespace Tests; using UserRights.Application; -using Xunit.Abstractions; - /// /// Represents a mock implementation. /// -public sealed class MockLsaUserRights : ILsaUserRights, IUserRightsSerializable +public sealed class MockLsaUserRights : ILsaUserRights { private readonly IDictionary> _database = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); private bool _connected; @@ -51,11 +49,7 @@ public void LsaAddAccountRights(SecurityIdentifier accountSid, params string[] u { ArgumentNullException.ThrowIfNull(accountSid); ArgumentNullException.ThrowIfNull(userRights); - - if (userRights.Length == 0) - { - throw new ArgumentException("Value cannot be an empty collection.", nameof(userRights)); - } + ArgumentOutOfRangeException.ThrowIfZero(userRights.Length, nameof(userRights)); if (!_connected) { @@ -119,11 +113,7 @@ public void LsaRemoveAccountRights(SecurityIdentifier accountSid, params string[ { ArgumentNullException.ThrowIfNull(accountSid); ArgumentNullException.ThrowIfNull(userRights); - - if (userRights.Length == 0) - { - throw new ArgumentException("Value cannot be an empty collection.", nameof(userRights)); - } + ArgumentOutOfRangeException.ThrowIfZero(userRights.Length, nameof(userRights)); if (!_connected) { @@ -144,33 +134,6 @@ public void LsaRemoveAccountRights(SecurityIdentifier accountSid, params string[ /// public void ResetConnection() => _connected = false; - /// - public void Deserialize(IXunitSerializationInfo info) - { - ArgumentNullException.ThrowIfNull(info); - - var items = info.GetValue(nameof(_database)); - foreach (var item in items) - { - _database.Add(item[0], [.. item[1..].Select(p => new SecurityIdentifier(p))]); - } - } - - /// - public void Serialize(IXunitSerializationInfo info) - { - ArgumentNullException.ThrowIfNull(info); - - // Flatten the map into an array of arrays composed of the principal and their security ids. - var data = _database.Select(p => - { - string[] items = [p.Key, ..p.Value.Select(x => x.Value)]; - return items; - }).ToArray(); - - info.AddValue(nameof(_database), data); - } - /// public override string ToString() => $"{string.Join(" | ", _database.Select(p => $"{p.Key}: {string.Join(',', p.Value)}"))}"; } \ No newline at end of file diff --git a/src/Tests/MockUserRightsManager.cs b/src/Tests/MockUserRightsManager.cs index df92593..2d3f204 100644 --- a/src/Tests/MockUserRightsManager.cs +++ b/src/Tests/MockUserRightsManager.cs @@ -5,7 +5,7 @@ namespace Tests; using UserRights.Application; /// -/// Represents a mock user rights manager with noop interface implementations. +/// Represents a mock user rights manager with noop interface implementations for testing the CLI. /// public class MockUserRightsManager : IUserRightsManager { diff --git a/src/Tests/RunWhenElevatedAttribute.cs b/src/Tests/RunWhenElevatedAttribute.cs new file mode 100644 index 0000000..e3ad24e --- /dev/null +++ b/src/Tests/RunWhenElevatedAttribute.cs @@ -0,0 +1,32 @@ +namespace Tests; + +/// +/// Represents an attribute used to mark a test method that requires elevated privileges. +/// +public sealed class RunWhenElevatedAttribute : ConditionBaseAttribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The ignore message indicating the reason for ignoring the test method or test class. + public RunWhenElevatedAttribute(string ignoreMessage = "Test requires that the current principal be a member of the Administrators group.") + : base(ConditionMode.Include) => IgnoreMessage = ignoreMessage; + + /// + public override bool IsConditionMet => GetAdministratorStatus(); + + /// + public override string GroupName => "RunWhenElevated"; + + /// + /// Gets a value indicating whether the current process is running with elevated (administrator) privileges. + /// + /// if the current process is running with elevated (administrator) privileges, otherwise . + private static bool GetAdministratorStatus() + { + using var identity = System.Security.Principal.WindowsIdentity.GetCurrent(); + var principal = new System.Security.Principal.WindowsPrincipal(identity); + + return principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator); + } +} \ No newline at end of file diff --git a/src/Tests/TestData.cs b/src/Tests/TestData.cs index 16bd207..34b9411 100644 --- a/src/Tests/TestData.cs +++ b/src/Tests/TestData.cs @@ -38,7 +38,7 @@ public static class TestData public static readonly string PrincipalName2 = "BUILTIN\\Users"; /// - /// Represents the security identity for the built in local users group. + /// Represents the security identity for the built-in local users group. /// public static readonly SecurityIdentifier PrincipalSid2 = new("S-1-5-32-545"); @@ -48,7 +48,7 @@ public static class TestData public static readonly string PrincipalName3 = "BUILTIN\\Guests"; /// - /// Represents the security identity for the built in local guests group. + /// Represents the security identity for the built-in local guests group. /// public static readonly SecurityIdentifier PrincipalSid3 = new("S-1-5-32-546"); } \ No newline at end of file diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj index fb01afb..7e2cf0a 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -1,4 +1,4 @@ - + net10.0-windows @@ -6,14 +6,22 @@ enable latest latest-All + All + false + false - + + + + + + \ No newline at end of file diff --git a/src/Tests/UserRightEntryEqualityComparer.cs b/src/Tests/UserRightEntryEqualityComparer.cs deleted file mode 100644 index ab50afd..0000000 --- a/src/Tests/UserRightEntryEqualityComparer.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace Tests; - -using UserRights.Application; - -/// -/// Represents the equality comparison for objects. -/// -public sealed class UserRightEntryEqualityComparer : IEqualityComparer -{ - /// - public bool Equals(IUserRightEntry? x, IUserRightEntry? y) - { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x is null) - { - return false; - } - - if (y is null) - { - return false; - } - - if (x.GetType() != y.GetType()) - { - return false; - } - - return string.Equals(x.Privilege, y.Privilege, StringComparison.Ordinal) - && string.Equals(x.SecurityId, y.SecurityId, StringComparison.Ordinal) - && string.Equals(x.AccountName, y.AccountName, StringComparison.Ordinal); - } - - /// - public int GetHashCode(IUserRightEntry obj) - { - ArgumentNullException.ThrowIfNull(obj); - - unchecked - { - var hashCode = StringComparer.Ordinal.GetHashCode(obj.Privilege); - hashCode = (hashCode * 397) ^ StringComparer.Ordinal.GetHashCode(obj.SecurityId); - - if (obj.AccountName is not null) - { - hashCode = (hashCode * 397) ^ StringComparer.Ordinal.GetHashCode(obj.AccountName); - } - - return hashCode; - } - } -} \ No newline at end of file diff --git a/src/Tests/UserRightsManagerFixture.cs b/src/Tests/UserRightsManagerFixture.cs new file mode 100644 index 0000000..df19701 --- /dev/null +++ b/src/Tests/UserRightsManagerFixture.cs @@ -0,0 +1,66 @@ +namespace Tests; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using UserRights.Application; + +/// +/// Represents a test fixture with the implementation. +/// +public class UserRightsManagerFixture : IDisposable +{ + private readonly ServiceProvider _serviceProvider; + + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + public UserRightsManagerFixture() + { + var serviceCollection = new ServiceCollection() + .AddLogging(builder => builder + .ClearProviders() + .SetMinimumLevel(LogLevel.Trace) + .AddDebug()); + + serviceCollection.AddSingleton(); + + _serviceProvider = serviceCollection.BuildServiceProvider(); + } + + /// + /// Gets an instance of a implementation. + /// + public IUserRightsManager UserRightsManager => + _disposed + ? throw new ObjectDisposedException(GetType().FullName) + : _serviceProvider.GetRequiredService(); + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases resources when they are no longer required. + /// + /// A value indicating whether the method call comes from a dispose method (its value is ) or from a finalizer (its value is ). + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _serviceProvider.Dispose(); + } + + _disposed = true; + } +} \ No newline at end of file diff --git a/src/UserRights.Application/UserRightEntry.cs b/src/UserRights.Application/UserRightEntry.cs index 121fb4c..792a93b 100644 --- a/src/UserRights.Application/UserRightEntry.cs +++ b/src/UserRights.Application/UserRightEntry.cs @@ -38,4 +38,25 @@ public UserRightEntry(string privilege, string securityId, string? accountName) /// The account name may be empty if the query was performed remotely due to the translation possibly not working. /// public string? AccountName { get; } + + /// + public virtual bool Equals(UserRightEntry? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return string.Equals(Privilege, other.Privilege, StringComparison.Ordinal) + && string.Equals(SecurityId, other.SecurityId, StringComparison.Ordinal) + && string.Equals(AccountName, other.AccountName, StringComparison.Ordinal); + } + + /// + public override int GetHashCode() => HashCode.Combine(Privilege, SecurityId, AccountName); } \ No newline at end of file diff --git a/src/global.json b/src/global.json index 5f2cbb2..ed89368 100644 --- a/src/global.json +++ b/src/global.json @@ -2,5 +2,11 @@ "sdk": { "version": "10.0.100", "rollForward": "latestFeature" + }, + "msbuild-sdks": { + "MSTest.Sdk": "4.0.2" + }, + "test": { + "runner": "Microsoft.Testing.Platform" } } \ No newline at end of file diff --git a/src/testconfig.json b/src/testconfig.json new file mode 100644 index 0000000..847daea --- /dev/null +++ b/src/testconfig.json @@ -0,0 +1,22 @@ +{ + "codeCoverage": { + "Configuration": { + "Format": "cobertura", + "CodeCoverage": { + "EnableDynamicNativeInstrumentation": false, + "EnableStaticNativeInstrumentation": false, + "Attributes": { + "Exclude": [ + "^System\\.CodeDom\\.Compiler\\.GeneratedCodeAttribute$" + ] + }, + "Sources": { + "Exclude": [ + ".*\\.g\\.cs$", + ".*\\\\Test.*\\.cs$" + ] + } + } + } + } +} \ No newline at end of file From c47985b46933a52c678aaefb58d9a7d9c459cfca Mon Sep 17 00:00:00 2001 From: "Joseph L. Casale" <9957114+jcasale@users.noreply.github.com> Date: Sat, 8 Nov 2025 16:03:26 -0700 Subject: [PATCH 14/15] Adds support for pre-release info to git tag parser. --- src/Directory.Build.props | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 3cb3bd1..c85dca0 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -25,11 +25,13 @@ - ^v((.+)-(\d+)-g(.+)) - $([System.Text.RegularExpressions.Regex]::Match($(GitTag), $(PatternGitTag)).Groups[2].Value) - $([System.Text.RegularExpressions.Regex]::Match($(GitTag), $(PatternGitTag)).Groups[2].Value) - $([System.Text.RegularExpressions.Regex]::Match($(GitTag), $(PatternGitTag)).Groups[2].Value) - $([System.Text.RegularExpressions.Regex]::Match($(GitTag), $(PatternGitTag)).Groups[1].Value) + ^v(((\d+\.\d+\.\d+\.\d+)(?:-[a-z]+\.\d+)?)-(\d+-g.+)) + $([System.Text.RegularExpressions.Regex]::Match($(GitTag), $(PatternGitTag)).Groups[3].Value) + $([System.Text.RegularExpressions.Regex]::Match($(GitTag), $(PatternGitTag)).Groups[3].Value) + $([System.Text.RegularExpressions.Regex]::Match($(GitTag), $(PatternGitTag)).Groups[3].Value) + $([System.Text.RegularExpressions.Regex]::Match($(GitTag), $(PatternGitTag)).Groups[2].Value) + $([System.Text.RegularExpressions.Regex]::Match($(GitTag), $(PatternGitTag)).Groups[4].Value) + $(FullVersion)+$(GitCommitInfo) From 93e7af1346b13e6c984a5de8e672b869c07996a2 Mon Sep 17 00:00:00 2001 From: "Joseph L. Casale" <9957114+jcasale@users.noreply.github.com> Date: Sun, 9 Nov 2025 07:47:01 -0700 Subject: [PATCH 15/15] Refactors and improves tests. --- .github/workflows/main.yml | 2 +- .github/workflows/unit-tests.yml | 3 +- src/Directory.Packages.props | 2 + src/Tests.Application/.editorconfig | 4 +- .../LsaUserRightsConnectTests.cs | 25 - .../LsaUserRightsDisposeTests.cs | 33 - .../LsaUserRightsGetPrincipalsTests.cs | 93 --- .../LsaUserRightsGrantPrivilegeTests.cs | 63 -- .../LsaUserRightsRevokePrivilegeTests.cs | 64 -- src/Tests.Application/LsaUserRightsTests.cs | 211 +++++++ src/Tests.Application/MSTestSettings.cs | 2 +- .../UserRightsManagerListTests.cs | 50 +- .../UserRightsManagerPrincipalTests.cs | 449 ++++++++------ .../UserRightsManagerPrivilegeTests.cs | 577 +++++++++++------- src/Tests.Cli/.editorconfig | 3 + src/Tests.Cli/CliCommandTests.cs | 569 +++++++++++++++++ src/Tests.Cli/CliSyntaxTests.cs | 568 +++++++++++++++++ src/Tests.Cli/ListCommandTests.cs | 165 ----- src/Tests.Cli/ListSyntaxTests.cs | 98 --- src/Tests.Cli/PrincipalCommandTests.cs | 259 -------- src/Tests.Cli/PrincipalSyntaxTests.cs | 260 -------- src/Tests.Cli/PrivilegeCommandTests.cs | 377 ------------ src/Tests.Cli/PrivilegeSyntaxTests.cs | 377 ------------ src/Tests.Cli/Tests.Cli.csproj | 5 + ...CliBuilderFixture.cs => CliMockBuilder.cs} | 85 +-- src/Tests/LsaUserRightsMockBuilder.cs | 167 +++++ src/Tests/LsaUserRightsSnapshotFixture.cs | 131 ++-- src/Tests/MockLsaUserRights.cs | 139 ----- src/Tests/MockUserRightsManager.cs | 24 - src/Tests/Tests.csproj | 5 + 30 files changed, 2239 insertions(+), 2571 deletions(-) delete mode 100644 src/Tests.Application/LsaUserRightsConnectTests.cs delete mode 100644 src/Tests.Application/LsaUserRightsDisposeTests.cs delete mode 100644 src/Tests.Application/LsaUserRightsGetPrincipalsTests.cs delete mode 100644 src/Tests.Application/LsaUserRightsGrantPrivilegeTests.cs delete mode 100644 src/Tests.Application/LsaUserRightsRevokePrivilegeTests.cs create mode 100644 src/Tests.Application/LsaUserRightsTests.cs create mode 100644 src/Tests.Cli/.editorconfig create mode 100644 src/Tests.Cli/CliCommandTests.cs create mode 100644 src/Tests.Cli/CliSyntaxTests.cs delete mode 100644 src/Tests.Cli/ListCommandTests.cs delete mode 100644 src/Tests.Cli/ListSyntaxTests.cs delete mode 100644 src/Tests.Cli/PrincipalCommandTests.cs delete mode 100644 src/Tests.Cli/PrincipalSyntaxTests.cs delete mode 100644 src/Tests.Cli/PrivilegeCommandTests.cs delete mode 100644 src/Tests.Cli/PrivilegeSyntaxTests.cs rename src/Tests/{CliBuilderFixture.cs => CliMockBuilder.cs} (54%) create mode 100644 src/Tests/LsaUserRightsMockBuilder.cs delete mode 100644 src/Tests/MockLsaUserRights.cs delete mode 100644 src/Tests/MockUserRightsManager.cs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 636eef3..68bc0bb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,7 +45,7 @@ jobs: - name: Run Application tests run: | cd .\src - dotnet.exe test --project .\Tests.Application\Tests.Application.csproj --configuration Debug --runtime win-x64 --output Detailed --max-parallel-test-modules 1 + dotnet.exe test --project .\Tests.Application\Tests.Application.csproj --configuration Debug --runtime win-x64 --output Detailed - name: Clean solution run: dotnet.exe clean .\src\UserRights.slnx --configuration Debug diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index c17f6cb..bff753c 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -59,8 +59,7 @@ jobs: --diagnostic-output-directory diags ` --report-trx ` --coverage ` - --coverage-settings .\testconfig.json ` - --max-parallel-test-modules 1 + --coverage-settings .\testconfig.json - name: Run report generator if: always() diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index a8209bf..69bd126 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -11,6 +11,8 @@ + + diff --git a/src/Tests.Application/.editorconfig b/src/Tests.Application/.editorconfig index 250fa43..26f6cee 100644 --- a/src/Tests.Application/.editorconfig +++ b/src/Tests.Application/.editorconfig @@ -1,5 +1,5 @@ -[LsaUserRightsDisposeTests.cs] -dotnet_diagnostic.CA1031.severity = none # Do not catch general exception types. +[*.cs] +dotnet_diagnostic.CA1707.severity = none # Identifiers should not contain underscores. [UserRightsManagerPrincipalTests.cs] dotnet_diagnostic.SA1515.severity = none # Single-line comment should be preceded by blank line. diff --git a/src/Tests.Application/LsaUserRightsConnectTests.cs b/src/Tests.Application/LsaUserRightsConnectTests.cs deleted file mode 100644 index 73a1411..0000000 --- a/src/Tests.Application/LsaUserRightsConnectTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Tests.Application; - -using UserRights.Application; - -/// -/// Represents tests for connection functionality. -/// -[TestClass] -public class LsaUserRightsConnectTests -{ - /// - /// Tests that only a single connection to the local security authority is allowed. - /// - [TestMethod] - [RunWhenElevated] - public void MultipleConnectionsThrowsException() - { - // Arrange. - using var policy = new LsaUserRights(); - policy.Connect(); - - // Act & Assert. - Assert.Throws(() => policy.Connect()); - } -} \ No newline at end of file diff --git a/src/Tests.Application/LsaUserRightsDisposeTests.cs b/src/Tests.Application/LsaUserRightsDisposeTests.cs deleted file mode 100644 index cd2a8db..0000000 --- a/src/Tests.Application/LsaUserRightsDisposeTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Tests.Application; - -using UserRights.Application; - -/// -/// Represents tests for disposal functionality. -/// -[TestClass] -public class LsaUserRightsDisposeTests -{ - /// - /// Tests whether dispose can be successfully called multiple times. - /// - [TestMethod] - [RunWhenElevated] - public void CanBeDisposedMultipleTimes() - { - // Arrange. - var policy = new LsaUserRights(); - - policy.Dispose(); - - // Act & Assert. - try - { - policy.Dispose(); - } - catch (Exception e) - { - Assert.Fail($"Multiple calls to Dispose() should not fail: {e}"); - } - } -} \ No newline at end of file diff --git a/src/Tests.Application/LsaUserRightsGetPrincipalsTests.cs b/src/Tests.Application/LsaUserRightsGetPrincipalsTests.cs deleted file mode 100644 index 05ef7a9..0000000 --- a/src/Tests.Application/LsaUserRightsGetPrincipalsTests.cs +++ /dev/null @@ -1,93 +0,0 @@ -namespace Tests.Application; - -using System.Security.Principal; - -using UserRights.Application; - -using static Tests.PrivilegeConstants; -using static Tests.SecurityIdentifierConstants; - -/// -/// Represents tests for list functionality. -/// -[TestClass] -[DoNotParallelize] -public sealed class LsaUserRightsGetPrincipalsTests : LsaUserRightsSnapshotFixture -{ - /// - /// Tests listing all the principals assigned to all privileges. - /// - [TestMethod] - [RunWhenElevated] - public void GetPrincipalsShouldWork() - { - // Arrange. - var expected = InitialState.Values - .SelectMany(p => p) - .Distinct() - .Order() - .ToArray(); - - using var policy = new LsaUserRights(); - policy.Connect(); - - // Act. - var actual = policy.LsaEnumerateAccountsWithUserRight() - .Order() - .ToArray(); - - // Assert. - CollectionAssert.AreEqual(expected, actual); - } - - /// - /// Tests listing the principals assigned to a single privilege. - /// - /// - /// The test verifies that the BUILTIN\Administrators group is assigned the SeTakeOwnershipPrivilege user right. - /// - [TestMethod] - [RunWhenElevated] - public void GetPrincipalsSinglePrivilegeShouldWork() - { - // Arrange. - var securityIdentifier = new SecurityIdentifier(Administrators); - - using var policy = new LsaUserRights(); - policy.Connect(); - - // Act. - var collection = policy.LsaEnumerateAccountsWithUserRight(SeTakeOwnershipPrivilege); - - // Assert. - Assert.Contains(securityIdentifier, collection); - } - - /// - /// Tests listing all the principals assigned to all privileges without connecting throws an exception. - /// - [TestMethod] - [RunWhenElevated] - public void GetPrincipalsWithoutConnectingThrowsException() - { - // Arrange. - using var policy = new LsaUserRights(); - - // Act & Assert. - Assert.Throws(() => policy.LsaEnumerateAccountsWithUserRight()); - } - - /// - /// Tests listing the principals assigned to a single privilege without connecting throws an exception. - /// - [TestMethod] - [RunWhenElevated] - public void GetPrincipalsSinglePrivilegeWithoutConnectingThrowsException() - { - // Arrange. - using var policy = new LsaUserRights(); - - // Act & Assert. - Assert.Throws(() => policy.LsaEnumerateAccountsWithUserRight(SeTakeOwnershipPrivilege)); - } -} \ No newline at end of file diff --git a/src/Tests.Application/LsaUserRightsGrantPrivilegeTests.cs b/src/Tests.Application/LsaUserRightsGrantPrivilegeTests.cs deleted file mode 100644 index efb678b..0000000 --- a/src/Tests.Application/LsaUserRightsGrantPrivilegeTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace Tests.Application; - -using System.Security.Principal; - -using UserRights.Application; - -using static Tests.PrivilegeConstants; -using static Tests.SecurityIdentifierConstants; - -/// -/// Represents tests for grant functionality. -/// -[TestClass] -[DoNotParallelize] -public sealed class LsaUserRightsGrantPrivilegeTests : LsaUserRightsSnapshotFixture -{ - /// - /// Tests granting a privilege. - /// - /// - /// The test verifies that the BUILTIN\Users group is not assigned the SeTakeOwnershipPrivilege user right. - /// - [TestMethod] - [RunWhenElevated] - public void GrantPrivilegeShouldWork() - { - // Arrange. - var securityIdentifier = new SecurityIdentifier(Users); - - InitialState.TryGetValue(SeMachineAccountPrivilege, out var initial); - Assert.DoesNotContain(securityIdentifier, initial ?? []); - - // Act. - using var policy = new LsaUserRights(); - policy.Connect(); - policy.LsaAddAccountRights(securityIdentifier, SeMachineAccountPrivilege); - - var current = GetCurrentState(); - - current.TryGetValue(SeMachineAccountPrivilege, out var collection); - - // Assert. - Assert.IsNotNull(collection); - Assert.Contains(securityIdentifier, collection); - } - - /// - /// Tests granting a privilege without connecting throws an exception. - /// - [TestMethod] - [RunWhenElevated] - public void GrantPrivilegeWithoutConnectingThrowsException() - { - // Arrange. - var securityIdentifier = new SecurityIdentifier(Users); - - // Act. - using var policy = new LsaUserRights(); - - // Assert. - Assert.Throws(() => policy.LsaAddAccountRights(securityIdentifier, SeMachineAccountPrivilege)); - } -} \ No newline at end of file diff --git a/src/Tests.Application/LsaUserRightsRevokePrivilegeTests.cs b/src/Tests.Application/LsaUserRightsRevokePrivilegeTests.cs deleted file mode 100644 index dc91360..0000000 --- a/src/Tests.Application/LsaUserRightsRevokePrivilegeTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace Tests.Application; - -using System.Security.Principal; - -using UserRights.Application; - -using static Tests.PrivilegeConstants; -using static Tests.SecurityIdentifierConstants; - -/// -/// Represents tests for revoke functionality. -/// -[TestClass] -[DoNotParallelize] -public sealed class LsaUserRightsRevokePrivilegeTests : LsaUserRightsSnapshotFixture -{ - /// - /// Tests revoking a privilege. - /// - /// - /// The test requires that the BUILTIN\Backup Operators group is assigned the SeBackupPrivilege user right. - /// - [TestMethod] - [RunWhenElevated] - public void RevokePrivilegeShouldWork() - { - // Arrange. - var securityIdentifier = new SecurityIdentifier(BackupOperators); - - InitialState.TryGetValue(SeBackupPrivilege, out var initial); - - Assert.IsNotNull(initial); - Assert.Contains(securityIdentifier, initial); - - // Act. - using var policy = new LsaUserRights(); - policy.Connect(); - policy.LsaRemoveAccountRights(securityIdentifier, SeBackupPrivilege); - - var current = GetCurrentState(); - - current.TryGetValue(SeBackupPrivilege, out var collection); - - // Assert. - Assert.DoesNotContain(securityIdentifier, collection ?? []); - } - - /// - /// Tests revoking a privilege without connecting throws an exception. - /// - [TestMethod] - [RunWhenElevated] - public void RevokePrivilegeWithoutConnectingThrowsException() - { - // Arrange. - var securityIdentifier = new SecurityIdentifier(BackupOperators); - - // Act. - using var policy = new LsaUserRights(); - - // Assert. - Assert.Throws(() => policy.LsaRemoveAccountRights(securityIdentifier, SeBackupPrivilege)); - } -} \ No newline at end of file diff --git a/src/Tests.Application/LsaUserRightsTests.cs b/src/Tests.Application/LsaUserRightsTests.cs new file mode 100644 index 0000000..9fb5603 --- /dev/null +++ b/src/Tests.Application/LsaUserRightsTests.cs @@ -0,0 +1,211 @@ +namespace Tests.Application; + +using System.Security.Cryptography; +using System.Security.Principal; + +using UserRights.Application; + +using static Tests.PrivilegeConstants; +using static Tests.SecurityIdentifierConstants; + +/// +/// Represents tests for interacting with the local security authority (LSA) database. +/// +[TestClass] +public class LsaUserRightsTests +{ + /// + /// Verifies connecting more than once will throw an exception. + /// + [TestMethod] + [RunWhenElevated] + public void Connect_WithConnectingMultipleTimes_ThrowsException() + { + // Arrange. + using var policy = new LsaUserRights(); + policy.Connect(); + + // Act & Assert. + Assert.Throws(() => policy.Connect()); + } + + /// + /// Verifies adding account rights works as expected. + /// + [TestMethod] + [RunWhenElevated] + public void LsaAddAccountRights_WithAccountAndPrivilege_ShouldWork() + { + // Arrange. + using var fixture = new LsaUserRightsSnapshotFixture(); + + var securityIdentifier = new SecurityIdentifier(Users); + + // Select a random user right that has not been assigned to the BUILTIN\Users group from the initial state. + var (right, existingAccounts) = fixture.InitialState.First(kvp => !kvp.Value.Contains(securityIdentifier)); + + Assert.DoesNotContain(securityIdentifier, existingAccounts); + + // Act. + using var policy = new LsaUserRights(); + policy.Connect(); + policy.LsaAddAccountRights(securityIdentifier, right); + + var current = fixture.GetCurrentState(); + + current.TryGetValue(right, out var updatedAccounts); + + // Assert. + Assert.IsNotNull(updatedAccounts); + Assert.Contains(securityIdentifier, updatedAccounts); + } + + /// + /// Verifies adding account rights without first connecting will throw an exception. + /// + [TestMethod] + [RunWhenElevated] + public void LsaAddAccountRights_WithAccountAndPrivilegeAndWithoutConnecting_ThrowsException() + { + // Arrange. + using var fixture = new LsaUserRightsSnapshotFixture(); + + var securityIdentifier = new SecurityIdentifier(Users); + + // Act. + using var policy = new LsaUserRights(); + + // Assert. + Assert.Throws(() => policy.LsaAddAccountRights(securityIdentifier, SeMachineAccountPrivilege)); + } + + /// + /// Verifies enumerating all accounts with user rights works as expected. + /// + [TestMethod] + [RunWhenElevated] + [DoNotParallelize] + public void LsaEnumerateAccountsWithUserRight_ShouldWork() + { + // Arrange. + using var fixture = new LsaUserRightsSnapshotFixture(); + + var expected = fixture.InitialState.Values + .SelectMany(p => p) + .Distinct() + .Order() + .ToArray(); + + using var policy = new LsaUserRights(); + policy.Connect(); + + // Act. + var actual = policy.LsaEnumerateAccountsWithUserRight() + .Order() + .ToArray(); + + // Assert. + CollectionAssert.AreEqual(expected, actual); + } + + /// + /// Verifies enumerating all accounts with user rights without first connecting will throw an exception. + /// + [TestMethod] + [RunWhenElevated] + public void LsaEnumerateAccountsWithUserRight_WithoutConnecting_ThrowsException() + { + // Arrange. + using var policy = new LsaUserRights(); + + // Act & Assert. + Assert.Throws(() => policy.LsaEnumerateAccountsWithUserRight()); + } + + /// + /// Verifies enumerating all accounts with a specific user right works as expected. + /// + [TestMethod] + [RunWhenElevated] + public void LsaEnumerateAccountsWithUserRight_WithPrivilege_ShouldWork() + { + // Arrange. + using var fixture = new LsaUserRightsSnapshotFixture(); + + // Select a random user right and the assigned accounts from the initial state. + var (right, expected) = fixture.InitialState.ElementAt(RandomNumberGenerator.GetInt32(fixture.InitialState.Count)); + + using var policy = new LsaUserRights(); + policy.Connect(); + + // Act. + var collection = policy.LsaEnumerateAccountsWithUserRight(right); + + // Assert. + CollectionAssert.AreEquivalent(expected.ToArray(), collection); + } + + /// + /// Verifies enumerating accounts with a specific user right without first connecting will throw an exception. + /// + [TestMethod] + [RunWhenElevated] + public void LsaEnumerateAccountsWithUserRight_WithPrivilegeAndWithoutConnecting_ThrowsException() + { + // Arrange. + using var policy = new LsaUserRights(); + + // Act & Assert. + Assert.Throws(() => policy.LsaEnumerateAccountsWithUserRight(SeTakeOwnershipPrivilege)); + } + + /// + /// Verifies removing a user right from an account works as expected. + /// + [TestMethod] + [RunWhenElevated] + public void LsaRemoveAccountRights_WithAccountAndPrivilege_ShouldWork() + { + // Arrange. + using var fixture = new LsaUserRightsSnapshotFixture(); + + var securityIdentifier = new SecurityIdentifier(BackupOperators); + + // Select a random user right that has been assigned to the BUILTIN\Backup Operators group from the initial state. + var (right, existingAccounts) = fixture.InitialState.First(kvp => kvp.Value.Contains(securityIdentifier)); + + Assert.Contains(securityIdentifier, existingAccounts); + + using var policy = new LsaUserRights(); + policy.Connect(); + + // Act. + policy.LsaRemoveAccountRights(securityIdentifier, right); + + var current = fixture.GetCurrentState(); + + current.TryGetValue(right, out var updatedAccounts); + + // Assert. + Assert.DoesNotContain(securityIdentifier, updatedAccounts ?? []); + } + + /// + /// Verifies removing a user right from an account without first connecting will throw an exception. + /// + [TestMethod] + [RunWhenElevated] + public void LsaRemoveAccountRights_WithoutConnecting_ThrowsException() + { + // Arrange. + using var fixture = new LsaUserRightsSnapshotFixture(); + + var securityIdentifier = new SecurityIdentifier(BackupOperators); + + // Act. + using var policy = new LsaUserRights(); + + // Assert. + Assert.Throws(() => policy.LsaRemoveAccountRights(securityIdentifier, SeBackupPrivilege)); + } +} \ No newline at end of file diff --git a/src/Tests.Application/MSTestSettings.cs b/src/Tests.Application/MSTestSettings.cs index a41798c..6b35270 100644 --- a/src/Tests.Application/MSTestSettings.cs +++ b/src/Tests.Application/MSTestSettings.cs @@ -1 +1 @@ -[assembly: DoNotParallelize] \ No newline at end of file +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] \ No newline at end of file diff --git a/src/Tests.Application/UserRightsManagerListTests.cs b/src/Tests.Application/UserRightsManagerListTests.cs index 3dd57b4..bb913d9 100644 --- a/src/Tests.Application/UserRightsManagerListTests.cs +++ b/src/Tests.Application/UserRightsManagerListTests.cs @@ -1,58 +1,38 @@ namespace Tests.Application; -using System.Security.Principal; - using UserRights.Application; -using UserRights.Extensions.Security; using static Tests.TestData; /// -/// Represents tests for list functionality. +/// Represents tests for enumerating all user rights. /// [TestClass] public class UserRightsManagerListTests { /// - /// Verifies listing user rights. + /// Verifies enumerating all user rights works as expected. /// [TestMethod] - public void SerializingToCsvShouldWork() + public void GetUserRights_ShouldWork() { // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var expected = database - .SelectMany(kvp => kvp.Value.Select(p => new UserRightEntry(kvp.Key, p.Value, p.ToAccount().Value))) - .ToArray(); - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + UserRightEntry[] expected = + [ + new(Privilege1, PrincipalSid1.Value, PrincipalName1), + new(Privilege1, PrincipalSid2.Value, PrincipalName2), + new(Privilege2, PrincipalSid2.Value, PrincipalName2), + new(Privilege2, PrincipalSid3.Value, PrincipalName3) + ]; + + var lsaUserRights = LsaUserRightsMockBuilder.CreateBuilder() + .WithGrant(expected) + .Build(); using var fixture = new UserRightsManagerFixture(); // Act. - var actual = fixture.UserRightsManager.GetUserRights(policy).ToArray(); + var actual = fixture.UserRightsManager.GetUserRights(lsaUserRights.Object).ToArray(); // Assert. CollectionAssert.AreEquivalent(expected, actual); diff --git a/src/Tests.Application/UserRightsManagerPrincipalTests.cs b/src/Tests.Application/UserRightsManagerPrincipalTests.cs index 7874fba..ac2786a 100644 --- a/src/Tests.Application/UserRightsManagerPrincipalTests.cs +++ b/src/Tests.Application/UserRightsManagerPrincipalTests.cs @@ -2,194 +2,233 @@ namespace Tests.Application; using System.Security.Principal; -using UserRights.Application; +using Moq; using static Tests.TestData; /// -/// Represents tests for modify principal functionality. +/// Represents tests for modifying the user rights for a specified principal. /// [TestClass] public class UserRightsManagerPrincipalTests { /// - /// Gets invalid method arguments for the unit test. + /// Gets invalid method arguments for the modify principal unit test. /// - public static IEnumerable<(ILsaUserRights Policy, string Principal, string[] Grants, string[] Revocations, bool RevokeAll, bool RevokeOthers, bool DryRun)> InvalidArgumentData + public static IEnumerable<(string Principal, string[] Grants, string[] Revocations, bool RevokeAll, bool RevokeOthers, bool DryRun)> InvalidArgumentData => + [ + // Verify null or empty principal. + new(null!, [Privilege1], [], false, false, false), + new(string.Empty, [Privilege1], [], false, false, false), + + // Verify null grant collection. + new(PrincipalName1, null!, [Privilege1], false, false, false), + + // Verify null revocation collection. + new(PrincipalName1, [Privilege1], null!, false, false, false), + + // Verify RevokeAll requirements. + new(PrincipalName1, [Privilege1], [], true, false, false), + new(PrincipalName1, [], [Privilege1], true, false, false), + new(PrincipalName1, [], [], true, true, false), + + // Verify RevokeOthers requirements. + new(PrincipalName1, [Privilege1], [], true, true, false), + new(PrincipalName1, [], [], false, true, false), + new(PrincipalName1, [Privilege1], [Privilege2], false, true, false), + + // Verify remaining requirements. + new(PrincipalName1, [], [], false, false, false), + + // Verify grant and revocation set restrictions. + new(PrincipalName1, [Privilege1], [Privilege1], false, false, false), + new(PrincipalName1, [Privilege1, Privilege1], [], false, false, false), + new(PrincipalName1, [], [Privilege1, Privilege1], false, false, false) + ]; + + /// + /// Verifies granting a privilege works as expected. + /// + [TestMethod] + public void ModifyPrincipal_WithGrant_ShouldWork() { - get - { - var policy = new MockLsaUserRights( - new Dictionary>(StringComparer.InvariantCultureIgnoreCase) - { - { "joey", new List { PrincipalSid1 } } - }); - - return - [ - // Verify null policy instance. - new(null!, PrincipalName1, [Privilege1], [], false, false, false), - - // Verify null or empty principal. - new(policy, null!, [Privilege1], [], false, false, false), - new(policy, string.Empty, [Privilege1], [], false, false, false), - - // Verify null grant collection. - new(policy, PrincipalName1, null!, [Privilege1], false, false, false), - - // Verify null revocation collection. - new(policy, PrincipalName1, [Privilege1], null!, false, false, false), - - // Verify RevokeAll requirements. - new(policy, PrincipalName1, [Privilege1], [], true, false, false), - new(policy, PrincipalName1, [], [Privilege1], true, false, false), - new(policy, PrincipalName1, [], [], true, true, false), - - // Verify RevokeOthers requirements. - new(policy, PrincipalName1, [Privilege1], [], true, true, false), - new(policy, PrincipalName1, [], [], false, true, false), - new(policy, PrincipalName1, [Privilege1], [Privilege2], false, true, false), - - // Verify remaining requirements. - new(policy, PrincipalName1, [], [], false, false, false), - - // Verify grant and revocation set restrictions. - new(policy, PrincipalName1, [Privilege1], [Privilege1], false, false, false), - new(policy, PrincipalName1, [Privilege1, Privilege1], [], false, false, false), - new(policy, PrincipalName1, [], [Privilege1, Privilege1], false, false, false) - ]; - } + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); + + using var fixture = new UserRightsManagerFixture(); + + // Act. + fixture.UserRightsManager.ModifyPrincipal(lsaUserRights.Object, PrincipalName1, [Privilege2], [], false, false, false); + + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid1)), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaAddAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); } /// - /// Verifies granting a privilege to a principal and revoking their other privileges is successful and does not modify other assignments. + /// Verifies granting a privilege with dry run enabled works as expected. /// [TestMethod] - public void GrantAndRevokeOthersShouldWork() + public void ModifyPrincipal_WithGrantAndDryRun_ShouldWork() { // Arrange. - var principals1 = new List - { - PrincipalSid1 - }; + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); - var principals2 = new List - { - PrincipalSid2 - }; + using var fixture = new UserRightsManagerFixture(); - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; + // Act. + fixture.UserRightsManager.ModifyPrincipal(lsaUserRights.Object, PrincipalName1, [Privilege2], [], false, false, true); - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid1)), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies granting and revoking a privilege from a principal works as expected. + /// + [TestMethod] + public void ModifyPrincipal_WithGrantAndRevoke_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); using var fixture = new UserRightsManagerFixture(); // Act. - fixture.UserRightsManager.ModifyPrincipal(policy, PrincipalName1, [Privilege2], [], false, true, false); + fixture.UserRightsManager.ModifyPrincipal(lsaUserRights.Object, PrincipalName1, [Privilege2], [Privilege1], false, false, false); // Assert. - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid1)), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaAddAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); } /// - /// Verifies a single grant with a single revoke is successful and does not modify other assignments. + /// Verifies granting and revoking a privilege from a principal with dry run enabled works as expected. /// [TestMethod] - public void GrantAndRevokeShouldWork() + public void ModifyPrincipal_WithGrantAndRevokeAndDryRun_ShouldWork() { // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var principals2 = new List - { - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); using var fixture = new UserRightsManagerFixture(); // Act. - fixture.UserRightsManager.ModifyPrincipal(policy, PrincipalName1, [Privilege2], [Privilege1], false, false, false); + fixture.UserRightsManager.ModifyPrincipal(lsaUserRights.Object, PrincipalName1, [Privilege2], [Privilege1], false, false, true); // Assert. - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid1)), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); } /// - /// Verifies a single grant is successful and does not modify other assignments. + /// Verifies granting a privilege and revoking the other privileges from a principal works as expected. /// [TestMethod] - public void GrantShouldWork() + public void ModifyPrincipal_WithGrantAndRevokeOthers_ShouldWork() { // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var principals2 = new List - { - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); using var fixture = new UserRightsManagerFixture(); // Act. - fixture.UserRightsManager.ModifyPrincipal(policy, PrincipalName1, [Privilege2], [], false, false, false); + fixture.UserRightsManager.ModifyPrincipal(lsaUserRights.Object, PrincipalName1, [Privilege2], [], false, true, false); // Assert. - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid1)), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaAddAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies granting a privilege and revoking the other privileges from a principal with dry run enabled works as expected. + /// + [TestMethod] + public void ModifyPrincipal_WithGrantAndRevokeOthersAndDryRun_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); + + using var fixture = new UserRightsManagerFixture(); + + // Act. + fixture.UserRightsManager.ModifyPrincipal(lsaUserRights.Object, PrincipalName1, [Privilege2], [], false, true, true); + + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid1)), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); } /// - /// Verifies invalid arguments throw an instance of . + /// Verifies modifying a principal with a null policy argument throws an exception. + /// + [TestMethod] + public void ModifyPrincipal_WithInvalidArguments_ThrowsException() + { + // Arrange. + using var fixture = new UserRightsManagerFixture(); + + // Act & Assert. + Assert.Throws(() => fixture.UserRightsManager.ModifyPrincipal(null!, PrincipalName1, [Privilege1], [], false, false, false)); + } + + /// + /// Verifies modifying a principal with invalid arguments throws an exception. /// - /// A connection to the local security authority. /// The principal to modify. /// The privileges to grant to the principal. /// The privileges to revoke from the principal. @@ -198,97 +237,125 @@ public void GrantShouldWork() /// Enables dry-run mode. [TestMethod] [DynamicData(nameof(InvalidArgumentData))] - public void InvalidArgumentsThrowsException(ILsaUserRights policy, string principal, string[] grants, string[] revocations, bool revokeAll, bool revokeOthers, bool dryRun) + public void ModifyPrincipal_WithInvalidArguments_ThrowsException(string principal, string[] grants, string[] revocations, bool revokeAll, bool revokeOthers, bool dryRun) { // Arrange. + var lsaUserRights = LsaUserRightsMockBuilder.CreateBuilder().Build(); + using var fixture = new UserRightsManagerFixture(); // Act & Assert. - Assert.Throws(() => fixture.UserRightsManager.ModifyPrincipal(policy, principal, grants, revocations, revokeAll, revokeOthers, dryRun)); + Assert.Throws(() => fixture.UserRightsManager.ModifyPrincipal(lsaUserRights.Object, principal, grants, revocations, revokeAll, revokeOthers, dryRun)); + + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies revoking a privilege from a principal works as expected. + /// + [TestMethod] + public void ModifyPrincipal_WithRevoke_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); + + using var fixture = new UserRightsManagerFixture(); + + // Act. + fixture.UserRightsManager.ModifyPrincipal(lsaUserRights.Object, PrincipalName2, [], [Privilege2], false, false, false); + + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid2)), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid2), It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); } /// - /// Verifies a revoking all privileges for a principal is successful and does not modify other assignments. + /// Verifies revoking all privileges from a principal works as expected. /// [TestMethod] - public void RevokeAllShouldWork() + public void ModifyPrincipal_WithRevokeAll_ShouldWork() { // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); using var fixture = new UserRightsManagerFixture(); // Act. - fixture.UserRightsManager.ModifyPrincipal(policy, PrincipalName1, [], [], true, false, false); + fixture.UserRightsManager.ModifyPrincipal(lsaUserRights.Object, PrincipalName1, [], [], true, false, false); // Assert. - Assert.IsEmpty(policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEqual(new[] { PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid1)), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); } /// - /// Verifies a single revocation is successful and does not modify other assignments. + /// Verifies revoking all privileges from a principal works as expected. /// [TestMethod] - public void RevokeShouldWork() + public void ModifyPrincipal_WithRevokeAllAndDryRun_ShouldWork() { // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var principals2 = new List - { - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); using var fixture = new UserRightsManagerFixture(); // Act. - fixture.UserRightsManager.ModifyPrincipal(policy, PrincipalName2, [], [Privilege2], false, false, false); + fixture.UserRightsManager.ModifyPrincipal(lsaUserRights.Object, PrincipalName1, [], [], true, false, true); // Assert. - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid1)), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies revoking a privilege from a principal with dry run enabled works as expected. + /// + [TestMethod] + public void ModifyPrincipal_WithRevokeAndDryRun_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); + + using var fixture = new UserRightsManagerFixture(); + + // Act. + fixture.UserRightsManager.ModifyPrincipal(lsaUserRights.Object, PrincipalName2, [], [Privilege2], false, false, true); + + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid2)), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); } } \ No newline at end of file diff --git a/src/Tests.Application/UserRightsManagerPrivilegeTests.cs b/src/Tests.Application/UserRightsManagerPrivilegeTests.cs index 9a962a5..c6cd508 100644 --- a/src/Tests.Application/UserRightsManagerPrivilegeTests.cs +++ b/src/Tests.Application/UserRightsManagerPrivilegeTests.cs @@ -3,406 +3,509 @@ namespace Tests.Application; using System.Security.Principal; using System.Text.RegularExpressions; -using UserRights.Application; +using Moq; using static Tests.TestData; /// -/// Represents tests for modify privilege functionality. +/// Represents tests for modifying the principals for a specified user right. /// [TestClass] public class UserRightsManagerPrivilegeTests { /// - /// Gets invalid method arguments for the unit test. + /// Gets invalid method arguments for the modify privilege unit test. /// /// A sequence of method arguments. - public static IEnumerable<(ILsaUserRights Policy, string Privilege, string[] Grants, string[] Revocations, bool RevokeAll, bool RevokeOthers, string RevokePattern, bool DryRun)> InvalidArgumentData + public static IEnumerable<(string Privilege, string[] Grants, string[] Revocations, bool RevokeAll, bool RevokeOthers, string? RevokePattern, bool DryRun)> InvalidArgumentData { get { - var policy = new MockLsaUserRights(); const string pattern = ".*"; return [ - // Verify null policy instance. - new(null!, Privilege1, [PrincipalName1], [], false, false, null!, new(false)), - // Verify null or empty privilege. - new(policy, null!, [PrincipalName1], [], false, false, null!, new(false)), - new(policy, string.Empty, [PrincipalName1], [], false, false, null!, new(false)), + new(null!, [PrincipalName1], [], false, false, null, false), + new(string.Empty, [PrincipalName1], [], false, false, null, false), // Verify null grant collection. - new(policy, Privilege1, null!, [PrincipalName1], false, false, null!, new(false)), + new(Privilege1, null!, [PrincipalName1], false, false, null, false), // Verify null revocation collection. - new(policy, Privilege1, [PrincipalName1], null!, false, false, null!, new(false)), + new(Privilege1, [PrincipalName1], null!, false, false, null, false), // Verify RevokeAll requirements. - new(policy, Privilege1, [PrincipalName1], [], true, false, null!, new(false)), - new(policy, Privilege1, [], [PrincipalName1], true, false, null!, new(false)), - new(policy, Privilege1, [], [], true, true, null!, new(false)), - new(policy, Privilege1, [], [], true, false, pattern, new(false)), + new(Privilege1, [PrincipalName1], [], true, false, null, false), + new(Privilege1, [], [PrincipalName1], true, false, null, false), + new(Privilege1, [], [], true, true, null, false), + new(Privilege1, [], [], true, false, pattern, false), // Verify RevokeOthers requirements. - new(policy, Privilege1, [], [], false, true, null!, new(false)), - new(policy, Privilege1, [PrincipalName1], [PrincipalName2], false, true, null!, new(false)), - new(policy, Privilege2, [], [], true, true, null!, new(false)), - new(policy, Privilege1, [], [], false, true, pattern, new(false)), + new(Privilege1, [], [], false, true, null, false), + new(Privilege1, [PrincipalName1], [PrincipalName2], false, true, null, false), + new(Privilege2, [], [], true, true, null, false), + new(Privilege1, [], [], false, true, pattern, false), // Verify RevokePattern requirements. - new(policy, Privilege1, [], [PrincipalName1], false, false, pattern, new(false)), - new(policy, Privilege2, [], [], true, false, pattern, new(false)), - new(policy, Privilege2, [], [], false, true, pattern, new(false)), + new(Privilege1, [], [PrincipalName1], false, false, pattern, false), + new(Privilege2, [], [], true, false, pattern, false), + new(Privilege2, [], [], false, true, pattern, false), // Verify remaining requirements. - new(policy, Privilege1, [], [], false, false, null!, new(false)), + new(Privilege1, [], [], false, false, null, false), // Verify grant and revocation set restrictions. - new(policy, Privilege1, [PrincipalName1], [PrincipalName1], false, false, null!, new(false)), - new(policy, Privilege1, [PrincipalName1, PrincipalName1], [], false, false, null!, new(false)), - new(policy, Privilege1, [], [PrincipalName1, PrincipalName1], false, false, null!, new(false)) + new(Privilege1, [PrincipalName1], [PrincipalName1], false, false, null, false), + new(Privilege1, [PrincipalName1, PrincipalName1], [], false, false, null, false), + new(Privilege1, [], [PrincipalName1, PrincipalName1], false, false, null, false) ]; } } /// - /// Verifies granting a principal to a privilege and revoking its other principals is successful and does not modify other assignments. + /// Verifies modifying a privilege with a null policy argument throws an exception. /// [TestMethod] - public void GrantAndRevokeOthersShouldWork() + public void ModifyPrivilege_WithInvalidArguments_ThrowsException() { // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; + var lsaUserRights = LsaUserRightsMockBuilder.CreateBuilder().Build(); + using var fixture = new UserRightsManagerFixture(); - var principals2 = new List - { - PrincipalSid2 - }; + // Act & Assert. + Assert.Throws(() => fixture.UserRightsManager.ModifyPrivilege(null!, Privilege1, [PrincipalName1], [], false, false, null, false)); - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; + lsaUserRights.VerifyNoOtherCalls(); + } - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); + /// + /// Verifies modifying a privilege with invalid arguments throws an exception. + /// + /// The privilege to modify. + /// The principals to grant the privilege to. + /// The principals to revoke the privilege from. + /// Revokes all principals from the privilege. + /// Revokes all principals from the privilege excluding those being granted. + /// Revokes all principals whose SID matches the regular expression excluding those being granted. + /// Enables dry-run mode. + [TestMethod] + [DynamicData(nameof(InvalidArgumentData))] + public void ModifyPrivilege_WithInvalidArguments_ThrowsException(string privilege, string[] grants, string[] revocations, bool revokeAll, bool revokeOthers, string revokePattern, bool dryRun) + { + // Arrange. + var lsaUserRights = LsaUserRightsMockBuilder.CreateBuilder().Build(); + using var fixture = new UserRightsManagerFixture(); + var regex = string.IsNullOrWhiteSpace(revokePattern) ? null : new Regex(revokePattern, RegexOptions.None, TimeSpan.FromSeconds(1)); + + // Act & Assert. + Assert.Throws(() => fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, privilege, grants, revocations, revokeAll, revokeOthers, regex, dryRun)); + + lsaUserRights.VerifyNoOtherCalls(); + } - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege1)); - CollectionAssert.AreEqual(new[] { PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege2)); + /// + /// Verifies granting a privilege works as expected. + /// + [TestMethod] + public void ModifyPrivilege_WithGrant_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege2) + .Build(); using var fixture = new UserRightsManagerFixture(); // Act. - fixture.UserRightsManager.ModifyPrivilege(policy, Privilege2, [PrincipalName1], [], false, true, null!, false); + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege2, [PrincipalName1], [], false, false, null, false); // Assert. - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege1)); - CollectionAssert.AreEqual(new[] { PrincipalSid1 }, policy.LsaEnumerateAccountsWithUserRight(Privilege2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaAddAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); } /// - /// Verifies a single grant with a single revoke is successful and does not modify other assignments. + /// Verifies granting a privilege with dry run enabled works as expected. /// [TestMethod] - public void GrantAndRevokeShouldWork() + public void ModifyPrivilege_WithGrantAndDryRun_ShouldWork() { // Arrange. - var principals1 = new List - { - PrincipalSid1 - }; + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege2) + .Build(); - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2 - }; + using var fixture = new UserRightsManagerFixture(); - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; + // Act. + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege2, [PrincipalName1], [], false, false, null, true); - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + /// + /// Verifies granting a privilege to a principal and revoking the privilege from another principal works as expected. + /// + [TestMethod] + public void ModifyPrivilege_WithGrantAndRevoke_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege2) + .Build(); using var fixture = new UserRightsManagerFixture(); // Act. - fixture.UserRightsManager.ModifyPrivilege(policy, Privilege1, [PrincipalName2], [PrincipalName1], false, false, null!, false); + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege1, [PrincipalName2], [PrincipalName1], false, false, null, false); // Assert. - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEqual(new[] { PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaAddAccountRights(It.Is(s => s == PrincipalSid2), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); } /// - /// Verifies granting a principal to a privilege and revoking all principals matching a pattern is successful and does not modify other assignments. + /// Verifies granting a privilege to a principal and revoking the privilege from another principal with dry run enabled works as expected. /// [TestMethod] - public void GrantAndRevokePatternShouldWork() + public void ModifyPrivilege_WithGrantAndRevokeAndDryRun_ShouldWork() { // Arrange. - var principals1 = new List - { - PrincipalSidCurrent, - PrincipalSid2, - PrincipalSid3 - }; + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege2) + .Build(); - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2, - PrincipalSid3 - }; + using var fixture = new UserRightsManagerFixture(); - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; + // Act. + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege1, [PrincipalName2], [PrincipalName1], false, false, null, true); + + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid2, PrincipalSid1 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } - CollectionAssert.AreEquivalent(new[] { PrincipalSidCurrent, PrincipalSid1, PrincipalSid2, PrincipalSid3 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSidCurrent)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid3)); + /// + /// Verifies granting a privilege to a principal and revoking the privilege from all other principals works as expected. + /// + [TestMethod] + public void ModifyPrivilege_WithGrantAndRevokeOthers_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); using var fixture = new UserRightsManagerFixture(); - var pattern = new Regex("^S-1-5-21", RegexOptions.None, TimeSpan.FromSeconds(1)); // Act. - fixture.UserRightsManager.ModifyPrivilege(policy, Privilege1, [PrincipalName1], [], false, false, pattern, false); + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege2, [PrincipalName1], [], false, true, null, false); // Assert. - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid3)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid2), It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaAddAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); } /// - /// Verifies a single grant is successful and does not modify other assignments. + /// Verifies granting a privilege to a principal and revoking the privilege from all other principals with dry run enabled works as expected. /// [TestMethod] - public void GrantShouldWork() + public void ModifyPrivilege_WithGrantAndRevokeOthersAndDryRun_ShouldWork() { // Arrange. - var principals1 = new List - { - PrincipalSid1 - }; + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); - var principals2 = new List - { - PrincipalSid2 - }; + using var fixture = new UserRightsManagerFixture(); - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; + // Act. + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege2, [PrincipalName1], [], false, true, null, true); - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies granting a privilege to a principal and revoking the privilege from all principals that match a pattern works as expected. + /// + [TestMethod] + public void ModifyPrivilege_WithGrantAndRevokePattern_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSidCurrent, Privilege1) + .WithGrant(PrincipalSid1, Privilege2) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .WithGrant(PrincipalSid3, Privilege1, Privilege2) + .Build(); using var fixture = new UserRightsManagerFixture(); + var pattern = new Regex("^S-1-5-21", RegexOptions.None, TimeSpan.FromSeconds(1)); // Act. - fixture.UserRightsManager.ModifyPrivilege(policy, Privilege2, [PrincipalName1], [], false, false, null!, false); + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege1, [PrincipalName1], [], false, false, pattern, false); // Assert. - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid3]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaAddAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSidCurrent), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); } /// - /// Verifies invalid arguments throw an instance of . + /// Verifies granting a privilege to a principal and revoking the privilege from all principals that match a pattern with dry run enabled works as expected. /// - /// A connection to the local security authority. - /// The privilege to modify. - /// The principals to grant the privilege to. - /// The principals to revoke the privilege from. - /// Revokes all principals from the privilege. - /// Revokes all principals from the privilege excluding those being granted. - /// Revokes all principals whose SID matches the regular expression excluding those being granted. - /// Enables dry-run mode. [TestMethod] - [DynamicData(nameof(InvalidArgumentData))] - public void InvalidArgumentsThrowsException(ILsaUserRights policy, string privilege, string[] grants, string[] revocations, bool revokeAll, bool revokeOthers, string revokePattern, bool dryRun) + public void ModifyPrivilege_WithGrantAndRevokePatternAndDryRun_ShouldWork() { // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSidCurrent, Privilege1) + .WithGrant(PrincipalSid1, Privilege2) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .WithGrant(PrincipalSid3, Privilege1, Privilege2) + .Build(); + using var fixture = new UserRightsManagerFixture(); - var regex = string.IsNullOrWhiteSpace(revokePattern) ? null : new Regex(revokePattern, RegexOptions.None, TimeSpan.FromSeconds(1)); + var pattern = new Regex("^S-1-5-21", RegexOptions.None, TimeSpan.FromSeconds(1)); - // Act & Assert. - Assert.Throws(() => fixture.UserRightsManager.ModifyPrivilege(policy, privilege, grants, revocations, revokeAll, revokeOthers, regex, dryRun)); + // Act. + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege1, [PrincipalName1], [], false, false, pattern, true); + + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSidCurrent, PrincipalSid1, PrincipalSid2, PrincipalSid3 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSidCurrent]); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid3]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); } /// - /// Verifies revoking all principals for a privilege is successful and does not modify other assignments. + /// Verifies revoking a privilege from a principal works as expected. /// [TestMethod] - public void RevokeAllShouldWork() + public void ModifyPrivilege_WithRevoke_ShouldWork() { // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2 - }; + using var fixture = new UserRightsManagerFixture(); - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; + // Act. + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege1, [], [PrincipalName2], false, false, null, false); - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege1)); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege2)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid2), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies revoking a privilege from a principal with dry run enabled works as expected. + /// + [TestMethod] + public void ModifyPrivilege_WithRevokeAndDryRun_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); using var fixture = new UserRightsManagerFixture(); // Act. - fixture.UserRightsManager.ModifyPrivilege(policy, Privilege1, [], [], true, false, null!, false); + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege1, [], [PrincipalName2], false, false, null, true); // Assert. - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - Assert.IsEmpty(policy.LsaEnumerateAccountsWithUserRight(Privilege1)); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); } /// - /// Verifies a single revocation is successful and does not modify other assignments. + /// Verifies revoking a privilege from all principals works as expected. /// [TestMethod] - public void RevokeShouldWork() + public void ModifyPrivilege_WithRevokeAll_ShouldWork() { // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); - var principals2 = new List - { - PrincipalSid2 - }; + using var fixture = new UserRightsManagerFixture(); - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; + // Act. + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege1, [], [], true, false, null, false); + + // Assert. + CollectionAssert.AreEqual(new[] { PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid2), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + /// + /// Verifies revoking a privilege from all principals with dry run enabled works as expected. + /// + [TestMethod] + public void ModifyPrivilege_WithRevokeAllAndDryRun_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); using var fixture = new UserRightsManagerFixture(); // Act. - fixture.UserRightsManager.ModifyPrivilege(policy, Privilege1, [], [PrincipalName2], false, false, null!, false); + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege1, [], [], true, false, null, true); // Assert. - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); } /// - /// Verifies revoking all non-builtin and virtual principals from a privilege is successful. + /// Verifies revoking a privilege from all principals that match a pattern works as expected. /// [TestMethod] - public void RevokePatternForAllButBuiltinAndVirtualShouldWork() + public void ModifyPrivilege_WithRevokePattern_ShouldWork() { // Arrange. - var principals1 = new List - { - PrincipalSidCurrent, - PrincipalSid2, - PrincipalSid3 - }; + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSidCurrent, Privilege1) + .WithGrant(PrincipalSid1, Privilege2) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .WithGrant(PrincipalSid3, Privilege1, Privilege2) + .Build(); - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2, - PrincipalSid3 - }; + using var fixture = new UserRightsManagerFixture(); + var pattern = new Regex("^S-1-5-21", RegexOptions.None, TimeSpan.FromSeconds(1)); - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; + // Act. + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege1, [], [], false, false, pattern, false); - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid3]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSidCurrent), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } - CollectionAssert.AreEquivalent(new[] { PrincipalSidCurrent, PrincipalSid1, PrincipalSid2, PrincipalSid3 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSidCurrent)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid3)); + /// + /// Verifies revoking a privilege from all principals that match a pattern with dry run enabled works as expected. + /// + [TestMethod] + public void ModifyPrivilege_WithRevokePatternAndDryRun_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSidCurrent, Privilege1) + .WithGrant(PrincipalSid1, Privilege2) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .WithGrant(PrincipalSid3, Privilege1, Privilege2) + .Build(); using var fixture = new UserRightsManagerFixture(); var pattern = new Regex("^S-1-5-21", RegexOptions.None, TimeSpan.FromSeconds(1)); // Act. - fixture.UserRightsManager.ModifyPrivilege(policy, Privilege1, [], [], false, false, pattern, false); + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege1, [], [], false, false, pattern, true); // Assert. - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid3)); + CollectionAssert.AreEquivalent(new[] { PrincipalSidCurrent, PrincipalSid1, PrincipalSid2, PrincipalSid3 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSidCurrent]); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid3]); + + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); } } \ No newline at end of file diff --git a/src/Tests.Cli/.editorconfig b/src/Tests.Cli/.editorconfig new file mode 100644 index 0000000..bb86061 --- /dev/null +++ b/src/Tests.Cli/.editorconfig @@ -0,0 +1,3 @@ +[*.cs] +dotnet_diagnostic.CA1707.severity = none # Identifiers should not contain underscores. +dotnet_diagnostic.SA1515.severity = none # Single-line comment should be preceded by blank line. \ No newline at end of file diff --git a/src/Tests.Cli/CliCommandTests.cs b/src/Tests.Cli/CliCommandTests.cs new file mode 100644 index 0000000..669f628 --- /dev/null +++ b/src/Tests.Cli/CliCommandTests.cs @@ -0,0 +1,569 @@ +namespace Tests.Cli; + +using System.Globalization; +using System.Security.Principal; +using System.Text.Json; + +using CsvHelper; +using CsvHelper.Configuration; + +using Moq; + +using UserRights.Application; +using UserRights.Cli; +using UserRights.Extensions.Security; + +using static Tests.TestData; + +/// +/// Represents CLI command tests. +/// +[TestClass] +public class CliCommandTests +{ + /// + /// Gets or sets the unit test context. + /// + public required TestContext TestContext { get; set; } + + /// + /// Verifies listing all user rights to a JSON formatted file works as expected. + /// + /// A task representing the asynchronous operation. + [TestMethod] + public async Task ListMode_WithJsonAndPath_ShouldWriteJsonToFile() + { + // Arrange. + UserRightEntry[] expected = + [ + new(Privilege1, PrincipalSid1.Value, PrincipalSid1.ToAccount().Value), + new(Privilege1, PrincipalSid2.Value, PrincipalSid2.ToAccount().Value), + new(Privilege2, PrincipalSid2.Value, PrincipalSid2.ToAccount().Value), + new(Privilege2, PrincipalSid3.Value, PrincipalSid3.ToAccount().Value) + ]; + + var lsaUserRights = LsaUserRightsMockBuilder.CreateBuilder() + .WithGrant(expected) + .Build(); + + using var fixture = new CliMockBuilder(lsaUserRights.Object); + + var rootCommand = fixture.CliBuilder.Build(); + + var file = Path.GetTempFileName(); + var args = new[] { "list", "--json", "--path", file }; + + // Act. + int rc; + UserRightEntry[] actual; + try + { + rc = await rootCommand.Parse(args).ThrowIfInvalid().RunAsync(TestContext.CancellationToken).ConfigureAwait(false); + + var stream = File.OpenRead(file); + await using (stream.ConfigureAwait(false)) + { + var results = await JsonSerializer.DeserializeAsync(stream, cancellationToken: TestContext.CancellationToken).ConfigureAwait(false); + actual = results?.ToArray() ?? []; + } + } + finally + { + File.Delete(file); + } + + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(expected, actual); + + lsaUserRights.Verify(x => x.Connect(), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid1)), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid2)), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid3)), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies listing all user rights to a CSV formatted file works as expected. + /// + /// A task representing the asynchronous operation. + [TestMethod] + public async Task ListMode_WithPath_ShouldWriteCsvToFile() + { + // Arrange. + UserRightEntry[] expected = + [ + new(Privilege1, PrincipalSid1.Value, PrincipalSid1.ToAccount().Value), + new(Privilege1, PrincipalSid2.Value, PrincipalSid2.ToAccount().Value), + new(Privilege2, PrincipalSid2.Value, PrincipalSid2.ToAccount().Value), + new(Privilege2, PrincipalSid3.Value, PrincipalSid3.ToAccount().Value) + ]; + + var lsaUserRights = LsaUserRightsMockBuilder.CreateBuilder() + .WithGrant(expected) + .Build(); + + using var fixture = new CliMockBuilder(lsaUserRights.Object); + + var rootCommand = fixture.CliBuilder.Build(); + + var file = Path.GetTempFileName(); + var args = new[] { "list", "--path", file }; + + // Act. + int rc; + UserRightEntry[] actual; + try + { + rc = await rootCommand.Parse(args).ThrowIfInvalid().RunAsync(TestContext.CancellationToken).ConfigureAwait(false); + + var csvConfiguration = new CsvConfiguration(CultureInfo.InvariantCulture) + { + PrepareHeaderForMatch = a => a.Header.ToUpperInvariant() ?? throw new InvalidOperationException() + }; + + using var streamReader = new StreamReader(file); + using var csvReader = new CsvReader(streamReader, csvConfiguration); + + actual = await csvReader.GetRecordsAsync(TestContext.CancellationToken) + .ToArrayAsync(TestContext.CancellationToken) + .ConfigureAwait(false); + } + finally + { + File.Delete(file); + } + + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(expected, actual); + + lsaUserRights.Verify(x => x.Connect(), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid1)), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid2)), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid3)), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies a single grant is successful and does not modify other assignments. + /// + [TestMethod] + public void PrincipalMode_WithGrant_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); + + using var fixture = new CliMockBuilder(lsaUserRights.Object); + + var rootCommand = fixture.CliBuilder.Build(); + + var args = new[] { "principal", PrincipalName1, "--grant", Privilege2 }; + + // Act. + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); + + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.Connect(), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid1)), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaAddAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies a single grant with a single revoke is successful and does not modify other assignments. + /// + [TestMethod] + public void PrincipalMode_WithGrantAndRevoke_ShouldWork() + { + // Arrange. + const string systemName = "host.example.com"; + + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithSystemName(systemName) + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); + + using var fixture = new CliMockBuilder(lsaUserRights.Object); + + var rootCommand = fixture.CliBuilder.Build(); + + var args = new[] { "principal", PrincipalName1, "--grant", Privilege2, "--revoke", Privilege1, "--system-name", systemName }; + + // Act. + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); + + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.Connect(It.Is(s => string.Equals(s, systemName, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid1)), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaAddAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies granting a privilege to a principal and revoking their other privileges is successful and does not modify other assignments. + /// + [TestMethod] + public void PrincipalMode_WithGrantAndRevokeOthers_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .Build(); + + using var fixture = new CliMockBuilder(lsaUserRights.Object); + + var rootCommand = fixture.CliBuilder.Build(); + + var args = new[] { "principal", PrincipalName1, "--grant", Privilege2, "--revoke-others" }; + + // Act. + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); + + // Assert. + Assert.AreEqual(0, rc); + Assert.ContainsSingle(lsaUserRightsMockBuilder.Database); + Assert.AreEqual(lsaUserRightsMockBuilder.Database.Keys.Single(), PrincipalSid1); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + + lsaUserRights.Verify(x => x.Connect(), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid1)), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaAddAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies a single revocation is successful and does not modify other assignments. + /// + [TestMethod] + public void PrincipalMode_WithRevoke_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); + + using var fixture = new CliMockBuilder(lsaUserRights.Object); + + var rootCommand = fixture.CliBuilder.Build(); + + var args = new[] { "principal", PrincipalName2, "--revoke", Privilege2 }; + + // Act. + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); + + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.Connect(), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid2)), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid2), It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies a revoking all privileges for a principal is successful and does not modify other assignments. + /// + [TestMethod] + public void PrincipalMode_WithRevokeAll_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); + + using var fixture = new CliMockBuilder(lsaUserRights.Object); + + var rootCommand = fixture.CliBuilder.Build(); + + var args = new[] { "principal", PrincipalName1, "--revoke-all" }; + + // Act. + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); + + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEqual(new[] { PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.Connect(), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountRights(It.Is(s => s == PrincipalSid1)), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies a single grant is successful and does not modify other assignments. + /// + [TestMethod] + public void PrivilegeMode_WithGrant_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege2) + .Build(); + + using var fixture = new CliMockBuilder(lsaUserRights.Object); + + var rootCommand = fixture.CliBuilder.Build(); + + var args = new[] { "privilege", Privilege2, "--grant", PrincipalName1 }; + + // Act. + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); + + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.Connect(), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaAddAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies a single grant with a single revoke is successful and does not modify other assignments. + /// + [TestMethod] + public void PrivilegeMode_WithGrantAndRevoke_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege2) + .Build(); + + using var fixture = new CliMockBuilder(lsaUserRights.Object); + + var rootCommand = fixture.CliBuilder.Build(); + + var args = new[] { "privilege", Privilege1, "--grant", PrincipalName2, "--revoke", PrincipalName1 }; + + // Act. + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); + + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEqual(new[] { PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.Connect(), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaAddAccountRights(It.Is(s => s == PrincipalSid2), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies granting a principal to a privilege and revoking its other principals is successful and does not modify other assignments. + /// + [TestMethod] + public void PrivilegeMode_WithGrantAndRevokeOthers_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); + + using var fixture = new CliMockBuilder(lsaUserRights.Object); + + var rootCommand = fixture.CliBuilder.Build(); + + var args = new[] { "privilege", Privilege2, "--grant", PrincipalName1, "--revoke-others" }; + + // Act. + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); + + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.Connect(), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaAddAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid2), It.Is(s => string.Equals(s, Privilege2, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies granting a principal to a privilege and revoking all principals matching a pattern is successful and does not modify other assignments. + /// + [TestMethod] + public void PrivilegeMode_WithGrantAndRevokePattern_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSidCurrent, Privilege1) + .WithGrant(PrincipalSid1, Privilege2) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .WithGrant(PrincipalSid3, Privilege1, Privilege2) + .Build(); + + using var fixture = new CliMockBuilder(lsaUserRights.Object); + + var rootCommand = fixture.CliBuilder.Build(); + + var args = new[] { "privilege", Privilege1, "--grant", PrincipalName1, "--revoke-pattern", "^S-1-5-21" }; + + // Act. + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); + + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid3]); + + lsaUserRights.Verify(x => x.Connect(), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaAddAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSidCurrent), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies a single revocation is successful and does not modify other assignments. + /// + [TestMethod] + public void PrivilegeMode_WithRevoke_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); + + using var fixture = new CliMockBuilder(lsaUserRights.Object); + + var rootCommand = fixture.CliBuilder.Build(); + + var args = new[] { "privilege", Privilege1, "--revoke", PrincipalName2 }; + + // Act. + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); + + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.Connect(), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid2), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies revoking all principals for a privilege is successful and does not modify other assignments. + /// + [TestMethod] + public void PrivilegeMode_WithRevokeAll_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1, Privilege2) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); + + using var fixture = new CliMockBuilder(lsaUserRights.Object); + + var rootCommand = fixture.CliBuilder.Build(); + + var args = new[] { "privilege", Privilege1, "--revoke-all" }; + + // Act. + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); + + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + + lsaUserRights.Verify(x => x.Connect(), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid1), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSid2), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies revoking all non-builtin and virtual principals from a privilege is successful. + /// + [TestMethod] + public void PrivilegeMode_WithRevokePattern_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSidCurrent, Privilege1) + .WithGrant(PrincipalSid1, Privilege2) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .WithGrant(PrincipalSid3, Privilege1, Privilege2) + .Build(); + + using var fixture = new CliMockBuilder(lsaUserRights.Object); + + var rootCommand = fixture.CliBuilder.Build(); + + var args = new[] { "privilege", Privilege1, "--revoke-pattern", "^S-1-5-21" }; + + // Act. + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); + + // Assert. + Assert.AreEqual(0, rc); + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid3]); + + lsaUserRights.Verify(x => x.Connect(), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.Verify(x => x.LsaRemoveAccountRights(It.Is(s => s == PrincipalSidCurrent), It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } +} \ No newline at end of file diff --git a/src/Tests.Cli/CliSyntaxTests.cs b/src/Tests.Cli/CliSyntaxTests.cs new file mode 100644 index 0000000..98f12c6 --- /dev/null +++ b/src/Tests.Cli/CliSyntaxTests.cs @@ -0,0 +1,568 @@ +namespace Tests.Cli; + +using UserRights.Cli; + +/// +/// Represents CLI syntax tests. +/// +[TestClass] +public class CliSyntaxTests +{ + /// + /// Gets invalid method arguments for the list mode CLI syntax tests. + /// + public static IEnumerable ListModeInvalidArgumentData + { + get + { + // Invalid path value (empty/whitespace), with variations of json and system-name. + var message = "Expected invalid path value to fail."; + yield return [message, "list", "--path", string.Empty]; + yield return [message, "list", "--path", " "]; + yield return [message, "list", "--json", "--path", string.Empty]; + yield return [message, "list", "--json", "--path", " "]; + yield return [message, "list", "--path", string.Empty, "--system-name", "host.example.com"]; + yield return [message, "list", "--path", " ", "--system-name", "host.example.com"]; + yield return [message, "list", "--json", "--path", string.Empty, "--system-name", "host.example.com"]; + yield return [message, "list", "--json", "--path", " ", "--system-name", "host.example.com"]; + yield return [message, "list", "-f", string.Empty]; + yield return [message, "list", "-f", " "]; + yield return [message, "list", "-j", "-f", string.Empty]; + yield return [message, "list", "-j", "-f", " "]; + yield return [message, "list", "-f", string.Empty, "-s", "host.example.com"]; + yield return [message, "list", "-f", " ", "-s", "host.example.com"]; + yield return [message, "list", "-j", "-f", string.Empty, "-s", "host.example.com"]; + yield return [message, "list", "-j", "-f", " ", "-s", "host.example.com"]; + + // Invalid system-name value (empty/whitespace), with/without json, with valid/absent path. + message = "Expected invalid system-name value to fail."; + yield return [message, "list", "--system-name", string.Empty]; + yield return [message, "list", "--system-name", " "]; + yield return [message, "list", "--json", "--system-name", string.Empty]; + yield return [message, "list", "--json", "--system-name", " "]; + yield return [message, "list", "--path", $"{Guid.NewGuid()}.csv", "--system-name", string.Empty]; + yield return [message, "list", "--path", $"{Guid.NewGuid()}.csv", "--system-name", " "]; + yield return [message, "list", "--json", "--path", $"{Guid.NewGuid()}.csv", "--system-name", string.Empty]; + yield return [message, "list", "--json", "--path", $"{Guid.NewGuid()}.csv", "--system-name", " "]; + yield return [message, "list", "-s", string.Empty]; + yield return [message, "list", "-s", " "]; + yield return [message, "list", "-j", "-s", string.Empty]; + yield return [message, "list", "-j", "-s", " "]; + yield return [message, "list", "-f", $"{Guid.NewGuid()}.csv", "-s", string.Empty]; + yield return [message, "list", "-f", $"{Guid.NewGuid()}.csv", "-s", " "]; + yield return [message, "list", "-j", "-f", $"{Guid.NewGuid()}.csv", "-s", string.Empty]; + yield return [message, "list", "-j", "-f", $"{Guid.NewGuid()}.csv", "-s", " "]; + + // Both path and system-name invalid simultaneously. + message = "Expected invalid path and system-name to fail."; + yield return [message, "list", "--path", string.Empty, "--system-name", string.Empty]; + yield return [message, "list", "--path", string.Empty, "--system-name", " "]; + yield return [message, "list", "--path", " ", "--system-name", string.Empty]; + yield return [message, "list", "--path", " ", "--system-name", " "]; + yield return [message, "list", "--json", "--path", string.Empty, "--system-name", string.Empty]; + yield return [message, "list", "--json", "--path", string.Empty, "--system-name", " "]; + yield return [message, "list", "--json", "--path", " ", "--system-name", string.Empty]; + yield return [message, "list", "--json", "--path", " ", "--system-name", " "]; + yield return [message, "list", "-f", string.Empty, "-s", string.Empty]; + yield return [message, "list", "-f", " ", "-s", " "]; + yield return [message, "list", "-j", "-f", string.Empty, "-s", " "]; + yield return [message, "list", "-j", "-f", " ", "-s", string.Empty]; + } + } + + /// + /// Gets valid method arguments for the list mode CLI syntax tests. + /// + public static IEnumerable ListModeValidArgumentData + { + get + { + // CSV mode. + var message = "Expected valid CSV output options to pass."; + yield return [message, "list"]; + yield return [message, "list", "--path", $"{Guid.NewGuid()}.csv"]; + yield return [message, "list", "-f", $"{Guid.NewGuid()}.csv"]; + yield return [message, "list", "--system-name", "host.example.com"]; + yield return [message, "list", "-s", "host.example.com"]; + yield return [message, "list", "--path", $"{Guid.NewGuid()}.csv", "--system-name", "host.example.com"]; + yield return [message, "list", "-f", $"{Guid.NewGuid()}.csv", "-s", "host.example.com"]; + + // JSON mode. + message = "Expected valid JSON output options to pass."; + yield return [message, "list", "--json"]; + yield return [message, "list", "-j"]; + yield return [message, "list", "--json", "--path", $"{Guid.NewGuid()}.json"]; + yield return [message, "list", "-j", "-f", $"{Guid.NewGuid()}.json"]; + yield return [message, "list", "--json", "--system-name", "host.example.com"]; + yield return [message, "list", "-j", "-s", "host.example.com"]; + yield return [message, "list", "--json", "--path", $"{Guid.NewGuid()}.json", "--system-name", "host.example.com"]; + yield return [message, "list", "-j", "-f", $"{Guid.NewGuid()}.json", "-s", "host.example.com"]; + } + } + + /// + /// Gets invalid method arguments for the principal mode CLI syntax tests. + /// + public static IEnumerable PrincipalModeInvalidArgumentData + { + get + { + // Missing required options (no grant/revoke/revoke-all). + var message = "Expected missing required options (no grant/revoke/revoke-all) to fail."; + yield return [message, "principal", "DOMAIN\\UserOrGroup"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--dry-run"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--system-name", "host.example.com"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-d", "-s", "host.example.com"]; + + // Overlap between grants and revocations. + message = "Expected overlap between grants and revocations to fail."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--revoke", "SeServiceLogonRight"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-g", "SeServiceLogonRight", "-r", "SeServiceLogonRight"]; + + // Duplicate grants. + message = "Expected duplicate grants to fail."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--grant", "SeServiceLogonRight"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-g", "SeBatchLogonRight", "-g", "SeBatchLogonRight"]; + + // Duplicate revocations. + message = "Expected duplicate revocations to fail."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--revoke", "SeServiceLogonRight", "--revoke", "SeServiceLogonRight"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-r", "SeBatchLogonRight", "-r", "SeBatchLogonRight"]; + + // --revoke-all combined with any other option (disallowed). + message = "Expected revoke-all combined with any other option to fail."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--revoke-all", "--grant", "SeServiceLogonRight"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--revoke-all", "--revoke", "SeBatchLogonRight"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--revoke-all", "--revoke-others"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-a", "-g", "SeServiceLogonRight"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-a", "-r", "SeBatchLogonRight"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-a", "-o"]; + + // Invalid usages of --revoke-others (must be used only with grants, and without revoke/revoke-all). + message = "Expected invalid usages of revoke-others to fail."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--revoke-others"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--revoke-others", "--revoke", "SeBatchLogonRight"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--revoke-others", "--revoke-all"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-o"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-o", "-r", "SeBatchLogonRight"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-o", "-a"]; + + // Empty or whitespace values for --system-name. + message = "Expected empty or whitespace values for system-name to fail."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--system-name", string.Empty]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--system-name", " "]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-g", "SeBatchLogonRight", "-s", string.Empty]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-g", "SeBatchLogonRight", "-s", " "]; + + // Empty or whitespace values for --grant. + message = "Expected empty or whitespace values for grant to fail."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--grant", string.Empty]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--grant", " "]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-g", string.Empty]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-g", " "]; + + // Empty or whitespace values for --revoke. + message = "Expected empty or whitespace values for revoke to fail."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--revoke", string.Empty]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--revoke", " "]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-r", string.Empty]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-r", " "]; + + // Empty or whitespace principal value. + message = "Expected empty or whitespace principal value to fail."; + yield return [message, "principal", string.Empty, "--grant", "SeServiceLogonRight"]; + yield return [message, "principal", " ", "--grant", "SeServiceLogonRight"]; + yield return [message, "principal", string.Empty, "--revoke", "SeBatchLogonRight"]; + yield return [message, "principal", " ", "--revoke", "SeBatchLogonRight"]; + yield return [message, "principal", string.Empty, "--revoke-all"]; + yield return [message, "principal", " ", "--revoke-all"]; + } + } + + /// + /// Gets valid method arguments for the principal mode CLI syntax tests. + /// + public static IEnumerable PrincipalModeValidArgumentData + { + get + { + // Single grant. + var message = "Expected single grant to pass."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-g", "SeBatchLogonRight"]; + + // Multiple grants (no duplicates). + message = "Expected multiple grants to pass."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--grant", "SeBatchLogonRight"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-g", "SeInteractiveLogonRight", "-g", "SeRemoteInteractiveLogonRight"]; + + // Single revoke. + message = "Expected single revoke to pass."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--revoke", "SeServiceLogonRight"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-r", "SeBatchLogonRight"]; + + // Multiple revokes (no duplicates). + message = "Expected multiple revokes to pass."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--revoke", "SeServiceLogonRight", "--revoke", "SeBatchLogonRight"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-r", "SeInteractiveLogonRight", "-r", "SeRemoteInteractiveLogonRight"]; + + // Grant(s) plus revoke(s) (no overlaps between sets). + message = "Expected grant(s) plus revoke(s) to pass."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--revoke", "SeBatchLogonRight"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-g", "SeInteractiveLogonRight", "-g", "SeCreateGlobalPrivilege", "-r", "SeRemoteInteractiveLogonRight"]; + + // Revoke-all (must be alone with respect to grant/revoke/others options). + message = "Expected revoke-all to pass."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--revoke-all"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-a"]; + + // Revoke-others with grants (and without revoke/revoke-all). + message = "Expected revoke-others with grants to pass."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--revoke-others"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-g", "SeBatchLogonRight", "-g", "SeInteractiveLogonRight", "-o"]; + + // Using system-name (non-empty) and/or dry-run. + message = "Expected a valid system-name and/or dry-run to pass."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--system-name", "host.example.com"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--revoke", "SeBatchLogonRight", "--system-name", "host.example.com"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--revoke-all", "--system-name", "host.example.com"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--revoke-others", "--dry-run"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-g", "SeInteractiveLogonRight", "-r", "SeRemoteInteractiveLogonRight", "-d", "-s", "host.example.com"]; + + // Mixed long/short forms while staying valid. + message = "Expected mixed long/short form arguments to pass."; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "-g", "SeServiceLogonRight", "--revoke", "SeBatchLogonRight"]; + yield return [message, "principal", "DOMAIN\\UserOrGroup", "--grant", "SeInteractiveLogonRight", "-o", "--dry-run"]; + } + } + + /// + /// Gets invalid method arguments for the privilege mode CLI syntax tests. + /// + public static IEnumerable PrivilegeModeInvalidArgumentData + { + get + { + // Missing required option(s): no grant/revoke/revoke-all/revoke-pattern. + var message = "Expected missing required option(s) to fail."; + yield return [message, "privilege", "SeServiceLogonRight"]; + yield return [message, "privilege", "SeBatchLogonRight", "--dry-run"]; + yield return [message, "privilege", "SeServiceLogonRight", "--system-name", "host.example.com"]; + yield return [message, "privilege", "SeBatchLogonRight", "-d", "-s", "host.example.com"]; + + // Invalid privilege argument (empty or whitespace). + message = "Expected invalid privilege argument to fail."; + yield return [message, "privilege", string.Empty, "--grant", "DOMAIN\\UserOrGroup"]; + yield return [message, "privilege", " ", "--revoke", "DOMAIN\\UserOrGroup"]; + + // Invalid grant values (empty or whitespace principal). + message = "Expected invalid grant values to fail."; + yield return [message, "privilege", "SeServiceLogonRight", "--grant", string.Empty]; + yield return [message, "privilege", "SeServiceLogonRight", "--grant", " "]; + yield return [message, "privilege", "SeBatchLogonRight", "-g", string.Empty]; + yield return [message, "privilege", "SeBatchLogonRight", "-g", " "]; + + // Invalid revoke values (empty or whitespace principal). + message = "Expected invalid revoke values to fail."; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke", string.Empty]; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke", " "]; + yield return [message, "privilege", "SeBatchLogonRight", "-r", string.Empty]; + yield return [message, "privilege", "SeBatchLogonRight", "-r", " "]; + + // Overlap between grants and revocations (same principal in both sets). + message = "Expected overlap between grants and revocations to fail."; + yield return [message, "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--revoke", "DOMAIN\\UserOrGroup"]; + yield return [message, "privilege", "SeBatchLogonRight", "-g", "DOMAIN\\UserOrGroup", "-r", "DOMAIN\\UserOrGroup"]; + + // Duplicate grants. + message = "Expected duplicate grants to fail."; + yield return [message, "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--grant", "DOMAIN\\UserOrGroup"]; + yield return [message, "privilege", "SeBatchLogonRight", "-g", "DOMAIN\\UserOrGroup", "-g", "DOMAIN\\UserOrGroup"]; + + // Duplicate revocations. + message = "Expected duplicate revocations to fail."; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke", "DOMAIN\\UserOrGroup", "--revoke", "DOMAIN\\UserOrGroup"]; + yield return [message, "privilege", "SeBatchLogonRight", "-r", "DOMAIN\\UserOrGroup", "-r", "DOMAIN\\UserOrGroup"]; + + // Invalid usages of --revoke-all. + message = "Expected invalid usages of revoke-all to fail."; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke-all", "--grant", "DOMAIN\\UserOrGroup"]; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke-all", "--revoke", "DOMAIN\\UserOrGroup"]; + yield return [message, "privilege", "SeBatchLogonRight", "--revoke-all", "--revoke-others"]; + yield return [message, "privilege", "SeBatchLogonRight", "--revoke-all", "--revoke-pattern", ".*"]; + yield return [message, "privilege", "SeBatchLogonRight", "-a", "-g", "DOMAIN\\UserOrGroup"]; + yield return [message, "privilege", "SeBatchLogonRight", "-a", "-r", "DOMAIN\\UserOrGroup"]; + yield return [message, "privilege", "SeBatchLogonRight", "-a", "-o"]; + yield return [message, "privilege", "SeBatchLogonRight", "-a", "-t", ".*"]; + + // Invalid usages of --revoke-others. + message = "Expected invalid usages of revoke-others to fail."; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke-others"]; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke-others", "--revoke", "DOMAIN\\UserOrGroup"]; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke-others", "--revoke-all"]; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke-others", "--revoke-pattern", ".*"]; + yield return [message, "privilege", "SeBatchLogonRight", "-o"]; + yield return [message, "privilege", "SeBatchLogonRight", "-o", "-r", "DOMAIN\\UserOrGroup"]; + yield return [message, "privilege", "SeBatchLogonRight", "-o", "-a"]; + yield return [message, "privilege", "SeBatchLogonRight", "-o", "-t", ".*"]; + + // Invalid usages of --revoke-pattern. + message = "Expected invalid usages of revoke-pattern to fail."; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke-pattern", string.Empty]; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke-pattern", " "]; + yield return [message, "privilege", "SeBatchLogonRight", "-t", string.Empty]; + yield return [message, "privilege", "SeBatchLogonRight", "-t", " "]; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke-pattern", "["]; + yield return [message, "privilege", "SeBatchLogonRight", "-t", "(? + /// Gets valid method arguments for the privilege mode CLI syntax tests. + /// + public static IEnumerable PrivilegeModeValidArgumentData + { + get + { + // Grant only. + var message = "Expected grant only to pass."; + yield return [message, "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup"]; + yield return [message, "privilege", "SeBatchLogonRight", "-g", "DOMAIN\\UserOrGroup"]; + yield return [message, "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--dry-run"]; + yield return [message, "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--system-name", "host.example.com"]; + yield return [message, "privilege", "SeBatchLogonRight", "-g", "DOMAIN\\UserOrGroup", "-d", "-s", "host.example.com"]; + + // Revoke only. + message = "Expected revoke only to pass."; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke", "DOMAIN\\UserOrGroup"]; + yield return [message, "privilege", "SeBatchLogonRight", "-r", "DOMAIN\\UserOrGroup"]; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke", "DOMAIN\\UserOrGroup", "--dry-run"]; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke", "DOMAIN\\UserOrGroup", "--system-name", "host.example.com"]; + yield return [message, "privilege", "SeBatchLogonRight", "-r", "DOMAIN\\UserOrGroup", "-d", "-s", "host.example.com"]; + + // Grant(s) and revoke(s) without overlap. + message = "Expected grant(s) and revoke(s) without overlap to pass."; + yield return [message, "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroupA", "--revoke", "DOMAIN\\UserOrGroupB"]; + yield return [message, "privilege", "SeBatchLogonRight", "-g", "DOMAIN\\UserOrGroupA", "--revoke", "DOMAIN\\UserOrGroupB", "--dry-run", "--system-name", "host.example.com"]; + + // Revoke-all (cannot mix with grants/revokes/others/pattern). + message = "Expected revoke-all to pass."; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke-all"]; + yield return [message, "privilege", "SeBatchLogonRight", "-a"]; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke-all", "--dry-run"]; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke-all", "--system-name", "host.example.com"]; + yield return [message, "privilege", "SeBatchLogonRight", "-a", "-d", "-s", "host.example.com"]; + + // Revoke-others with grants (no revoke/revoke-all/revoke-pattern). + message = "Expected revoke-others with grants to pass."; + yield return [message, "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--revoke-others"]; + yield return [message, "privilege", "SeBatchLogonRight", "-g", "DOMAIN\\UserOrGroup", "-o"]; + yield return [message, "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--revoke-others", "--dry-run"]; + yield return [message, "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--revoke-others", "--system-name", "host.example.com"]; + yield return [message, "privilege", "SeBatchLogonRight", "-g", "DOMAIN\\UserOrGroup", "-o", "-d", "-s", "host.example.com"]; + + // Revoke-pattern alone (valid regex; no revoke/revoke-all/revoke-others). + message = "Expected revoke-pattern alone to pass."; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-"]; + yield return [message, "privilege", "SeBatchLogonRight", "-t", "^S-1-5-21-"]; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-", "--dry-run"]; + yield return [message, "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-", "--system-name", "host.example.com"]; + yield return [message, "privilege", "SeBatchLogonRight", "-t", "^S-1-5-21-", "-d", "-s", "host.example.com"]; + + // Revoke-pattern with grants (no revoke/revoke-all/revoke-others). + message = "Expected revoke-pattern with grants to pass."; + yield return [message, "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--revoke-pattern", "^S-1-5-21-"]; + yield return [message, "privilege", "SeBatchLogonRight", "-g", "DOMAIN\\UserOrGroup", "-t", "^S-1-5-21-"]; + yield return [message, "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--revoke-pattern", "^S-1-5-21-", "--dry-run"]; + yield return [message, "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--revoke-pattern", "^S-1-5-21-", "--system-name", "host.example.com"]; + yield return [message, "privilege", "SeBatchLogonRight", "-g", "DOMAIN\\UserOrGroup", "-t", "^S-1-5-21-", "-d", "-s", "host.example.com"]; + } + } + + /// + /// Verifies the CLI rejects parsing list mode with invalid arguments. + /// + /// The test failure message. + /// The test arguments. + [TestMethod] + [DynamicData(nameof(ListModeInvalidArgumentData))] + public void ListMode_WithInvalidArguments_IsRejected(string message, params string[] args) + { + // Arrange. + using var fixture = new CliMockBuilder(); + + var rootCommand = fixture.CliBuilder.Build(); + + // Act. + var rc = rootCommand.Parse(args).Run(); + + // Assert. + Assert.AreNotEqual(0, rc, message); + } + + /// + /// Verifies the CLI rejects parsing list mode with invalid arguments. + /// + /// The test failure message. + /// The test arguments. + [TestMethod] + [DynamicData(nameof(ListModeInvalidArgumentData))] + public void ListMode_WithInvalidArguments_ThrowsException(string message, params string[] args) + { + using var fixture = new CliMockBuilder(); + + Assert.Throws(() => fixture.CliBuilder.Build().Parse(args).ThrowIfInvalid().Run(), message); + } + + /// + /// Verifies the CLI accepts parsing list mode with valid arguments. + /// + /// The test failure message. + /// The test arguments. + [TestMethod] + [DynamicData(nameof(ListModeValidArgumentData))] + public void ListMode_WithValidArguments_IsAccepted(string message, params string[] args) + { + // Arrange. + using var fixture = new CliMockBuilder(); + + var rootCommand = fixture.CliBuilder.Build(); + + // Act. + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); + + // Assert. + Assert.AreEqual(0, rc, message); + } + + /// + /// Verifies the CLI rejects parsing principal mode with invalid arguments. + /// + /// The test failure message. + /// The test arguments. + [TestMethod] + [DynamicData(nameof(PrincipalModeInvalidArgumentData))] + public void PrincipalMode_WithInvalidArguments_IsRejected(string message, params string[] args) + { + using var fixture = new CliMockBuilder(); + + Assert.Throws(() => fixture.CliBuilder.Build().Parse(args).ThrowIfInvalid().Run(), message); + } + + /// + /// Verifies the CLI rejects parsing principal mode with invalid arguments. + /// + /// The test failure message. + /// The test arguments. + [TestMethod] + [DynamicData(nameof(PrincipalModeInvalidArgumentData))] + public void PrincipalMode_WithInvalidArguments_ThrowsException(string message, params string[] args) + { + // Arrange. + using var fixture = new CliMockBuilder(); + + var rootCommand = fixture.CliBuilder.Build(); + + // Act. + var rc = rootCommand.Parse(args).Run(); + + // Assert. + Assert.AreNotEqual(0, rc, message); + } + + /// + /// Verifies the CLI accepts parsing principal mode with valid arguments. + /// + /// The test failure message. + /// The test arguments. + [TestMethod] + [DynamicData(nameof(PrincipalModeValidArgumentData))] + public void PrincipalMode_WithValidArguments_IsAccepted(string message, params string[] args) + { + // Arrange. + using var fixture = new CliMockBuilder(); + + var rootCommand = fixture.CliBuilder.Build(); + + // Act. + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); + + // Assert. + Assert.AreEqual(0, rc, message); + } + + /// + /// Verifies the CLI rejects parsing privilege mode with invalid arguments. + /// + /// The test failure message. + /// The test arguments. + [TestMethod] + [DynamicData(nameof(PrivilegeModeInvalidArgumentData))] + public void PrivilegeMode_WithInvalidArguments_IsRejected(string message, params string[] args) + { + using var fixture = new CliMockBuilder(); + + Assert.Throws(() => fixture.CliBuilder.Build().Parse(args).ThrowIfInvalid().Run(), message); + } + + /// + /// Verifies the CLI rejects parsing privilege mode with invalid arguments. + /// + /// The test failure message. + /// The test arguments. + [TestMethod] + [DynamicData(nameof(PrivilegeModeInvalidArgumentData))] + public void PrivilegeMode_WithInvalidArguments_ThrowsException(string message, params string[] args) + { + // Arrange. + using var fixture = new CliMockBuilder(); + + var rootCommand = fixture.CliBuilder.Build(); + + // Act. + var rc = rootCommand.Parse(args).Run(); + + // Assert. + Assert.AreNotEqual(0, rc, message); + } + + /// + /// Verifies the CLI accepts parsing privilege mode with valid arguments. + /// + /// The test failure message. + /// The test arguments. + [TestMethod] + [DynamicData(nameof(PrivilegeModeValidArgumentData))] + public void PrivilegeMode_WithValidArguments_IsAccepted(string message, params string[] args) + { + // Arrange. + using var fixture = new CliMockBuilder(); + + var rootCommand = fixture.CliBuilder.Build(); + + // Act. + var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); + + // Assert. + Assert.AreEqual(0, rc, message); + } +} \ No newline at end of file diff --git a/src/Tests.Cli/ListCommandTests.cs b/src/Tests.Cli/ListCommandTests.cs deleted file mode 100644 index 400f45c..0000000 --- a/src/Tests.Cli/ListCommandTests.cs +++ /dev/null @@ -1,165 +0,0 @@ -namespace Tests.Cli; - -using System.Globalization; -using System.Security.Principal; -using System.Text.Json; - -using CsvHelper; -using CsvHelper.Configuration; - -using UserRights.Application; -using UserRights.Cli; -using UserRights.Extensions.Security; - -using static Tests.TestData; - -/// -/// Represents integration tests for list functionality. -/// -[TestClass] -public class ListCommandTests -{ - /// - /// Gets or sets the unit test context. - /// - public required TestContext TestContext { get; set; } - - /// - /// Verifies listing user rights to a JSON file. - /// - /// A task representing the asynchronous operation. - [TestMethod] - public async Task PathAndJsonShouldWork() - { - // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var expected = database - .SelectMany(kvp => kvp.Value.Select(p => new UserRightEntry(kvp.Key, p.Value, p.ToAccount().Value))) - .OrderBy(p => p.Privilege, StringComparer.OrdinalIgnoreCase) - .ThenBy(p => p.SecurityId, StringComparer.OrdinalIgnoreCase) - .ToArray(); - - var policy = new MockLsaUserRights(database); - - using var fixture = new CliBuilderFixture(policy); - - var rootCommand = fixture.CliBuilder.Build(); - - var file = Path.GetTempFileName(); - var args = new[] { "list", "--json", "--path", file }; - - // Act. - int rc; - UserRightEntry[] actual; - try - { - rc = await rootCommand.Parse(args).ThrowIfInvalid().RunAsync(TestContext.CancellationToken).ConfigureAwait(false); - - var stream = File.OpenRead(file); - await using (stream.ConfigureAwait(false)) - { - var results = await JsonSerializer.DeserializeAsync(stream, cancellationToken: TestContext.CancellationToken).ConfigureAwait(false); - actual = results - ?.OrderBy(p => p.Privilege, StringComparer.OrdinalIgnoreCase) - .ThenBy(p => p.SecurityId, StringComparer.OrdinalIgnoreCase) - .ToArray() ?? []; - } - } - finally - { - File.Delete(file); - } - - // Assert. - Assert.AreEqual(0, rc); - CollectionAssert.AreEqual(expected, actual); - } - - /// - /// Verifies listing user rights to a CSV file. - /// - /// A task representing the asynchronous operation. - [TestMethod] - public async Task PathShouldWork() - { - // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - - var expected = database - .SelectMany(kvp => kvp.Value.Select(p => new UserRightEntry(kvp.Key, p.Value, p.ToAccount().Value))) - .OrderBy(p => p.Privilege, StringComparer.OrdinalIgnoreCase) - .ThenBy(p => p.SecurityId, StringComparer.OrdinalIgnoreCase) - .ToArray(); - - using var fixture = new CliBuilderFixture(policy); - - var rootCommand = fixture.CliBuilder.Build(); - - var file = Path.GetTempFileName(); - var args = new[] { "list", "--path", file }; - - // Act. - int rc; - UserRightEntry[] actual; - try - { - rc = await rootCommand.Parse(args).ThrowIfInvalid().RunAsync(TestContext.CancellationToken).ConfigureAwait(false); - - var csvConfiguration = new CsvConfiguration(CultureInfo.InvariantCulture) - { - PrepareHeaderForMatch = a => a.Header.ToUpperInvariant() ?? throw new InvalidOperationException() - }; - - using var streamReader = new StreamReader(file); - using var csvReader = new CsvReader(streamReader, csvConfiguration); - - actual = await csvReader.GetRecordsAsync(TestContext.CancellationToken) - .OrderBy(p => p.Privilege, StringComparer.OrdinalIgnoreCase) - .ThenBy(p => p.SecurityId, StringComparer.OrdinalIgnoreCase) - .ToArrayAsync(TestContext.CancellationToken) - .ConfigureAwait(false); - } - finally - { - File.Delete(file); - } - - // Assert. - Assert.AreEqual(0, rc); - CollectionAssert.AreEqual(expected, actual); - } -} \ No newline at end of file diff --git a/src/Tests.Cli/ListSyntaxTests.cs b/src/Tests.Cli/ListSyntaxTests.cs deleted file mode 100644 index 8988373..0000000 --- a/src/Tests.Cli/ListSyntaxTests.cs +++ /dev/null @@ -1,98 +0,0 @@ -namespace Tests.Cli; - -using UserRights.Cli; - -/// -/// Represents syntax tests for list functionality. -/// -[TestClass] -public sealed class ListSyntaxTests : CliBuilderFixture -{ - /// - /// Verifies list mode with CSV formatted output sent to STDOUT is parsed successfully. - /// - [TestMethod] - public void CsvToStdoutShouldWork() - { - // Arrange. - var args = new[] { "list" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Verifies list mode with CSV formatted output sent to a file is parsed successfully. - /// - [TestMethod] - public void CsvToPathShouldWork() - { - // Arrange. - var args = new[] { "list", "--path", "file.csv" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Ensures an empty or whitespace path is rejected. - /// - /// The test arguments. - [TestMethod] - [DataRow("list", "--path", "")] - [DataRow("list", "--path", " ")] - public void PathWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); - - /// - /// Ensures an empty or whitespace system name is rejected. - /// - /// The test arguments. - [TestMethod] - [DataRow("list", "--system-name", "")] - [DataRow("list", "--system-name", " ")] - public void SystemNameWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); - - /// - /// Verifies list mode with JSON formatted output sent to STDOUT is parsed successfully. - /// - [TestMethod] - public void JsonToStdoutShouldWork() - { - // Arrange. - var args = new[] { "list", "--json" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Verifies list mode with JSON formatted output sent to a file is parsed successfully. - /// - [TestMethod] - public void JsonToPathShouldWork() - { - // Arrange. - var args = new[] { "list", "--json", "--path", "file.json" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } -} \ No newline at end of file diff --git a/src/Tests.Cli/PrincipalCommandTests.cs b/src/Tests.Cli/PrincipalCommandTests.cs deleted file mode 100644 index ebd1eaa..0000000 --- a/src/Tests.Cli/PrincipalCommandTests.cs +++ /dev/null @@ -1,259 +0,0 @@ -namespace Tests.Cli; - -using System.Security.Principal; - -using UserRights.Cli; - -using static Tests.TestData; - -/// -/// Represents integration tests for modify principal functionality. -/// -[TestClass] -public class PrincipalCommandTests -{ - /// - /// Verifies granting a privilege to a principal and revoking their other privileges is successful and does not modify other assignments. - /// - [TestMethod] - public void GrantAndRevokeOthersShouldWork() - { - // Arrange. - var principals1 = new List - { - PrincipalSid1 - }; - - var principals2 = new List - { - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - - policy.ResetConnection(); - - using var fixture = new CliBuilderFixture(policy); - - var rootCommand = fixture.CliBuilder.Build(); - - var args = new[] { "principal", PrincipalName1, "--grant", Privilege2, "--revoke-others" }; - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - } - - /// - /// Verifies a single grant with a single revoke is successful and does not modify other assignments. - /// - [TestMethod] - public void GrantAndRevokeShouldWork() - { - // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var principals2 = new List - { - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - - policy.ResetConnection(); - - using var fixture = new CliBuilderFixture(policy); - - var rootCommand = fixture.CliBuilder.Build(); - - var args = new[] { "principal", PrincipalName1, "--grant", Privilege2, "--revoke", Privilege1 }; - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - } - - /// - /// Verifies a single grant is successful and does not modify other assignments. - /// - [TestMethod] - public void GrantShouldWork() - { - // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var principals2 = new List - { - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - - policy.ResetConnection(); - - using var fixture = new CliBuilderFixture(policy); - - var rootCommand = fixture.CliBuilder.Build(); - - var args = new[] { "principal", PrincipalName1, "--grant", Privilege2 }; - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - } - - /// - /// Verifies a revoking all privileges for a principal is successful and does not modify other assignments. - /// - [TestMethod] - public void RevokeAllShouldWork() - { - // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - - policy.ResetConnection(); - - using var fixture = new CliBuilderFixture(policy); - - var rootCommand = fixture.CliBuilder.Build(); - - var args = new[] { "principal", PrincipalName1, "--revoke-all" }; - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - Assert.IsEmpty(policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - } - - /// - /// Verifies a single revocation is successful and does not modify other assignments. - /// - [TestMethod] - public void RevokeShouldWork() - { - // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var principals2 = new List - { - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - - policy.ResetConnection(); - - using var fixture = new CliBuilderFixture(policy); - - var rootCommand = fixture.CliBuilder.Build(); - - var args = new[] { "principal", PrincipalName2, "--revoke", Privilege2 }; - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - } -} \ No newline at end of file diff --git a/src/Tests.Cli/PrincipalSyntaxTests.cs b/src/Tests.Cli/PrincipalSyntaxTests.cs deleted file mode 100644 index 37087da..0000000 --- a/src/Tests.Cli/PrincipalSyntaxTests.cs +++ /dev/null @@ -1,260 +0,0 @@ -namespace Tests.Cli; - -using UserRights.Cli; - -/// -/// Represents syntax tests for principal functionality. -/// -[TestClass] -public sealed class PrincipalSyntaxTests : CliBuilderFixture -{ - /// - /// Ensures granting a privilege and revoking a different privilege is accepted. - /// - [TestMethod] - public void GrantAndRevokeShouldWork() - { - // Arrange. - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--revoke", "SeBatchLogonRight" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Ensures granting multiple privileges is accepted. - /// - [TestMethod] - public void GrantMultipleShouldWork() - { - // Arrange. - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--grant", "SeBatchLogonRight" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Ensures granting a privilege is accepted. - /// - [TestMethod] - public void GrantShouldWork() - { - // Arrange. - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Ensures an empty or whitespace grant is rejected. - /// - /// The test arguments. - [TestMethod] - [DataRow("principal", "DOMAIN\\UserOrGroup", "--grant", "")] - [DataRow("principal", "DOMAIN\\UserOrGroup", "--grant", " ")] - public void GrantWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); - - /// - /// Ensures granting a privilege and revoking all other privileges is rejected. - /// - [TestMethod] - public void GrantWithRevokeAllThrowsException() - { - // Arrange. - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--revoke-all" }; - var rootCommand = CliBuilder.Build(); - - // Act & Assert. - Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures specifying no options is rejected. - /// - [TestMethod] - public void NoOptionsThrowsException() - { - // Arrange. - var args = new[] { "principal" }; - var rootCommand = CliBuilder.Build(); - - // Act & Assert. - Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures overlapping or duplicate privileges are rejected. - /// - /// The test arguments. - [TestMethod] - [DataRow("principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--revoke", "SeServiceLogonRight")] - [DataRow("principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--grant", "SeServiceLogonRight")] - [DataRow("principal", "DOMAIN\\UserOrGroup", "--revoke", "SeServiceLogonRight", "--revoke", "SeServiceLogonRight")] - public void OverlappingGrantsAndRevokesThrowsException(params string[] args) - => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); - - /// - /// Ensures an empty or whitespace principal is rejected. - /// - /// The test arguments. - [TestMethod] - [DataRow("principal", "", "--grant", "SeServiceLogonRight")] - [DataRow("principal", " ", "--grant", "SeServiceLogonRight")] - public void PrincipalWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); - - /// - /// Ensures revoking all privileges is accepted. - /// - [TestMethod] - public void RevokeAllShouldWork() - { - // Arrange. - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-all" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Ensures granting a privilege and revoking all privileges is rejected. - /// - [TestMethod] - public void RevokeAllWithGrantsThrowsException() - { - // Arrange. - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-all", "--grant", "SeServiceLogonRight" }; - var rootCommand = CliBuilder.Build(); - - // Act & Assert. - Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures revoking a privilege and revoking all privileges is rejected. - /// - [TestMethod] - public void RevokeAllWithRevocationsThrowsException() - { - // Arrange. - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-all", "--revoke", "SeServiceLogonRight" }; - var rootCommand = CliBuilder.Build(); - - // Act & Assert. - Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures revoking all privileges and revoking other privileges is rejected. - /// - [TestMethod] - public void RevokeAllWithRevokeOthersThrowsException() - { - // Arrange. - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-all", "--revoke-others" }; - var rootCommand = CliBuilder.Build(); - - // Act & Assert. - Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures revoking multiple privileges is accepted. - /// - [TestMethod] - public void RevokeMultipleShouldWork() - { - // Arrange. - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke", "SeServiceLogonRight", "--revoke", "SeBatchLogonRight" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Ensures revoke other privileges without granting a privilege is rejected. - /// - [TestMethod] - public void RevokeOthersWithOutGrantsThrowsException() - { - // Arrange. - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-others" }; - - // Act & Assert. - Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures revoke other privileges while also revoking a privilege is rejected. - /// - [TestMethod] - public void RevokeOthersWithRevocationsThrowsException() - { - // Arrange. - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-others", "--revoke", "SeServiceLogonRight" }; - var rootCommand = CliBuilder.Build(); - - // Act & Assert. - Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures revoking a privilege is accepted. - /// - [TestMethod] - public void RevokeShouldWork() - { - // Arrange. - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke", "SeServiceLogonRight" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Ensures an empty or whitespace revocation is rejected. - /// - /// The test arguments. - [TestMethod] - [DataRow("principal", "DOMAIN\\UserOrGroup", "--revoke", "")] - [DataRow("principal", "DOMAIN\\UserOrGroup", "--revoke", " ")] - public void RevokeWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); - - /// - /// Ensures an empty or whitespace system name is rejected. - /// - /// The test arguments. - [TestMethod] - [DataRow("principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--system-name", "")] - [DataRow("principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--system-name", " ")] - public void SystemNameWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); -} \ No newline at end of file diff --git a/src/Tests.Cli/PrivilegeCommandTests.cs b/src/Tests.Cli/PrivilegeCommandTests.cs deleted file mode 100644 index a1f459c..0000000 --- a/src/Tests.Cli/PrivilegeCommandTests.cs +++ /dev/null @@ -1,377 +0,0 @@ -namespace Tests.Cli; - -using System.Security.Principal; - -using UserRights.Cli; - -using static Tests.TestData; - -/// -/// Represents integration tests for modify privilege functionality. -/// -[TestClass] -public class PrivilegeCommandTests -{ - /// - /// Verifies granting a principal to a privilege and revoking its other principals is successful and does not modify other assignments. - /// - [TestMethod] - public void GrantAndRevokeOthersShouldWork() - { - // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var principals2 = new List - { - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege1)); - CollectionAssert.AreEqual(new[] { PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege2)); - - policy.ResetConnection(); - - using var fixture = new CliBuilderFixture(policy); - - var rootCommand = fixture.CliBuilder.Build(); - - var args = new[] { "privilege", Privilege2, "--grant", PrincipalName1, "--revoke-others" }; - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege1)); - CollectionAssert.AreEqual(new[] { PrincipalSid1 }, policy.LsaEnumerateAccountsWithUserRight(Privilege2)); - } - - /// - /// Verifies a single grant with a single revoke is successful and does not modify other assignments. - /// - [TestMethod] - public void GrantAndRevokePasses() - { - // Arrange. - var principals1 = new List - { - PrincipalSid1 - }; - - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - - policy.ResetConnection(); - - using var fixture = new CliBuilderFixture(policy); - - var rootCommand = fixture.CliBuilder.Build(); - - var args = new[] { "privilege", Privilege1, "--grant", PrincipalName2, "--revoke", PrincipalName1 }; - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - } - - /// - /// Verifies granting a principal to a privilege and revoking all principals matching a pattern is successful and does not modify other assignments. - /// - [TestMethod] - public void GrantAndRevokePatternPasses() - { - // Arrange. - var principals1 = new List - { - PrincipalSidCurrent, - PrincipalSid2, - PrincipalSid3 - }; - - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2, - PrincipalSid3 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSidCurrent, PrincipalSid1, PrincipalSid2, PrincipalSid3 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSidCurrent)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid3)); - - policy.ResetConnection(); - - using var fixture = new CliBuilderFixture(policy); - - var rootCommand = fixture.CliBuilder.Build(); - - var args = new[] { "privilege", Privilege1, "--grant", PrincipalName1, "--revoke-pattern", "^S-1-5-21" }; - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid3)); - } - - /// - /// Verifies a single grant is successful and does not modify other assignments. - /// - [TestMethod] - public void GrantPasses() - { - // Arrange. - var principals1 = new List - { - PrincipalSid1 - }; - - var principals2 = new List - { - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - - policy.ResetConnection(); - - using var fixture = new CliBuilderFixture(policy); - - var rootCommand = fixture.CliBuilder.Build(); - - var args = new[] { "privilege", Privilege2, "--grant", PrincipalName1 }; - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - } - - /// - /// Verifies revoking all principals for a privilege is successful and does not modify other assignments. - /// - [TestMethod] - public void RevokeAllPasses() - { - // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege1)); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege2)); - - policy.ResetConnection(); - - using var fixture = new CliBuilderFixture(policy); - - var rootCommand = fixture.CliBuilder.Build(); - - var args = new[] { "privilege", Privilege1, "--revoke-all" }; - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - Assert.IsEmpty(policy.LsaEnumerateAccountsWithUserRight(Privilege1)); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight(Privilege2)); - } - - /// - /// Verifies a single revocation is successful and does not modify other assignments. - /// - [TestMethod] - public void RevokePasses() - { - // Arrange. - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; - - var principals2 = new List - { - PrincipalSid2 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - - policy.ResetConnection(); - - using var fixture = new CliBuilderFixture(policy); - - var rootCommand = fixture.CliBuilder.Build(); - - var args = new[] { "privilege", Privilege1, "--revoke", PrincipalName2 }; - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - } - - /// - /// Verifies revoking all non-builtin and virtual principals from a privilege is successful. - /// - [TestMethod] - public void RevokePatternForAllButBuiltinAndVirtualPasses() - { - // Arrange. - var principals1 = new List - { - PrincipalSidCurrent, - PrincipalSid2, - PrincipalSid3 - }; - - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2, - PrincipalSid3 - }; - - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); - - CollectionAssert.AreEquivalent(new[] { PrincipalSidCurrent, PrincipalSid1, PrincipalSid2, PrincipalSid3 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSidCurrent)); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid3)); - - policy.ResetConnection(); - - using var fixture = new CliBuilderFixture(policy); - - var rootCommand = fixture.CliBuilder.Build(); - - var args = new[] { "privilege", Privilege1, "--revoke-pattern", "^S-1-5-21" }; - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2, PrincipalSid3 }, policy.LsaEnumerateAccountsWithUserRight()); - CollectionAssert.AreEqual(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid3)); - } -} \ No newline at end of file diff --git a/src/Tests.Cli/PrivilegeSyntaxTests.cs b/src/Tests.Cli/PrivilegeSyntaxTests.cs deleted file mode 100644 index 44b52e5..0000000 --- a/src/Tests.Cli/PrivilegeSyntaxTests.cs +++ /dev/null @@ -1,377 +0,0 @@ -namespace Tests.Cli; - -using UserRights.Cli; - -/// -/// Represents syntax tests for privilege functionality. -/// -[TestClass] -public sealed class PrivilegeSyntaxTests : CliBuilderFixture -{ - /// - /// Ensures granting a context and revoking a different context is accepted. - /// - [TestMethod] - public void GrantAndRevokeShouldWork() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\User", "--revoke", "DOMAIN\\Group" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Ensures granting multiple contexts is accepted. - /// - [TestMethod] - public void GrantMultipleShouldWork() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\User", "--grant", "DOMAIN\\Group" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Ensures granting a context is accepted. - /// - [TestMethod] - public void GrantShouldWork() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\User" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Ensures an empty or whitespace grant is rejected. - /// - /// The test arguments. - [TestMethod] - [DataRow("privilege", "SeServiceLogonRight", "--grant", "")] - [DataRow("privilege", "SeServiceLogonRight", "--grant", " ")] - public void GrantWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); - - /// - /// Ensures granting a context and revoking all contexts is rejected. - /// - [TestMethod] - public void GrantWithRevokeAllThrowsException() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\User", "--revoke-all" }; - var rootCommand = CliBuilder.Build(); - - // Act & Assert. - Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures specifying no options is rejected. - /// - [TestMethod] - public void NoOptionsThrowsException() - { - // Arrange. - var args = new[] { "privilege" }; - var rootCommand = CliBuilder.Build(); - - // Act & Assert. - Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures overlapping or duplicate contexts are rejected. - /// - /// The test arguments. - [TestMethod] - [DataRow("privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--revoke", "DOMAIN\\UserOrGroup")] - [DataRow("privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--grant", "DOMAIN\\UserOrGroup")] - [DataRow("privilege", "SeServiceLogonRight", "--revoke", "DOMAIN\\UserOrGroup", "--revoke", "DOMAIN\\UserOrGroup")] - public void OverlappingGrantsAndRevokesThrowsException(params string[] args) - => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); - - /// - /// Ensures an empty or whitespace principal is rejected. - /// - /// The test arguments. - [TestMethod] - [DataRow("privilege", "", "--grant", "DOMAIN\\UserOrGroup")] - [DataRow("privilege", " ", "--grant", "DOMAIN\\UserOrGroup")] - public void PrivilegeWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); - - /// - /// Ensures revoking all contexts is accepted. - /// - [TestMethod] - public void RevokeAllShouldWork() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-all" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Ensures granting a context and revoking all contexts is rejected. - /// - [TestMethod] - public void RevokeAllWithGrantsThrowsException() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-all", "--grant", "DOMAIN\\UserOrGroup" }; - var rootCommand = CliBuilder.Build(); - - // Act & Assert. - Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures revoking a context and revoking all contexts is rejected. - /// - [TestMethod] - public void RevokeAllWithRevocationsThrowsException() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-all", "--revoke", "DOMAIN\\UserOrGroup" }; - var rootCommand = CliBuilder.Build(); - - // Act & Assert. - Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures revoking all contexts and revoking other contexts is rejected. - /// - [TestMethod] - public void RevokeAllWithRevokeOthersThrowsException() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-all", "--revoke-others" }; - var rootCommand = CliBuilder.Build(); - - // Act & Assert. - Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures revoking multiple contexts is accepted. - /// - [TestMethod] - public void RevokeMultipleShouldWork() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke", "DOMAIN\\User", "--revoke", "DOMAIN\\Group" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Ensures revoke other contexts without granting a context is rejected. - /// - [TestMethod] - public void RevokeOthersWithOutGrantsThrowsException() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-others" }; - var rootCommand = CliBuilder.Build(); - - // Act & Assert. - Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures revoke other contexts with revoking a context is rejected. - /// - [TestMethod] - public void RevokeOthersWithRevocationsThrowsException() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-others", "--revoke", "DOMAIN\\UserOrGroup" }; - var rootCommand = CliBuilder.Build(); - - // Act & Assert. - Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures revoking a valid pattern is accepted. - /// - [TestMethod] - public void RevokePatternShouldWork() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Ensures granting a context and revoking a valid pattern is accepted. - /// - [TestMethod] - public void RevokePatternWithGrantShouldWork() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--revoke-pattern", "^S-1-5-21-" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Ensures revoking a valid regex is accepted. - /// - /// The test arguments. - [TestMethod] - [DataRow("privilege", "SeServiceLogonRight", "--revoke-pattern", "^xyz.*")] - [DataRow("privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-")] - [DataRow("privilege", "SeServiceLogonRight", "--revoke-pattern", "(?i)^[A-Z]+")] - public void RevokePatternWithValidRegexShouldWork(params string[] args) - { - // Arrange. - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Ensures revoking a pattern and revoking all contexts is rejected. - /// - [TestMethod] - public void RevokePatternWithRevokeAllThrowsException() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-", "--revoke-all" }; - var rootCommand = CliBuilder.Build(); - - // Act & Assert. - Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures revoking a pattern and revoking other contexts is rejected. - /// - [TestMethod] - public void RevokePatternWithRevokeOthersThrowsException() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-", "--revoke-others" }; - var rootCommand = CliBuilder.Build(); - - // Act & Assert. - Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures revoking a pattern and revoking a context is rejected. - /// - [TestMethod] - public void RevokePatternWithRevokeThrowsException() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-", "--revoke", "DOMAIN\\UserOrGroup" }; - var rootCommand = CliBuilder.Build(); - - // Act & Assert. - Assert.Throws(() => rootCommand.Parse(args).ThrowIfInvalid().Run()); - } - - /// - /// Ensures revoking an invalid regex is rejected. - /// - /// The test arguments. - [TestMethod] - [DataRow("privilege", "SeServiceLogonRight", "--revoke-pattern", "[0-9]{3,1}")] - [DataRow("privilege", "SeServiceLogonRight", "--revoke-pattern", "^[S-1-5-21-")] - public void RevokePatternWithInvalidRegexThrowsException(params string[] args) - => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); - - /// - /// Ensures an empty or whitespace revocation pattern is rejected. - /// - /// The test arguments. - [TestMethod] - [DataRow("privilege", "SeServiceLogonRight", "--revoke-pattern", "")] - [DataRow("privilege", "SeServiceLogonRight", "--revoke-pattern", " ")] - public void RevokePatternWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); - - /// - /// Ensures revoking a context is accepted. - /// - [TestMethod] - public void RevokeShouldWork() - { - // Arrange. - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke", "DOMAIN\\UserOrGroup" }; - var rootCommand = CliBuilder.Build(); - - // Act. - var rc = rootCommand.Parse(args).ThrowIfInvalid().Run(); - - // Assert. - Assert.AreEqual(0, rc); - } - - /// - /// Ensures an empty or whitespace revocation is rejected. - /// - /// The test arguments. - [TestMethod] - [DataRow("privilege", "SeServiceLogonRight", "--revoke", "")] - [DataRow("privilege", "SeServiceLogonRight", "--revoke", " ")] - public void RevokeWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); - - /// - /// Ensures an empty or whitespace system name is rejected. - /// - /// The test arguments. - [TestMethod] - [DataRow("privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--system-name", "")] - [DataRow("privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--system-name", " ")] - public void SystemNameWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => CliBuilder.Build().Parse(args).ThrowIfInvalid().Run()); -} \ No newline at end of file diff --git a/src/Tests.Cli/Tests.Cli.csproj b/src/Tests.Cli/Tests.Cli.csproj index 67f6e28..b840c3c 100644 --- a/src/Tests.Cli/Tests.Cli.csproj +++ b/src/Tests.Cli/Tests.Cli.csproj @@ -13,6 +13,11 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Tests/CliBuilderFixture.cs b/src/Tests/CliMockBuilder.cs similarity index 54% rename from src/Tests/CliBuilderFixture.cs rename to src/Tests/CliMockBuilder.cs index 9aa438a..6b49afc 100644 --- a/src/Tests/CliBuilderFixture.cs +++ b/src/Tests/CliMockBuilder.cs @@ -1,51 +1,88 @@ namespace Tests; -using System.Security.Principal; +using System.Text.RegularExpressions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Moq; + using UserRights.Application; using UserRights.Cli; /// /// Represents a test fixture with a implementation for testing the CLI. /// -public class CliBuilderFixture : IDisposable +public class CliMockBuilder : IDisposable { private readonly ServiceProvider _serviceProvider; private bool _disposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// - /// Creates a fixture with an empty, mock implementation, and a mock implementation. + /// Creates a CLI with an empty, mock implementation, and a mock implementation. /// - public CliBuilderFixture() + public CliMockBuilder() { + var repository = new MockRepository(MockBehavior.Strict); + + // Mock the LSA user rights interface. + var lsaUserRights = repository.Create(); + + // Only calls to Connect are expected. + lsaUserRights.Setup(x => x.Connect(It.IsAny())); + + // Mock the user rights manager interface. + var userRightsManager = repository.Create(); + userRightsManager + .Setup(x => x.GetUserRights(It.IsAny())) + .Returns([]); + + userRightsManager + .Setup(x => x.ModifyPrincipal( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())); + + userRightsManager + .Setup(x => x.ModifyPrivilege( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())); + var serviceCollection = new ServiceCollection() .AddLogging(builder => builder .ClearProviders() .SetMinimumLevel(LogLevel.Trace) .AddDebug()); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(lsaUserRights.Object); + serviceCollection.AddSingleton(userRightsManager.Object); serviceCollection.AddSingleton(); _serviceProvider = serviceCollection.BuildServiceProvider(); } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The existing LSA user rights implementation. /// - /// Creates a fixture with a user-supplied implementation, and a complete instance of a implementation. + /// Creates a CLI with a user-supplied implementation, and a complete instance of a implementation. /// - public CliBuilderFixture(ILsaUserRights policy) + public CliMockBuilder(ILsaUserRights policy) { ArgumentNullException.ThrowIfNull(policy); @@ -62,34 +99,6 @@ public CliBuilderFixture(ILsaUserRights policy) _serviceProvider = serviceCollection.BuildServiceProvider(); } - /// - /// Initializes a new instance of the class. - /// - /// The entries to include in the mock policy database. - /// The remote system name to execute the task on (default localhost). - /// - /// Creates a fixture with user-supplied, existing policy entries, and mock instances of a and implementations. - /// - public CliBuilderFixture(IDictionary> database, string? systemName = null) - { - ArgumentNullException.ThrowIfNull(database); - - var serviceCollection = new ServiceCollection() - .AddLogging(builder => builder - .ClearProviders() - .SetMinimumLevel(LogLevel.Trace) - .AddDebug()); - - var policy = new MockLsaUserRights(database); - policy.Connect(systemName); - - serviceCollection.AddSingleton(policy); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - - _serviceProvider = serviceCollection.BuildServiceProvider(); - } - /// /// Gets a CLI builder with a mock implementation of . /// diff --git a/src/Tests/LsaUserRightsMockBuilder.cs b/src/Tests/LsaUserRightsMockBuilder.cs new file mode 100644 index 0000000..a150a7a --- /dev/null +++ b/src/Tests/LsaUserRightsMockBuilder.cs @@ -0,0 +1,167 @@ +namespace Tests; + +using System.Collections.Immutable; +using System.Security.Principal; + +using Moq; + +using UserRights.Application; + +/// +/// Represents a builder for creating mocks of the interface for testing purposes. +/// +public sealed class LsaUserRightsMockBuilder +{ + private readonly Dictionary> _database = []; + private string? _systemName; + + /// + /// Prevents a default instance of the class from being created. + /// + private LsaUserRightsMockBuilder() + { + } + + /// + /// Gets an immutable copy of the internal database of user rights assignments. + /// + public ImmutableDictionary> Database + => _database + .Where(kvp => kvp.Value.Count > 0) + .ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableList()); + + /// + /// Creates a new instance of the class. + /// + /// An instance of the class. + public static LsaUserRightsMockBuilder CreateBuilder() => new(); + + /// + /// Builds and returns a mock of the interface. + /// + /// A mock instance of interface. + public Mock Build() + { + var mock = new Mock(MockBehavior.Strict); + + // Configure the mock to handle the Connect(string) method. + if (string.IsNullOrWhiteSpace(_systemName)) + { + mock.Setup(x => x.Connect(It.IsAny())); + } + else + { + mock.Setup(x => x.Connect(It.Is(s => string.Equals(s, _systemName, StringComparison.Ordinal)))); + } + + // Configure the mock to handle the LsaAddAccountRights(SecurityIdentifier, string[]) method. + mock.Setup(x => x.LsaAddAccountRights(It.IsAny(), It.IsAny())).Callback((SecurityIdentifier accountSid, string[] userRights) => + { + if (_database.TryGetValue(accountSid, out var assignments)) + { + assignments.UnionWith(userRights); + } + else + { + _database[accountSid] = new(userRights, StringComparer.OrdinalIgnoreCase); + } + }); + + // Configure the mock to handle the LsaEnumerateAccountRights(SecurityIdentifier) method. + mock.Setup(x => x.LsaEnumerateAccountRights(It.IsAny())).Returns((SecurityIdentifier accountSid) => + { + if (_database.TryGetValue(accountSid, out var assignments)) + { + return [.. assignments]; + } + + return []; + }); + + // Configure the mock to handle the LsaEnumerateAccountsWithUserRight(string?) method. + mock.Setup(x => x.LsaEnumerateAccountsWithUserRight(It.IsAny())).Returns((string? userRight) => + { + if (string.IsNullOrWhiteSpace(userRight)) + { + return [.. _database.Keys]; + } + + return [.. _database.Where(kvp => kvp.Value.Contains(userRight)).Select(kvp => kvp.Key)]; + }); + + // Configure the mock to handle the LsaRemoveAccountRights(SecurityIdentifier, string[]) method. + mock.Setup(x => x.LsaRemoveAccountRights(It.IsAny(), It.IsAny())).Callback((SecurityIdentifier accountSid, string[] userRights) => + { + if (_database.TryGetValue(accountSid, out var assignments)) + { + assignments.ExceptWith(userRights); + } + }); + + return mock; + } + + /// + /// Sets the system name to be used for the call to . + /// + /// The remote system name to execute the task on (default localhost). + /// The current instance of with the updated system name. + public LsaUserRightsMockBuilder WithSystemName(string systemName) + { + ArgumentException.ThrowIfNullOrWhiteSpace(systemName); + + _systemName = systemName; + + return this; + } + + /// + /// Adds a principal and associated right to the existing collection of assignments. + /// + /// The principal that should be assigned the rights. + /// The privileges that the principal should possess. + /// The current instance of with the updated system name. + public LsaUserRightsMockBuilder WithGrant(SecurityIdentifier accountSid, params string[] userRights) + { + ArgumentNullException.ThrowIfNull(accountSid); + ArgumentNullException.ThrowIfNull(userRights); + ArgumentOutOfRangeException.ThrowIfZero(userRights.Length, nameof(userRights)); + + if (_database.TryGetValue(accountSid, out var assignments)) + { + assignments.ExceptWith(userRights); + } + else + { + _database[accountSid] = new(userRights, StringComparer.OrdinalIgnoreCase); + } + + return this; + } + + /// + /// Adds a principal and associated right to the existing collection of assignments. + /// + /// The principal and privilege sequence to assign. + /// The current instance of with the updated system name. + public LsaUserRightsMockBuilder WithGrant(params UserRightEntry[] entries) + { + ArgumentNullException.ThrowIfNull(entries); + ArgumentOutOfRangeException.ThrowIfZero(entries.Length, nameof(entries)); + + foreach (var entry in entries) + { + var accountSid = new SecurityIdentifier(entry.SecurityId); + if (_database.TryGetValue(accountSid, out var assignments)) + { + assignments.Add(entry.Privilege); + } + else + { + _database[accountSid] = new(StringComparer.OrdinalIgnoreCase) { entry.Privilege }; + } + } + + return this; + } +} \ No newline at end of file diff --git a/src/Tests/LsaUserRightsSnapshotFixture.cs b/src/Tests/LsaUserRightsSnapshotFixture.cs index 94e36d7..5dcbf1b 100644 --- a/src/Tests/LsaUserRightsSnapshotFixture.cs +++ b/src/Tests/LsaUserRightsSnapshotFixture.cs @@ -17,7 +17,7 @@ namespace Tests; /// /// This fixture creates a temporary directory and backs up the security database when instantiated, then restores it during disposal. /// -public abstract class LsaUserRightsSnapshotFixture : IDisposable +public class LsaUserRightsSnapshotFixture : IDisposable { private const string ExportSecurityTemplateName = "export.ini"; private const string ExportSecurityLogName = "export.log"; @@ -25,7 +25,15 @@ public abstract class LsaUserRightsSnapshotFixture : IDisposable private const string RestoreSecurityTemplateName = "restore.ini"; private const string RestoreSecurityLogName = "restore.log"; - private readonly DirectoryInfo? _directory = CreateTempDirectory(); + private readonly string _exportSecurityArguments = string.Create( + CultureInfo.InvariantCulture, + $"/export /cfg {ExportSecurityTemplateName} /areas user_rights /log {ExportSecurityLogName}"); + + private readonly string _restoreSecurityArguments = string.Create( + CultureInfo.InvariantCulture, + $"/configure /db {RestoreSecurityDatabaseName} /cfg {RestoreSecurityTemplateName} /areas user_rights /log {RestoreSecurityLogName}"); + + private readonly DirectoryInfo? _directory; private readonly IReadOnlyDictionary> _initialState; private bool _disposed; @@ -33,12 +41,14 @@ public abstract class LsaUserRightsSnapshotFixture : IDisposable /// /// Initializes a new instance of the class. /// - protected LsaUserRightsSnapshotFixture() + public LsaUserRightsSnapshotFixture() { + _directory = CreateTempDirectory(); + try { // Create a backup to restore during disposal. - CreateSecurityDatabaseBackup(_directory.FullName); + RunSecurityEditor(_exportSecurityArguments, _directory.FullName); // Load the contents of the backup for use as initial state. _initialState = ReadSecurityDatabaseBackup(_directory.FullName); @@ -48,6 +58,7 @@ protected LsaUserRightsSnapshotFixture() } catch { + // Prevent disposal from restoring the backup or deleting the temporary directory if initialization fails. _directory = null; throw; @@ -57,7 +68,7 @@ protected LsaUserRightsSnapshotFixture() /// /// Gets the initial state of user rights assignments before they are modified through test execution. /// - protected IReadOnlyDictionary> InitialState + public IReadOnlyDictionary> InitialState { get { @@ -67,6 +78,27 @@ protected IReadOnlyDictionary> I } } + /// + /// Gets the current state of the security database. + /// + /// A map of privilege to security identifiers. + public IReadOnlyDictionary> GetCurrentState() + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var directoryInfo = CreateTempDirectory(); + try + { + RunSecurityEditor(_exportSecurityArguments, directoryInfo.FullName); + + return ReadSecurityDatabaseBackup(directoryInfo.FullName); + } + finally + { + directoryInfo.Delete(true); + } + } + /// public void Dispose() { @@ -89,7 +121,7 @@ protected virtual void Dispose(bool disposing) { if (_directory is not null) { - RestoreSecurityDatabaseBackup(_directory.FullName); + RunSecurityEditor(_restoreSecurityArguments, _directory.FullName); _directory.Delete(true); } @@ -98,25 +130,6 @@ protected virtual void Dispose(bool disposing) _disposed = true; } - /// - /// Gets the current state of the security database. - /// - /// A map of privilege to security identifiers. - protected IReadOnlyDictionary> GetCurrentState() - { - ObjectDisposedException.ThrowIf(_disposed, this); - - var directoryInfo = CreateTempDirectory(); - - CreateSecurityDatabaseBackup(directoryInfo.FullName); - - var results = ReadSecurityDatabaseBackup(directoryInfo.FullName); - - directoryInfo.Delete(true); - - return results; - } - /// /// Creates an updated restore template. /// @@ -162,65 +175,11 @@ private static void CreateRestoreTemplate(string workingDirectory, IReadOnlyDict File.WriteAllLines(pathRestore, lines, Encoding.Unicode); } - /// - /// Creates a backup of the security database. - /// - /// The path to a directory where the backup files will be created. - private static void CreateSecurityDatabaseBackup(string workingDirectory) - { - ArgumentException.ThrowIfNullOrWhiteSpace(workingDirectory); - - var arguments = string.Create( - CultureInfo.InvariantCulture, - $"/export /cfg {ExportSecurityTemplateName} /areas user_rights /log {ExportSecurityLogName}"); - - var stringBuilder = new StringBuilder(); - - using var process = new Process(); - - process.StartInfo.FileName = "secedit.exe"; - process.StartInfo.Arguments = arguments; - process.StartInfo.WorkingDirectory = workingDirectory; - process.StartInfo.CreateNoWindow = true; - process.StartInfo.UseShellExecute = false; - process.StartInfo.RedirectStandardError = true; - - process.ErrorDataReceived += (_, args) => stringBuilder.AppendLine(args.Data); - - process.Start(); - - process.BeginErrorReadLine(); - - process.WaitForExit(); - - if (process.ExitCode != 0) - { - var message = string.Create( - CultureInfo.InvariantCulture, - $"Failed to export the security database, exit code: {process.ExitCode}\r\n{stringBuilder}"); - - throw new InvalidOperationException(message); - } - } - /// /// Creates a temporary directory. /// /// The temporary directory info instance. - private static DirectoryInfo CreateTempDirectory() - { - var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - var directoryInfo = new DirectoryInfo(path); - - if (directoryInfo.Exists) - { - throw new InvalidOperationException("Failed to create temporary directory."); - } - - directoryInfo.Create(); - - return directoryInfo; - } + private static DirectoryInfo CreateTempDirectory() => Directory.CreateTempSubdirectory("userrights-"); /// /// Reads a backup of the security database. @@ -269,16 +228,14 @@ private static ReadOnlyDictionary - /// Restores a backup of the security database. + /// Executes the security editor utility. /// + /// The command line arguments to pass to the security editor utility. /// The path to a directory where the backup files exist. - private static void RestoreSecurityDatabaseBackup(string workingDirectory) + private static void RunSecurityEditor(string arguments, string workingDirectory) { ArgumentException.ThrowIfNullOrWhiteSpace(workingDirectory); - - var arguments = string.Create( - CultureInfo.InvariantCulture, - $"/configure /db {RestoreSecurityDatabaseName} /cfg {RestoreSecurityTemplateName} /areas user_rights /log {RestoreSecurityLogName}"); + ArgumentException.ThrowIfNullOrWhiteSpace(workingDirectory); var stringBuilder = new StringBuilder(); @@ -303,7 +260,7 @@ private static void RestoreSecurityDatabaseBackup(string workingDirectory) { var message = string.Create( CultureInfo.InvariantCulture, - $"Failed to restore the security database, exit code: {process.ExitCode}\r\n{stringBuilder}"); + $"Failed to execute the security editor utility, exit code: {process.ExitCode}\r\n{stringBuilder}"); throw new InvalidOperationException(message); } diff --git a/src/Tests/MockLsaUserRights.cs b/src/Tests/MockLsaUserRights.cs deleted file mode 100644 index 2bb8f12..0000000 --- a/src/Tests/MockLsaUserRights.cs +++ /dev/null @@ -1,139 +0,0 @@ -namespace Tests; - -using System.Security.Principal; - -using UserRights.Application; - -/// -/// Represents a mock implementation. -/// -public sealed class MockLsaUserRights : ILsaUserRights -{ - private readonly IDictionary> _database = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); - private bool _connected; - - /// - /// Initializes a new instance of the class. - /// - public MockLsaUserRights() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// A map of privilege to assigned principals. - public MockLsaUserRights(IDictionary> database) - { - ArgumentNullException.ThrowIfNull(database); - - foreach (var kvp in database) - { - _database.Add(kvp.Key, kvp.Value); - } - } - - /// - public void Connect(string? systemName = null) - { - if (_connected) - { - throw new InvalidOperationException("A connection to the policy database already exists."); - } - - _connected = true; - } - - /// - public void LsaAddAccountRights(SecurityIdentifier accountSid, params string[] userRights) - { - ArgumentNullException.ThrowIfNull(accountSid); - ArgumentNullException.ThrowIfNull(userRights); - ArgumentOutOfRangeException.ThrowIfZero(userRights.Length, nameof(userRights)); - - if (!_connected) - { - throw new InvalidOperationException("A connection to the policy database is required."); - } - - foreach (var userRight in userRights) - { - if (_database.TryGetValue(userRight, out var accountSids)) - { - if (!accountSids.Contains(accountSid)) - { - accountSids.Add(accountSid); - } - } - else - { - accountSids = [accountSid]; - - _database.Add(userRight, accountSids); - } - } - } - - /// - public string[] LsaEnumerateAccountRights(SecurityIdentifier accountSid) - { - ArgumentNullException.ThrowIfNull(accountSid); - - if (!_connected) - { - throw new InvalidOperationException("A connection to the policy database is required."); - } - - return [.. _database.Where(p => p.Value.Contains(accountSid)).Select(p => p.Key)]; - } - - /// - public SecurityIdentifier[] LsaEnumerateAccountsWithUserRight(string? userRight = null) - { - if (!_connected) - { - throw new InvalidOperationException("A connection to the policy database is required."); - } - - if (string.IsNullOrWhiteSpace(userRight)) - { - return [.. _database.Values.SelectMany(p => p).Distinct()]; - } - - if (_database.TryGetValue(userRight, out var accountSids)) - { - return [.. accountSids]; - } - - return []; - } - - /// - public void LsaRemoveAccountRights(SecurityIdentifier accountSid, params string[] userRights) - { - ArgumentNullException.ThrowIfNull(accountSid); - ArgumentNullException.ThrowIfNull(userRights); - ArgumentOutOfRangeException.ThrowIfZero(userRights.Length, nameof(userRights)); - - if (!_connected) - { - throw new InvalidOperationException("A connection to the policy database is required."); - } - - foreach (var userRight in userRights) - { - if (_database.TryGetValue(userRight, out var principals) && principals.Contains(accountSid)) - { - principals.Remove(accountSid); - } - } - } - - /// - /// Allow a test to assert the policy database before manipulating it. - /// - public void ResetConnection() => _connected = false; - - /// - public override string ToString() => $"{string.Join(" | ", _database.Select(p => $"{p.Key}: {string.Join(',', p.Value)}"))}"; -} \ No newline at end of file diff --git a/src/Tests/MockUserRightsManager.cs b/src/Tests/MockUserRightsManager.cs deleted file mode 100644 index 2d3f204..0000000 --- a/src/Tests/MockUserRightsManager.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Tests; - -using System.Text.RegularExpressions; - -using UserRights.Application; - -/// -/// Represents a mock user rights manager with noop interface implementations for testing the CLI. -/// -public class MockUserRightsManager : IUserRightsManager -{ - /// - public IEnumerable GetUserRights(IUserRights policy) => []; - - /// - public void ModifyPrincipal(IUserRights policy, string principal, string[] grants, string[] revocations, bool revokeAll, bool revokeOthers, bool dryRun) - { - } - - /// - public void ModifyPrivilege(IUserRights policy, string privilege, string[] grants, string[] revocations, bool revokeAll, bool revokeOthers, Regex? revokePattern, bool dryRun) - { - } -} \ No newline at end of file diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj index 7e2cf0a..9dd04bd 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -17,6 +17,11 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive +