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