diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6a06894..68bc0bb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,17 +30,25 @@ 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 "label=$label" >> $env:GITHUB_OUTPUT - - name: Run tests - run: dotnet.exe test .\src\UserRights.sln --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 - name: Clean solution - run: dotnet.exe clean .\src\UserRights.sln --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 @@ -58,7 +66,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/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..bff753c --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,96 @@ +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 + + - 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/.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.Build.props b/src/Directory.Build.props index 2b9f876..c85dca0 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,15 +4,16 @@ Windows User Rights Assignment Utility Utility for managing user right assignments. Copyright © Joseph L. Casale 2022 + false true - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -24,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) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props new file mode 100644 index 0000000..69bd126 --- /dev/null +++ b/src/Directory.Packages.props @@ -0,0 +1,27 @@ + + + true + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Tests.Application/.editorconfig b/src/Tests.Application/.editorconfig new file mode 100644 index 0000000..26f6cee --- /dev/null +++ b/src/Tests.Application/.editorconfig @@ -0,0 +1,9 @@ +[*.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. + +[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 deleted file mode 100644 index 1a8c063..0000000 --- a/src/Tests.Application/LsaUserRightsConnectTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Tests.Application; - -using System; - -using UserRights.Application; -using Xunit; - -/// -/// Represents tests for connection functionality. -/// -[Collection("lsa")] -public class LsaUserRightsConnectTests -{ - /// - /// Tests that only a single connection to the local security authority is allowed. - /// - [AdminOnlyFact] - public void MultipleConnectionsThrowsException() - { - using var policy = new LsaUserRights(); - policy.Connect(); - - 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 818ebc6..0000000 --- a/src/Tests.Application/LsaUserRightsDisposeTests.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Tests.Application; - -using UserRights.Application; -using Xunit; - -/// -/// Represents tests for disposal functionality. -/// -[Collection("lsa")] -public class LsaUserRightsDisposeTests -{ - /// - /// Tests whether dispose can be successfully called multiple times. - /// - [Fact] - public void CanBeDisposedMultipleTimes() - { - var policy = new LsaUserRights(); - - policy.Dispose(); - - var exception = Record.Exception(policy.Dispose); - - Assert.Null(exception); - } -} \ 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 6db92cc..0000000 --- a/src/Tests.Application/LsaUserRightsGetPrincipalsTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -namespace Tests.Application; - -using System; -using System.Linq; -using System.Security.Principal; - -using UserRights.Application; -using Xunit; - -using static Tests.PrivilegeConstants; -using static Tests.SecurityIdentifierConstants; - -/// -/// Represents tests for list functionality. -/// -[Collection("lsa")] -public sealed class LsaUserRightsGetPrincipalsTests : LsaUserRightsTestBase -{ - /// - /// Tests listing all the principals assigned to all privileges. - /// - [AdminOnlyFact] - public void GetPrincipalsShouldWork() - { - var expected = InitialState.Values - .SelectMany(p => p) - .Distinct() - .Order() - .ToArray(); - - using var policy = new LsaUserRights(); - policy.Connect(); - - var actual = policy.LsaEnumerateAccountsWithUserRight() - .Order() - .ToArray(); - - Assert.Equal(expected, actual); - } - - /// - /// Tests listing the principals assigned to a single privilege. - /// - /// - /// We assume the BUILTIN\Administrators group is granted the SeTakeOwnershipPrivilege privilege. - /// - [AdminOnlyFact] - public void GetPrincipalsSinglePrivilegeShouldWork() - { - var securityIdentifier = new SecurityIdentifier(Administrators); - - using var policy = new LsaUserRights(); - policy.Connect(); - - var collection = policy.LsaEnumerateAccountsWithUserRight(SeTakeOwnershipPrivilege).ToArray(); - - Assert.Contains(securityIdentifier, collection); - } - - /// - /// Tests listing all the principals assigned to all privileges without connecting throws an exception. - /// - [AdminOnlyFact] - public void GetPrincipalsWithoutConnectingThrowsException() - { - using var policy = new LsaUserRights(); - - Assert.Throws(() => policy.LsaEnumerateAccountsWithUserRight().ToArray()); - } - - /// - /// Tests listing the principals assigned to a single privilege without connecting throws an exception. - /// - [AdminOnlyFact] - public void GetPrincipalsSinglePrivilegeWithoutConnectingThrowsException() - { - using var policy = new LsaUserRights(); - - Assert.Throws(() => policy.LsaEnumerateAccountsWithUserRight(SeTakeOwnershipPrivilege).ToArray()); - } -} \ 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 4392c17..0000000 --- a/src/Tests.Application/LsaUserRightsGrantPrivilegeTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace Tests.Application; - -using System; -using System.Security.Principal; - -using UserRights.Application; -using Xunit; - -using static Tests.PrivilegeConstants; -using static Tests.SecurityIdentifierConstants; - -/// -/// Represents tests for grant functionality. -/// -[Collection("lsa")] -public sealed class LsaUserRightsGrantPrivilegeTests : LsaUserRightsTestBase -{ - /// - /// Tests granting a privilege. - /// - /// - /// We assume the BUILTIN\Users group is not granted the SeMachineAccountPrivilege privilege. - /// - [AdminOnlyFact] - public void GrantPrivilegeShouldWork() - { - var securityIdentifier = new SecurityIdentifier(Users); - - if (InitialState.TryGetValue(SeMachineAccountPrivilege, out var initial)) - { - Assert.DoesNotContain(securityIdentifier, initial); - } - - using var policy = new LsaUserRights(); - policy.Connect(); - policy.LsaAddAccountRights(securityIdentifier, SeMachineAccountPrivilege); - - var current = GetCurrentState(); - - current.TryGetValue(SeMachineAccountPrivilege, out var collection); - - Assert.NotNull(collection); - Assert.Contains(securityIdentifier, collection); - } - - /// - /// Tests granting a privilege without connecting throws an exception. - /// - [AdminOnlyFact] - public void GrantPrivilegeWithoutConnectingThrowsException() - { - var securityIdentifier = new SecurityIdentifier(Users); - - using var policy = new LsaUserRights(); - - 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 f6fa5a0..0000000 --- a/src/Tests.Application/LsaUserRightsRevokePrivilegeTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace Tests.Application; - -using System; -using System.Security.Principal; - -using UserRights.Application; -using Xunit; - -using static Tests.PrivilegeConstants; -using static Tests.SecurityIdentifierConstants; - -/// -/// Represents tests for revoke functionality. -/// -[Collection("lsa")] -public sealed class LsaUserRightsRevokePrivilegeTests : LsaUserRightsTestBase -{ - /// - /// Tests revoking a privilege. - /// - /// - /// We assume the BUILTIN\Backup Operators is granted the SeBackupPrivilege privilege. - /// - [AdminOnlyFact] - public void RevokePrivilegeShouldWork() - { - var securityIdentifier = new SecurityIdentifier(BackupOperators); - - InitialState.TryGetValue(SeBackupPrivilege, out var initial); - - Assert.NotNull(initial); - Assert.Contains(securityIdentifier, initial); - - 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); - } - } - - /// - /// Tests revoking a privilege without connecting throws an exception. - /// - [AdminOnlyFact] - public void RevokePrivilegeWithoutConnectingThrowsException() - { - var securityIdentifier = new SecurityIdentifier(BackupOperators); - - using var policy = new LsaUserRights(); - - 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 new file mode 100644 index 0000000..6b35270 --- /dev/null +++ b/src/Tests.Application/MSTestSettings.cs @@ -0,0 +1 @@ +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] \ No newline at end of file diff --git a/src/Tests.Application/Tests.Application.csproj b/src/Tests.Application/Tests.Application.csproj index e96f27f..960b8a5 100644 --- a/src/Tests.Application/Tests.Application.csproj +++ b/src/Tests.Application/Tests.Application.csproj @@ -1,38 +1,18 @@ - + - net9.0-windows + Exe + net10.0-windows + enable enable latest latest-All - + All false - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - \ No newline at end of file diff --git a/src/Tests.Application/UserRightsManagerListTests.cs b/src/Tests.Application/UserRightsManagerListTests.cs index 530c783..bb913d9 100644 --- a/src/Tests.Application/UserRightsManagerListTests.cs +++ b/src/Tests.Application/UserRightsManagerListTests.cs @@ -1,162 +1,40 @@ 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; -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. +/// Represents tests for enumerating all user rights. /// -public sealed class UserRightsManagerListTests : UserRightsManagerTestBase +[TestClass] +public class UserRightsManagerListTests { /// - /// Verifies invalid arguments throw an instance of . - /// - [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() - { - 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([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. + /// Verifies enumerating all user rights works as expected. /// - /// A task that represents the asynchronous write operation. - [Fact] - public async Task SerializingToJsonShouldWork() + [TestMethod] + public void GetUserRights_ShouldWork() { - 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()); - - 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); - - var actual = JsonSerializer.Deserialize(serialized) - ?.OrderBy(p => p.Privilege, StringComparer.OrdinalIgnoreCase) - .ThenBy(p => p.SecurityId, StringComparer.OrdinalIgnoreCase) - .ToArray() ?? []; - - Assert.Equal(expected, actual, new UserRightEntryEqualityComparer()); + // Arrange. + 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(lsaUserRights.Object).ToArray(); + + // 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 3e5fd15..ac2786a 100644 --- a/src/Tests.Application/UserRightsManagerPrincipalTests.cs +++ b/src/Tests.Application/UserRightsManagerPrincipalTests.cs @@ -1,274 +1,361 @@ namespace Tests.Application; -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Principal; -using Microsoft.Extensions.DependencyInjection; -using UserRights.Application; -using Xunit; +using Moq; using static Tests.TestData; /// -/// Represents tests for modify principal functionality. +/// Represents tests for modifying the user rights for a specified principal. /// -public sealed class UserRightsManagerPrincipalTests : UserRightsManagerTestBase +[TestClass] +public class UserRightsManagerPrincipalTests { /// - /// Generates invalid method arguments for the method. + /// Gets invalid method arguments for the modify principal unit test. /// - /// A sequence of method arguments. - public static TheoryData InvalidArguments() + 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() { - var policy = new MockLsaUserRights( - new Dictionary>(StringComparer.InvariantCultureIgnoreCase) - { - { "joey", new List { PrincipalSid1 } } - }); - - return new() - { - // 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 } - }; + // 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 a 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. /// - [Fact] - public void GrantAndRevokeOthersShouldWork() + [TestMethod] + public void ModifyPrincipal_WithGrantAndDryRun_ShouldWork() { - 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"); - - Assert.Equal([PrincipalSid1, PrincipalSid2], policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - - var manager = ServiceProvider.GetRequiredService(); - manager.ModifyPrincipal(policy, PrincipalName1, [Privilege2], [], false, true, false); - - Assert.Equal([PrincipalSid1, PrincipalSid2], policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + // 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, 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 a single grant with a single revoke is successful and does not modify other assignments. + /// Verifies granting and revoking a privilege from a principal works as expected. /// - [Fact] - public void GrantAndRevokeShouldWork() + [TestMethod] + public void ModifyPrincipal_WithGrantAndRevoke_ShouldWork() { - 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"); - - 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)); - - var manager = ServiceProvider.GetRequiredService(); - manager.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)); + // 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], [Privilege1], false, false, false); + + // Assert. + 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 is successful and does not modify other assignments. + /// Verifies granting and revoking a privilege from a principal with dry run enabled works as expected. /// - [Fact] - public void GrantShouldWork() + [TestMethod] + public void ModifyPrincipal_WithGrantAndRevokeAndDryRun_ShouldWork() { - 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"); - - 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)); - - var manager = ServiceProvider.GetRequiredService(); - manager.ModifyPrincipal(policy, PrincipalName1, [Privilege2], [], false, false, 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[] { Privilege1, Privilege2 }.Order(StringComparer.OrdinalIgnoreCase), policy.LsaEnumerateAccountRights(PrincipalSid2).Order(StringComparer.OrdinalIgnoreCase)); + // 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], [Privilege1], 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 == PrincipalSid1)), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies granting a privilege and revoking the other privileges from a principal works as expected. + /// + [TestMethod] + public void ModifyPrincipal_WithGrantAndRevokeOthers_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, false); + + // Assert. + 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. /// 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 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(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 revoking all privileges from a principal works as expected. + /// + [TestMethod] + public void ModifyPrincipal_WithRevokeAll_ShouldWork() { - var manager = ServiceProvider.GetRequiredService(); + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); - Assert.ThrowsAny(() => manager.ModifyPrincipal(policy, principal, grants, revocations, revokeAll, revokeOthers, dryRun)); + using var fixture = new UserRightsManagerFixture(); + + // Act. + fixture.UserRightsManager.ModifyPrincipal(lsaUserRights.Object, PrincipalName1, [], [], true, false, false); + + // Assert. + 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 revoking all privileges for a principal is successful and does not modify other assignments. + /// Verifies revoking all privileges from a principal works as expected. /// - [Fact] - public void RevokeAllShouldWork() + [TestMethod] + public void ModifyPrincipal_WithRevokeAllAndDryRun_ShouldWork() { - 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"); - - 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)); - - var manager = ServiceProvider.GetRequiredService(); - manager.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)); + // 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, [], [], true, 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 == PrincipalSid1)), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); } /// - /// Verifies a single revocation is successful and does not modify other assignments. + /// Verifies revoking a privilege from a principal with dry run enabled works as expected. /// - [Fact] - public void RevokeShouldWork() + [TestMethod] + public void ModifyPrincipal_WithRevokeAndDryRun_ShouldWork() { - 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"); - - 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)); - - var manager = ServiceProvider.GetRequiredService(); - manager.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)); + // 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 3ec99b4..c6cd508 100644 --- a/src/Tests.Application/UserRightsManagerPrivilegeTests.cs +++ b/src/Tests.Application/UserRightsManagerPrivilegeTests.cs @@ -1,381 +1,511 @@ namespace Tests.Application; -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Principal; using System.Text.RegularExpressions; -using Microsoft.Extensions.DependencyInjection; - -using UserRights.Application; - -using Xunit; +using Moq; using static Tests.TestData; /// -/// Represents integration tests for modify privilege functionality. +/// Represents tests for modifying the principals for a specified user right. /// -public sealed class UserRightsManagerPrivilegeTests : UserRightsManagerTestBase +[TestClass] +public class UserRightsManagerPrivilegeTests { /// - /// Generates invalid method arguments for the method. + /// Gets invalid method arguments for the modify privilege unit test. /// /// A sequence of method arguments. - public static TheoryData InvalidArguments() + public static IEnumerable<(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 } - }; + const string pattern = ".*"; + + return + [ + // Verify null or empty privilege. + new(null!, [PrincipalName1], [], false, false, null, false), + new(string.Empty, [PrincipalName1], [], false, false, null, false), + + // Verify null grant collection. + new(Privilege1, null!, [PrincipalName1], false, false, null, false), + + // Verify null revocation collection. + new(Privilege1, [PrincipalName1], null!, false, false, null, false), + + // Verify RevokeAll requirements. + 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(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(Privilege1, [], [PrincipalName1], false, false, pattern, false), + new(Privilege2, [], [], true, false, pattern, false), + new(Privilege2, [], [], false, true, pattern, false), + + // Verify remaining requirements. + new(Privilege1, [], [], false, false, null, false), + + // Verify grant and revocation set restrictions. + 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. /// - [Fact] - public void GrantAndRevokeOthersShouldWork() + [TestMethod] + public void ModifyPrivilege_WithInvalidArguments_ThrowsException() { - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; + // Arrange. + 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 } - }; - - 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)); - - var manager = ServiceProvider.GetRequiredService(); - manager.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)); + lsaUserRights.VerifyNoOtherCalls(); } /// - /// Verifies a single grant with a single revoke is successful and does not modify other assignments. + /// Verifies modifying a privilege with invalid arguments throws an exception. /// - [Fact] - public void GrantAndRevokeShouldWork() + /// 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) { - var principals1 = new List - { - PrincipalSid1 - }; + // 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)); - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2 - }; + // Act & Assert. + Assert.Throws(() => fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, privilege, grants, revocations, revokeAll, revokeOthers, regex, dryRun)); - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; + lsaUserRights.VerifyNoOtherCalls(); + } - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); + /// + /// 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(lsaUserRights.Object, Privilege2, [PrincipalName1], [], false, false, null, false); + + // Assert. + 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(); + } - 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)); + /// + /// Verifies granting a privilege with dry run enabled works as expected. + /// + [TestMethod] + public void ModifyPrivilege_WithGrantAndDryRun_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege2) + .Build(); + + using var fixture = new UserRightsManagerFixture(); - var manager = ServiceProvider.GetRequiredService(); - manager.ModifyPrivilege(policy, Privilege1, [PrincipalName2], [PrincipalName1], false, false, null!, false); + // Act. + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege2, [PrincipalName1], [], false, false, null, true); - 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 }, 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(); } /// - /// 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 works as expected. /// - [Fact] - public void GrantAndRevokePatternShouldWork() + [TestMethod] + public void ModifyPrivilege_WithGrantAndRevoke_ShouldWork() { - var principals1 = new List - { - PrincipalSidCurrent, - PrincipalSid2, - PrincipalSid3 - }; - - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2, - PrincipalSid3 - }; + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege2) + .Build(); + + using var fixture = new UserRightsManagerFixture(); + + // Act. + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege1, [PrincipalName2], [PrincipalName1], false, false, null, false); + + // Assert. + 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(); + } - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; + /// + /// Verifies granting a privilege to a principal and revoking the privilege from another principal with dry run enabled works as expected. + /// + [TestMethod] + public void ModifyPrivilege_WithGrantAndRevokeAndDryRun_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege2) + .Build(); - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); + using var fixture = new UserRightsManagerFixture(); - 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)); + // Act. + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege1, [PrincipalName2], [PrincipalName1], false, false, null, true); - var manager = ServiceProvider.GetRequiredService(); - var pattern = new Regex("^S-1-5-21", RegexOptions.None, TimeSpan.FromSeconds(1)); - manager.ModifyPrivilege(policy, Privilege1, [PrincipalName1], [], false, false, pattern, false); + // Assert. + CollectionAssert.AreEquivalent(new[] { PrincipalSid2, PrincipalSid1 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEqual(new[] { Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); - 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)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(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. + /// Verifies granting a privilege to a principal and revoking the privilege from all other principals works as expected. /// - [Fact] - public void GrantShouldWork() + [TestMethod] + public void ModifyPrivilege_WithGrantAndRevokeOthers_ShouldWork() { - var principals1 = new List - { - PrincipalSid1 - }; - - var principals2 = new List - { - PrincipalSid2 - }; + // 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(lsaUserRights.Object, Privilege2, [PrincipalName1], [], false, true, null, false); + + // Assert. + 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(); + } - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; + /// + /// 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 ModifyPrivilege_WithGrantAndRevokeOthersAndDryRun_ShouldWork() + { + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, Privilege2) + .Build(); - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); + 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(lsaUserRights.Object, Privilege2, [PrincipalName1], [], false, true, null, true); - var manager = ServiceProvider.GetRequiredService(); - manager.ModifyPrivilege(policy, Privilege2, [PrincipalName1], [], false, false, null!, false); + // 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]); - 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)); + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege2, 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 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. - [Theory] - [MemberData(nameof(InvalidArguments))] - public void InvalidArgumentsThrowsException(IUserRightsSerializable policy, string privilege, string[] grants, string[] revocations, bool revokeAll, bool revokeOthers, string revokePattern, bool dryRun) + [TestMethod] + public void ModifyPrivilege_WithGrantAndRevokePattern_ShouldWork() { - var manager = ServiceProvider.GetRequiredService(); - var regex = string.IsNullOrWhiteSpace(revokePattern) ? null : new Regex(revokePattern, RegexOptions.None, TimeSpan.FromSeconds(1)); + // 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(lsaUserRights.Object, Privilege1, [PrincipalName1], [], false, false, pattern, false); - Assert.ThrowsAny(() => manager.ModifyPrivilege(policy, privilege, grants, revocations, revokeAll, revokeOthers, regex, dryRun)); + // Assert. + 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 revoking all principals for a privilege is successful and does not modify other assignments. + /// 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. /// - [Fact] - public void RevokeAllShouldWork() + [TestMethod] + public void ModifyPrivilege_WithGrantAndRevokePatternAndDryRun_ShouldWork() { - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; + // 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)); - var principals2 = new List - { - PrincipalSid1, - PrincipalSid2 - }; + // Act. + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege1, [PrincipalName1], [], false, false, pattern, true); - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; - - 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()); - - var manager = ServiceProvider.GetRequiredService(); - manager.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[] { 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 a single revocation is successful and does not modify other assignments. + /// Verifies revoking a privilege from a principal works as expected. /// - [Fact] - public void RevokeShouldWork() + [TestMethod] + public void ModifyPrivilege_WithRevoke_ShouldWork() { - var principals1 = new List - { - PrincipalSid1, - PrincipalSid2 - }; + // 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(lsaUserRights.Object, Privilege1, [], [PrincipalName2], false, false, null, false); + + // 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, 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(); + } - var principals2 = new List - { - PrincipalSid2 - }; + /// + /// 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(); - var database = new Dictionary>(StringComparer.Ordinal) - { - { Privilege1, principals1 }, - { Privilege2, principals2 } - }; + using var fixture = new UserRightsManagerFixture(); - var policy = new MockLsaUserRights(database); - policy.Connect("SystemName"); + // Act. + fixture.UserRightsManager.ModifyPrivilege(lsaUserRights.Object, Privilege1, [], [PrincipalName2], false, false, null, true); - 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. + CollectionAssert.AreEquivalent(new[] { PrincipalSid1, PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { Privilege1 }, lsaUserRightsMockBuilder.Database[PrincipalSid1]); + CollectionAssert.AreEquivalent(new[] { Privilege1, Privilege2 }, lsaUserRightsMockBuilder.Database[PrincipalSid2]); - var manager = ServiceProvider.GetRequiredService(); - manager.ModifyPrivilege(policy, Privilege1, [], [PrincipalName2], false, false, null!, false); + lsaUserRights.Verify(x => x.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); + /// + /// Verifies revoking a privilege from all principals works as expected. + /// + [TestMethod] + public void ModifyPrivilege_WithRevokeAll_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(lsaUserRights.Object, Privilege1, [], [], true, false, null, false); + + // Assert. + CollectionAssert.AreEqual(new[] { PrincipalSid2 }, lsaUserRightsMockBuilder.Database.Keys.ToArray()); + CollectionAssert.AreEqual(new[] { 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.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. + /// Verifies revoking a privilege from all principals with dry run enabled works as expected. /// - [Fact] - public void RevokePatternForAllButBuiltinAndVirtualShouldWork() + [TestMethod] + public void ModifyPrivilege_WithRevokeAllAndDryRun_ShouldWork() { - var principals1 = new List - { - PrincipalSidCurrent, - PrincipalSid2, - PrincipalSid3 - }; + // Arrange. + var lsaUserRightsMockBuilder = LsaUserRightsMockBuilder.CreateBuilder(); + var lsaUserRights = lsaUserRightsMockBuilder + .WithGrant(PrincipalSid1, Privilege1) + .WithGrant(PrincipalSid2, Privilege1, 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, [], [], true, false, null, 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.LsaEnumerateAccountsWithUserRight(It.Is(s => string.Equals(s, Privilege1, StringComparison.Ordinal))), Times.Exactly(1)); + lsaUserRights.VerifyNoOtherCalls(); + } + + /// + /// Verifies revoking a privilege from all principals that match a pattern works as expected. + /// + [TestMethod] + public void ModifyPrivilege_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 UserRightsManagerFixture(); + var pattern = new Regex("^S-1-5-21", RegexOptions.None, TimeSpan.FromSeconds(1)); + + // 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]); - 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)); + 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(); + } - var manager = ServiceProvider.GetRequiredService(); + /// + /// 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)); - 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(lsaUserRights.Object, Privilege1, [], [], 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(); } } \ 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 2473187..0000000 --- a/src/Tests.Application/UserRightsManagerTestBase.cs +++ /dev/null @@ -1,89 +0,0 @@ -namespace Tests.Application; - -using System; - -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/.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/CliTestBase.cs b/src/Tests.Cli/CliTestBase.cs deleted file mode 100644 index 33a8b12..0000000 --- a/src/Tests.Cli/CliTestBase.cs +++ /dev/null @@ -1,82 +0,0 @@ -namespace Tests.Cli; - -using System; - -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 deleted file mode 100644 index d909e6d..0000000 --- a/src/Tests.Cli/ListCommandTests.cs +++ /dev/null @@ -1,175 +0,0 @@ -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; -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 -{ - /// - /// Verifies listing user rights to a JSON file. - /// - /// A task representing the asynchronous operation. - [Fact] - public async Task PathAndJsonShouldWork() - { - 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); - - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); - - var configuration = builder.Build(); - - var file = Path.GetTempFileName(); - var args = new[] - { - "list", - "--json", - "--path", - file - }; - - int rc; - UserRightEntry[] actual; - try - { - rc = await configuration.Parse(args).Validate().InvokeAsync(); - - 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() ?? []; - } - finally - { - File.Delete(file); - } - - Assert.Equal(0, rc); - Assert.Equal(expected, actual, new UserRightEntryEqualityComparer()); - } - - /// - /// Verifies listing user rights to a CSV file. - /// - /// A task representing the asynchronous operation. - [Fact] - public async Task PathShouldWork() - { - 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(); - - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); - - var configuration = builder.Build(); - - var file = Path.GetTempFileName(); - var args = new[] - { - "list", - "--path", - file - }; - - int rc; - UserRightEntry[] actual; - try - { - rc = await configuration.Parse(args).Validate().InvokeAsync(); - - 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() - .OrderBy(p => p.Privilege, StringComparer.OrdinalIgnoreCase) - .ThenBy(p => p.SecurityId, StringComparer.OrdinalIgnoreCase) - .ToArrayAsync(); - } - finally - { - File.Delete(file); - } - - Assert.Equal(0, rc); - Assert.Equal(expected, actual, new UserRightEntryEqualityComparer()); - } -} \ 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 dc9a523..0000000 --- a/src/Tests.Cli/ListSyntaxTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -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 -{ - 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] - public void CsvToStdoutShouldWork() - { - var args = new[] { "list" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(0, rc); - } - - /// - /// Verifies list mode with CSV formatted output sent to a file is parsed successfully. - /// - [Fact] - public void CsvToPathShouldWork() - { - var args = new[] { "list", "--path", "file.csv" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(0, rc); - } - - /// - /// Ensures an empty or whitespace path is rejected. - /// - /// The test arguments. - [Theory] - [InlineData("list", "--path", "")] - [InlineData("list", "--path", " ")] - public void PathWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); - - /// - /// Ensures an empty or whitespace system name is rejected. - /// - /// The test arguments. - [Theory] - [InlineData("list", "--system-name", "")] - [InlineData("list", "--system-name", " ")] - public void SystemNameWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); - - /// - /// Verifies list mode with JSON formatted output sent to STDOUT is parsed successfully. - /// - [Fact] - public void JsonToStdoutShouldWork() - { - var args = new[] { "list", "--json" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(0, rc); - } - - /// - /// Verifies list mode with JSON formatted output sent to a file is parsed successfully. - /// - [Fact] - public void JsonToPathShouldWork() - { - var args = new[] { "list", "--json", "--path", "file.csv" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(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 deleted file mode 100644 index e97e376..0000000 --- a/src/Tests.Cli/PrincipalCommandTests.cs +++ /dev/null @@ -1,301 +0,0 @@ -namespace Tests.Cli; - -using System; -using System.Collections.Generic; -using System.Linq; -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 -{ - /// - /// Verifies a granting a privilege to a principal and revoking their other privileges is successful and does not modify other assignments. - /// - [Fact] - public void GrantAndRevokeOthersShouldWork() - { - 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"); - - Assert.Equal([PrincipalSid1, PrincipalSid2], policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - - policy.ResetConnection(); - - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); - - var configuration = builder.Build(); - - var args = new[] - { - "principal", - PrincipalName1, - "--grant", - Privilege2, - "--revoke-others" - }; - - var rc = configuration.Parse(args).Validate().Invoke(); - - 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)); - } - - /// - /// Verifies a single grant with a single revoke is successful and does not modify other assignments. - /// - [Fact] - public void GrantAndRevokeShouldWork() - { - 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"); - - 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)); - - policy.ResetConnection(); - - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); - - var configuration = builder.Build(); - - var args = new[] - { - "principal", - PrincipalName1, - "--grant", - Privilege2, - "--revoke", - Privilege1 - }; - - var rc = configuration.Parse(args).Validate().Invoke(); - - 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)); - } - - /// - /// Verifies a single grant is successful and does not modify other assignments. - /// - [Fact] - public void GrantShouldWork() - { - 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"); - - 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)); - - policy.ResetConnection(); - - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); - - var configuration = builder.Build(); - - var args = new[] - { - "principal", - PrincipalName1, - "--grant", - Privilege2 - }; - - var rc = configuration.Parse(args).Validate().Invoke(); - - 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)); - } - - /// - /// Verifies a revoking all privileges for a principal is successful and does not modify other assignments. - /// - [Fact] - public void RevokeAllShouldWork() - { - 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"); - - 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)); - - policy.ResetConnection(); - - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); - - var configuration = builder.Build(); - - var args = new[] - { - "principal", - PrincipalName1, - "--revoke-all" - }; - - var rc = configuration.Parse(args).Validate().Invoke(); - - 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)); - } - - /// - /// Verifies a single revocation is successful and does not modify other assignments. - /// - [Fact] - public void RevokeShouldWork() - { - 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"); - - 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)); - - policy.ResetConnection(); - - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); - - var configuration = builder.Build(); - - var args = new[] - { - "principal", - PrincipalName2, - "--revoke", - Privilege2 - }; - - var rc = configuration.Parse(args).Validate().Invoke(); - - 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)); - } -} \ 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 b8223f0..0000000 --- a/src/Tests.Cli/PrincipalSyntaxTests.cs +++ /dev/null @@ -1,244 +0,0 @@ -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 -{ - 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] - public void GrantAndRevokeShouldWork() - { - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--revoke", "SeBatchLogonRight" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(0, rc); - } - - /// - /// Ensures granting multiple privileges is accepted. - /// - [Fact] - public void GrantMultipleShouldWork() - { - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--grant", "SeBatchLogonRight" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(0, rc); - } - - /// - /// Ensures granting a privilege is accepted. - /// - [Fact] - public void GrantShouldWork() - { - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(0, rc); - } - - /// - /// Ensures an empty or whitespace grant is rejected. - /// - /// The test arguments. - [Theory] - [InlineData("principal", "DOMAIN\\UserOrGroup", "--grant", "")] - [InlineData("principal", "DOMAIN\\UserOrGroup", "--grant", " ")] - public void GrantWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); - - /// - /// Ensures granting a privilege and revoking all other privileges is rejected. - /// - [Fact] - public void GrantWithRevokeAllThrowsException() - { - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--grant", "SeServiceLogonRight", "--revoke-all" }; - var configuration = _builder.Build(); - - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); - } - - /// - /// Ensures specifying no options is rejected. - /// - [Fact] - public void NoOptionsThrowsException() - { - var args = new[] { "principal" }; - var configuration = _builder.Build(); - - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); - } - - /// - /// Ensures overlapping or duplicate privileges is 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")] - public void OverlappingGrantsAndRevokesThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); - - /// - /// Ensures an empty or whitespace principal is rejected. - /// - /// The test arguments. - [Theory] - [InlineData("principal", "", "--grant", "SeServiceLogonRight")] - [InlineData("principal", " ", "--grant", "SeServiceLogonRight")] - public void PrincipalWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); - - /// - /// Ensures revoking all privileges is accepted. - /// - [Fact] - public void RevokeAllShouldWork() - { - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-all" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(0, rc); - } - - /// - /// Ensures granting a privilege and granting all privileges is rejected. - /// - [Fact] - public void RevokeAllWithGrantsThrowsException() - { - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-all", "--grant", "SeServiceLogonRight" }; - var configuration = _builder.Build(); - - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); - } - - /// - /// Ensures revoking a privilege and revoking all privileges is rejected. - /// - [Fact] - public void RevokeAllWithRevocationsThrowsException() - { - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-all", "--revoke", "SeServiceLogonRight" }; - var configuration = _builder.Build(); - - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); - } - - /// - /// Ensures revoking all privileges and revoking other privileges is rejected. - /// - [Fact] - public void RevokeAllWithRevokeOthersThrowsException() - { - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-all", "--revoke-others" }; - var configuration = _builder.Build(); - - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); - } - - /// - /// Ensures revoking multiple privileges is accepted. - /// - [Fact] - public void RevokeMultipleShouldWork() - { - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke", "SeServiceLogonRight", "--revoke", "SeBatchLogonRight" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(0, rc); - } - - /// - /// Ensures revoke other privileges without granting a privilege is rejected. - /// - [Fact] - public void RevokeOthersWithOutGrantsThrowsException() - { - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-others" }; - - Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); - } - - /// - /// Ensures revoke other privileges with revoking a privilege is rejected. - /// - [Fact] - public void RevokeOthersWithRevocationsThrowsException() - { - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke-others", "--revoke", "SeServiceLogonRight" }; - var configuration = _builder.Build(); - - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); - } - - /// - /// Ensures revoking a privilege is accepted. - /// - [Fact] - public void RevokeShouldWork() - { - var args = new[] { "principal", "DOMAIN\\UserOrGroup", "--revoke", "SeServiceLogonRight" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(0, rc); - } - - /// - /// Ensures an empty or whitespace revocation is rejected. - /// - /// The test arguments. - [Theory] - [InlineData("principal", "DOMAIN\\UserOrGroup", "--revoke", "")] - [InlineData("principal", "DOMAIN\\UserOrGroup", "--revoke", " ")] - public void RevokeWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); - - /// - /// 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", " ")] - public void SystemNameWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); -} \ 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 ae144f7..0000000 --- a/src/Tests.Cli/PrivilegeCommandTests.cs +++ /dev/null @@ -1,435 +0,0 @@ -namespace Tests.Cli; - -using System; -using System.Collections.Generic; -using System.Linq; -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 -{ - /// - /// Verifies granting a principal to a privilege and revoking its other principals is successful and does not modify other assignments. - /// - [Fact] - public void GrantAndRevokeOthersShouldWork() - { - 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"); - - 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)); - - policy.ResetConnection(); - - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); - - var configuration = builder.Build(); - - var args = new[] - { - "privilege", - Privilege2, - "--grant", - PrincipalName1, - "--revoke-others" - }; - - var rc = configuration.Parse(args).Validate().Invoke(); - - 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)); - } - - /// - /// Verifies a single grant with a single revoke is successful and does not modify other assignments. - /// - [Fact] - public void GrantAndRevokePasses() - { - 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"); - - 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)); - - policy.ResetConnection(); - - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); - - var configuration = builder.Build(); - - var args = new[] - { - "privilege", - Privilege1, - "--grant", - PrincipalName2, - "--revoke", - PrincipalName1 - }; - - var rc = configuration.Parse(args).Validate().Invoke(); - - 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)); - } - - /// - /// Verifies granting a principal to a privilege and revoking all principals matching a pattern is successful and does not modify other assignments. - /// - [Fact] - public void GrantAndRevokePatternPasses() - { - 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"); - - 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)); - - policy.ResetConnection(); - - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); - - var configuration = builder.Build(); - - var args = new[] - { - "privilege", - Privilege1, - "--grant", - PrincipalName1, - "--revoke-pattern", - "^S-1-5-21" - }; - - var rc = configuration.Parse(args).Validate().Invoke(); - - 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)); - } - - /// - /// Verifies a single grant is successful and does not modify other assignments. - /// - [Fact] - public void GrantPasses() - { - 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"); - - Assert.Equal(new[] { PrincipalSid1, PrincipalSid2 }.Order(), policy.LsaEnumerateAccountsWithUserRight().Order()); - Assert.Equal(new[] { Privilege1 }, policy.LsaEnumerateAccountRights(PrincipalSid1)); - Assert.Equal(new[] { Privilege2 }, policy.LsaEnumerateAccountRights(PrincipalSid2)); - - policy.ResetConnection(); - - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); - - var configuration = builder.Build(); - - var args = new[] - { - "privilege", - Privilege2, - "--grant", - PrincipalName1 - }; - - var rc = configuration.Parse(args).Validate().Invoke(); - - 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)); - } - - /// - /// Verifies revoking all principals for a privilege is successful and does not modify other assignments. - /// - [Fact] - public void RevokeAllPasses() - { - 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"); - - 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()); - - policy.ResetConnection(); - - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); - - var configuration = builder.Build(); - - var args = new[] - { - "privilege", - Privilege1, - "--revoke-all" - }; - - var rc = configuration.Parse(args).Validate().Invoke(); - - 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()); - } - - /// - /// Verifies a single revocation is successful and does not modify other assignments. - /// - [Fact] - public void RevokePasses() - { - 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"); - - 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)); - - policy.ResetConnection(); - - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); - - var configuration = builder.Build(); - - var args = new[] - { - "privilege", - Privilege1, - "--revoke", - PrincipalName2 - }; - - var rc = configuration.Parse(args).Validate().Invoke(); - - 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)); - } - - /// - /// Verifies revoking all non builtin and virtual principals from a privilege is successful. - /// - [Fact] - public void RevokePatternForAllButBuiltinAndVirtualPasses() - { - 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"); - - 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)); - - policy.ResetConnection(); - - ServiceCollection.AddSingleton(policy); - ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(); - - var builder = ServiceProvider.GetRequiredService(); - - var configuration = builder.Build(); - - var args = new[] - { - "privilege", - Privilege1, - "--revoke-pattern", - "^S-1-5-21" - }; - - var rc = configuration.Parse(args).Validate().Invoke(); - - 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)); - } -} \ 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 9c4b5db..0000000 --- a/src/Tests.Cli/PrivilegeSyntaxTests.cs +++ /dev/null @@ -1,348 +0,0 @@ -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 -{ - 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] - public void GrantAndRevokeShouldWork() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\User", "--revoke", "DOMAIN\\Group" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(0, rc); - } - - /// - /// Ensures granting multiple contexts is accepted. - /// - [Fact] - public void GrantMultipleShouldWork() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\User", "--grant", "DOMAIN\\Group" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(0, rc); - } - - /// - /// Ensures granting a context is accepted. - /// - [Fact] - public void GrantShouldWork() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\User" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(0, rc); - } - - /// - /// Ensures an empty or whitespace grant is rejected. - /// - /// The test arguments. - [Theory] - [InlineData("privilege", "SeServiceLogonRight", "--grant", "")] - [InlineData("privilege", "SeServiceLogonRight", "--grant", " ")] - public void GrantWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); - - /// - /// Ensures granting a context and revoking all contexts is rejected. - /// - [Fact] - public void GrantWithRevokeAllThrowsException() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\User", "--revoke-all" }; - var configuration = _builder.Build(); - - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); - } - - /// - /// Ensures specifying no options is rejected. - /// - [Fact] - public void NoOptionsThrowsException() - { - var args = new[] { "privilege" }; - var configuration = _builder.Build(); - - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); - } - - /// - /// Ensures overlapping or duplicate contexts is 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")] - public void OverlappingGrantsAndRevokesThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); - - /// - /// Ensures an empty or whitespace principal is rejected. - /// - /// The test arguments. - [Theory] - [InlineData("privilege", "", "--grant", "DOMAIN\\UserOrGroup")] - [InlineData("privilege", " ", "--grant", "DOMAIN\\UserOrGroup")] - public void PrivilegeWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); - - /// - /// Ensures revoking all contexts is accepted. - /// - [Fact] - public void RevokeAllShouldWork() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-all" }; - var configuration = _builder.Build(); - - int? rc = null; - var exception = Record.Exception(() => rc = configuration.Parse(args).Validate().Invoke()); - - Assert.Null(exception); - Assert.Equal(0, rc); - } - - /// - /// Ensures granting a context and revoking all contexts is rejected. - /// - [Fact] - public void RevokeAllWithGrantsThrowsException() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-all", "--grant", "DOMAIN\\UserOrGroup" }; - var configuration = _builder.Build(); - - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); - } - - /// - /// Ensures revoking a context and revoking all contexts is rejected. - /// - [Fact] - public void RevokeAllWithRevocationsThrowsException() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-all", "--revoke", "DOMAIN\\UserOrGroup" }; - var configuration = _builder.Build(); - - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); - } - - /// - /// Ensures revoking all contexts and revoking other contexts is rejected. - /// - [Fact] - public void RevokeAllWithRevokeOthersThrowsException() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-all", "--revoke-others" }; - var configuration = _builder.Build(); - - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); - } - - /// - /// Ensures revoking multiple contexts is accepted. - /// - [Fact] - public void RevokeMultipleShouldWork() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke", "DOMAIN\\User", "--revoke", "DOMAIN\\Group" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(0, rc); - } - - /// - /// Ensures revoke other contexts without granting a context is rejected. - /// - [Fact] - public void RevokeOthersWithOutGrantsThrowsException() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-others" }; - var configuration = _builder.Build(); - - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); - } - - /// - /// Ensures revoke other contexts with revoking a context is rejected. - /// - [Fact] - public void RevokeOthersWithRevocationsThrowsException() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-others", "--revoke", "DOMAIN\\UserOrGroup" }; - var configuration = _builder.Build(); - - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); - } - - /// - /// Ensures revoking a valid pattern is accepted. - /// - [Fact] - public void RevokePatternShouldWork() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(0, rc); - } - - /// - /// Ensures granting a context and revoking a valid pattern is accepted. - /// - [Fact] - public void RevokePatternWithGrantShouldWork() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--grant", "DOMAIN\\UserOrGroup", "--revoke-pattern", "^S-1-5-21-" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(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]+")] - public void RevokePatternWithValidRegexShouldWork(params string[] args) - { - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(0, rc); - } - - /// - /// Ensures revoking a pattern and revoking all contexts is rejected. - /// - [Fact] - public void RevokePatternWithRevokeAllThrowsException() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-", "--revoke-all" }; - var configuration = _builder.Build(); - - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); - } - - /// - /// Ensures revoking a pattern and revoking other contexts is rejected. - /// - [Fact] - public void RevokePatternWithRevokeOthersThrowsException() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-", "--revoke-others" }; - var configuration = _builder.Build(); - - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); - } - - /// - /// Ensures revoking a pattern and revoking a context is rejected. - /// - [Fact] - public void RevokePatternWithRevokeThrowsException() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke-pattern", "^S-1-5-21-", "--revoke", "DOMAIN\\UserOrGroup" }; - var configuration = _builder.Build(); - - Assert.Throws(() => configuration.Parse(args).Validate().Invoke()); - } - - /// - /// 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-")] - public void RevokePatternWithInvalidRegexThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); - - /// - /// Ensures an empty or whitespace revocation pattern is rejected. - /// - /// The test arguments. - [Theory] - [InlineData("privilege", "SeServiceLogonRight", "--revoke-pattern", "")] - [InlineData("privilege", "SeServiceLogonRight", "--revoke-pattern", " ")] - public void RevokePatternWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); - - /// - /// Ensures revoking a context is accepted. - /// - [Fact] - public void RevokeShouldWork() - { - var args = new[] { "privilege", "SeServiceLogonRight", "--revoke", "DOMAIN\\UserOrGroup" }; - var configuration = _builder.Build(); - - var rc = configuration.Parse(args).Validate().Invoke(); - - Assert.Equal(0, rc); - } - - /// - /// Ensures an empty or whitespace revocation is rejected. - /// - /// The test arguments. - [Theory] - [InlineData("privilege", "SeServiceLogonRight", "--revoke", "")] - [InlineData("privilege", "SeServiceLogonRight", "--revoke", " ")] - public void RevokeWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); - - /// - /// 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", " ")] - public void SystemNameWithInvalidStringThrowsException(params string[] args) - => Assert.Throws(() => _builder.Build().Parse(args).Validate().Invoke()); -} \ No newline at end of file diff --git a/src/Tests.Cli/Tests.Cli.csproj b/src/Tests.Cli/Tests.Cli.csproj index a2858a5..b840c3c 100644 --- a/src/Tests.Cli/Tests.Cli.csproj +++ b/src/Tests.Cli/Tests.Cli.csproj @@ -1,38 +1,27 @@ - + - net9.0-windows + Exe + net10.0-windows + enable enable latest latest-All - + All false - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive + + + all - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - \ 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/CliMockBuilder.cs b/src/Tests/CliMockBuilder.cs new file mode 100644 index 0000000..6b49afc --- /dev/null +++ b/src/Tests/CliMockBuilder.cs @@ -0,0 +1,135 @@ +namespace Tests; + +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 CliMockBuilder : IDisposable +{ + private readonly ServiceProvider _serviceProvider; + + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a CLI with an empty, mock implementation, and a mock implementation. + /// + 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(lsaUserRights.Object); + serviceCollection.AddSingleton(userRightsManager.Object); + serviceCollection.AddSingleton(); + + _serviceProvider = serviceCollection.BuildServiceProvider(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The existing LSA user rights implementation. + /// + /// Creates a CLI with a user-supplied implementation, and a complete instance of a implementation. + /// + public CliMockBuilder(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(); + } + + /// + /// 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/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.Application/LsaUserRightsTestBase.cs b/src/Tests/LsaUserRightsSnapshotFixture.cs similarity index 67% rename from src/Tests.Application/LsaUserRightsTestBase.cs rename to src/Tests/LsaUserRightsSnapshotFixture.cs index 2b89597..5dcbf1b 100644 --- a/src/Tests.Application/LsaUserRightsTestBase.cs +++ b/src/Tests/LsaUserRightsSnapshotFixture.cs @@ -1,24 +1,23 @@ -namespace Tests.Application; +namespace Tests; -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; 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 class LsaUserRightsSnapshotFixture : IDisposable { private const string ExportSecurityTemplateName = "export.ini"; private const string ExportSecurityLogName = "export.log"; @@ -26,20 +25,30 @@ public abstract class LsaUserRightsTestBase : 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; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - protected LsaUserRightsTestBase() + 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); @@ -49,6 +58,7 @@ protected LsaUserRightsTestBase() } catch { + // Prevent disposal from restoring the backup or deleting the temporary directory if initialization fails. _directory = null; throw; @@ -58,7 +68,7 @@ protected LsaUserRightsTestBase() /// /// Gets the initial state of user rights assignments before they are modified through test execution. /// - protected IReadOnlyDictionary> InitialState + public IReadOnlyDictionary> InitialState { get { @@ -68,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() { @@ -90,32 +121,13 @@ protected virtual void Dispose(bool disposing) { if (_directory is not null) { - RestoreSecurityDatabaseBackup(_directory.FullName); + RunSecurityEditor(_restoreSecurityArguments, _directory.FullName); _directory.Delete(true); } - - _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; + _disposed = true; } /// @@ -153,7 +165,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); } @@ -163,69 +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.Format( - CultureInfo.InvariantCulture, - "/export /cfg {0} /areas user_rights /log {1}", - ExportSecurityTemplateName, - 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(5000); - - if (process.ExitCode != 0) - { - var message = string.Format( - CultureInfo.InvariantCulture, - "Failed to export the security database, exit code: {0}\r\n{1}", - process.ExitCode, - 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. @@ -274,19 +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.Format( - CultureInfo.InvariantCulture, - "/configure /db {0} /cfg {1} /areas user_rights /log {2}", - RestoreSecurityDatabaseName, - RestoreSecurityTemplateName, - RestoreSecurityLogName); + ArgumentException.ThrowIfNullOrWhiteSpace(workingDirectory); var stringBuilder = new StringBuilder(); @@ -305,15 +254,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 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 f6d6b9e..0000000 --- a/src/Tests/MockLsaUserRights.cs +++ /dev/null @@ -1,179 +0,0 @@ -namespace Tests; - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Principal; - -using UserRights.Application; - -using Xunit.Abstractions; - -/// -/// Represents a mock implementation. -/// -public sealed class MockLsaUserRights : ILsaUserRights, IUserRightsSerializable -{ - 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); - - if (userRights.Length == 0) - { - throw new ArgumentException("Value cannot be an empty collection.", 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); - - if (userRights.Length == 0) - { - throw new ArgumentException("Value cannot be an empty collection.", 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 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 deleted file mode 100644 index 38ed591..0000000 --- a/src/Tests/MockUserRightsManager.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Tests; - -using System.Collections.Generic; -using System.Text.RegularExpressions; - -using UserRights.Application; - -/// -/// Represents a mock user rights manager with noop interface implementations. -/// -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/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 2acca78..34b9411 100644 --- a/src/Tests/TestData.cs +++ b/src/Tests/TestData.cs @@ -1,6 +1,5 @@ namespace Tests; -using System; using System.Security.Principal; /// @@ -39,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"); @@ -49,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 e16c39f..9dd04bd 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -1,18 +1,32 @@ - + - net9.0-windows + net10.0-windows + enable enable latest latest-All + All + false + false - + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + \ No newline at end of file diff --git a/src/Tests/UserRightEntryEqualityComparer.cs b/src/Tests/UserRightEntryEqualityComparer.cs deleted file mode 100644 index 91f19e4..0000000 --- a/src/Tests/UserRightEntryEqualityComparer.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace Tests; - -using System; -using System.Collections.Generic; - -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/.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.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..b5de3ac 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; @@ -56,19 +55,13 @@ 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); 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++) { @@ -87,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."); } } } @@ -114,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]; @@ -142,10 +132,7 @@ public unsafe string[] LsaEnumerateAccountRights(SecurityIdentifier accountSid) } finally { - if (userRights is not null) - { - PInvoke.LsaFreeMemory(userRights); - } + PInvoke.LsaFreeMemory(userRights); } } } @@ -193,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]; @@ -211,10 +198,7 @@ SecurityIdentifier[] Method(LSA_UNICODE_STRING right) } finally { - if (buffer is not null) - { - PInvoke.LsaFreeMemory(buffer); - } + PInvoke.LsaFreeMemory(buffer); } } } @@ -231,19 +215,13 @@ 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); 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++) { @@ -262,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."); } } } @@ -286,8 +263,9 @@ protected virtual void Dispose(bool disposing) if (disposing) { _handle?.Dispose(); - _disposed = true; } + + _disposed = true; } /// @@ -327,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.Application/UserRightEntry.cs b/src/UserRights.Application/UserRightEntry.cs index 6ea5c0b..792a93b 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. /// @@ -40,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/UserRights.Application/UserRights.Application.csproj b/src/UserRights.Application/UserRights.Application.csproj index d6fb065..ea1454b 100644 --- a/src/UserRights.Application/UserRights.Application.csproj +++ b/src/UserRights.Application/UserRights.Application.csproj @@ -1,15 +1,16 @@ - net9.0-windows + net10.0-windows + enable enable latest latest-All - - + + 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 2a5897f..198b30c 100644 --- a/src/UserRights.Cli/CliBuilder.cs +++ b/src/UserRights.Cli/CliBuilder.cs @@ -1,15 +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.Tasks; using Microsoft.Extensions.Logging; @@ -39,10 +34,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") { @@ -53,29 +48,16 @@ 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; } } - var configuration = new CommandLineConfiguration(rootCommand) - { - // Disable the default exception handler to allow logging errors to the event log. - EnableDefaultExceptionHandler = false - }; - - return configuration; + return rootCommand; } /// @@ -97,7 +79,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."); } @@ -111,7 +95,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."); } @@ -125,7 +111,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); @@ -135,8 +121,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; @@ -144,14 +133,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 { @@ -160,17 +142,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); } } }); @@ -222,7 +197,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."); } @@ -231,9 +208,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."); } @@ -242,8 +221,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."); } @@ -271,13 +251,15 @@ 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."); } }); - // 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) ?? []; @@ -299,9 +281,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."); } @@ -310,9 +295,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."); } @@ -321,7 +309,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."); } @@ -350,7 +340,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); @@ -416,7 +406,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."); } @@ -425,10 +417,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."); } @@ -437,7 +431,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."); } @@ -465,13 +461,15 @@ 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."); } }); - // 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) ?? []; @@ -493,10 +491,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."); } @@ -505,10 +506,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."); } @@ -518,6 +522,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."); @@ -530,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); } } }); @@ -538,9 +544,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."); } @@ -549,7 +557,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."); } @@ -579,7 +589,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 diff --git a/src/UserRights.Cli/CliExtensions.cs b/src/UserRights.Cli/CliExtensions.cs index cc4d384..75905e8 100644 --- a/src/UserRights.Cli/CliExtensions.cs +++ b/src/UserRights.Cli/CliExtensions.cs @@ -1,6 +1,5 @@ namespace UserRights.Cli; -using System; using System.CommandLine; using System.Globalization; using System.Text; @@ -11,12 +10,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..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; @@ -25,6 +24,9 @@ public HelpExamplesAction(HelpAction helpAction) _helpAction = helpAction; } + /// + public override bool ClearsParseErrors => true; + /// public override int Invoke(ParseResult parseResult) { @@ -97,6 +99,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.Cli/ProgramInfo.cs b/src/UserRights.Cli/ProgramInfo.cs index dfc629a..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; /// @@ -24,15 +23,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/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 f092145..29dbafb 100644 --- a/src/UserRights.Cli/UserRights.Cli.csproj +++ b/src/UserRights.Cli/UserRights.Cli.csproj @@ -1,15 +1,16 @@ - net9.0-windows + net10.0-windows + enable enable latest latest-All - - + + 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 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 c13be21..001702e 100644 --- a/src/UserRights.Extensions/UserRights.Extensions.csproj +++ b/src/UserRights.Extensions/UserRights.Extensions.csproj @@ -1,14 +1,15 @@ - net9.0-windows + net10.0-windows + enable enable latest latest-All - + \ No newline at end of file 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 @@ + + + + + + + + + + + + + + + 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 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 2c4effd..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; @@ -25,10 +23,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 }; diff --git a/src/UserRights/Program.cs b/src/UserRights/Program.cs index fb75897..2e315b9 100644 --- a/src/UserRights/Program.cs +++ b/src/UserRights/Program.cs @@ -1,17 +1,17 @@ namespace UserRights; -using System; using System.Globalization; using System.Text; -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 +144,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 60f8e7c..b14aba5 100644 --- a/src/UserRights/UserRights.csproj +++ b/src/UserRights/UserRights.csproj @@ -2,23 +2,23 @@ Exe - net9.0-windows + net10.0-windows + enable enable latest latest-All - - - - - - - - - - + + + + + + + + + diff --git a/src/global.json b/src/global.json index 918e756..ed89368 100644 --- a/src/global.json +++ b/src/global.json @@ -1,6 +1,12 @@ { "sdk": { - "version": "9.0.305", + "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