From 648defd8431285d7c9297ce3827366f5f6ef6354 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Thu, 12 Sep 2024 02:55:16 -0700 Subject: [PATCH] Add admin check and fallback method to `Get-UserProfilesFromRegistry` --- CHANGELOG.md | 38 +- build.yaml | 3 +- source/Private/Get-SIDProfileInfo.ps1 | 2 + source/Private/Get-SIDProfileInfoFallback.ps1 | 59 +++ .../Public/Get-UserProfilesFromRegistry.ps1 | 15 +- source/prefix.ps1 | 36 ++ .../Get-SIDProfileInfoFallback.tests.ps1 | 144 ++++++ .../Get-UserProfilesFromRegistry.tests.ps1 | 464 ++++++++++++++++-- 8 files changed, 708 insertions(+), 53 deletions(-) create mode 100644 source/Private/Get-SIDProfileInfoFallback.ps1 create mode 100644 source/prefix.ps1 create mode 100644 tests/Unit/Private/Get-SIDProfileInfoFallback.tests.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index dee1c74..7baef7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.2.0] - 2024-09-12 - ### Added - New helper function `Validate-SIDFormat` to verify SID value upon retrieval in `Get-ProfilePathFromSID` +- **Admin Detection and Environment Variable**: Added logic to detect whether the + current user is an administrator and set an environment variable + `WinProfileOps_IsAdmin` accordingly. + + - If the user is an administrator, `$ENV:WinProfileOps_IsAdmin` is set to + `$true`. If not, it's set to `$false`. + + - The environment variable is automatically removed when the module is + unloaded or when PowerShell exits. + + - Registered an `OnRemove` script block and a `PowerShell.Exiting` event to + ensure cleanup of the environment variable on module removal or session exit. + +- **Get-SIDProfileInfoFallback**: Introduced a new fallback function + `Get-SIDProfileInfoFallback` that retrieves non-special user profile + information using the CIM/WMI method. + +### Changed + +- **Get-UserProfilesFromRegistry**: Updated the function to handle scenarios + where the current user does not have administrative privileges. + + - The function now checks if the user is an administrator by evaluating the + `WinProfileOps_IsAdmin` environment variable. + + - If the user has administrator privileges, the function retrieves user + profiles from the registry using `Get-SIDProfileInfo`. + + - If the user lacks administrative privileges, the function falls back to the + `Get-SIDProfileInfoFallback` method, which retrieves user profiles using + CIM/WMI without requiring registry access. + + - A warning is logged when the fallback method is used, indicating that + special system accounts are excluded. + +## [0.2.0] - 2024-09-12 ### Added diff --git a/build.yaml b/build.yaml index e8f5644..ac7b116 100644 --- a/build.yaml +++ b/build.yaml @@ -19,8 +19,7 @@ Encoding: UTF8 #SemVer: '99.0.0-preview1' # Suffix to add to Root module PSM1 after merge (here, the Set-Alias exporting IB tasks) -# suffix: suffix.ps1 -# prefix: prefix.ps1 +prefix: prefix.ps1 VersionedOutputDirectory: true #################################################### diff --git a/source/Private/Get-SIDProfileInfo.ps1 b/source/Private/Get-SIDProfileInfo.ps1 index c4f3331..e1c6c7c 100644 --- a/source/Private/Get-SIDProfileInfo.ps1 +++ b/source/Private/Get-SIDProfileInfo.ps1 @@ -98,3 +98,5 @@ function Get-SIDProfileInfo return $ProfileRegistryItems } + + diff --git a/source/Private/Get-SIDProfileInfoFallback.ps1 b/source/Private/Get-SIDProfileInfoFallback.ps1 new file mode 100644 index 0000000..0f066be --- /dev/null +++ b/source/Private/Get-SIDProfileInfoFallback.ps1 @@ -0,0 +1,59 @@ +<# +.SYNOPSIS + Retrieves non-special user profile information from a remote or local computer. + +.DESCRIPTION + The Get-SIDProfileInfoFallback function uses the CIM (Common Information Model) method to retrieve user profile + information from the specified computer. It filters out special profiles and returns the SID, profile path, and other + relevant information for each user profile found on the system. This function serves as a fallback method for obtaining + profile information without requiring administrative privileges to access the registry. + +.PARAMETER ComputerName + The name of the computer to query for user profiles. If not provided, the function will default to the local computer. + +.OUTPUTS + [PSCustomObject[]] + Returns an array of PSCustomObject where each object contains: + - SID: The security identifier for the user profile. + - ProfilePath: The local file system path to the user profile. + - ComputerName: The name of the computer from which the profile was retrieved. + - ExistsInRegistry: Always set to $true, as this function is a fallback and does not query the registry directly. + +.EXAMPLE + Get-SIDProfileInfoFallback + + Retrieves non-special user profiles from the local computer and returns their SID, profile path, and other details. + +.EXAMPLE + Get-SIDProfileInfoFallback -ComputerName "Server01" + + Retrieves non-special user profiles from the remote computer "Server01" and returns their SID, profile path, and other details. + +.NOTES + This function does not require administrative privileges to access profile information, as it relies on CIM/WMI methods + to retrieve data. It specifically filters out special profiles (such as system profiles) using the "Special=False" filter. +#> + +function Get-SIDProfileInfoFallback +{ + [OutputType([PSCustomObject[]])] + [CmdletBinding()] + param ( + [string]$ComputerName = $env:COMPUTERNAME + ) + # Use CIM as a fallback method to get user profile information + $profiles = Get-CimInstance -ClassName Win32_UserProfile -ComputerName $ComputerName -Filter "Special=False" + + $ProfileRegistryItems = foreach ($profile in $profiles) + { + # Return a PSCustomObject similar to what Get-SIDProfileInfo returns + [PSCustomObject]@{ + SID = $profile.SID + ProfilePath = $profile.LocalPath + ComputerName = $ComputerName + ExistsInRegistry = $true + } + } + + return $ProfileRegistryItems +} diff --git a/source/Public/Get-UserProfilesFromRegistry.ps1 b/source/Public/Get-UserProfilesFromRegistry.ps1 index 6885478..bde8a2d 100644 --- a/source/Public/Get-UserProfilesFromRegistry.ps1 +++ b/source/Public/Get-UserProfilesFromRegistry.ps1 @@ -43,9 +43,18 @@ function Get-UserProfilesFromRegistry return @() # Return an empty array } - # Get registry profiles and return them - $RegistryProfiles = Get-SIDProfileInfo -ComputerName $ComputerName -ErrorAction Stop - return $RegistryProfiles + # If user is an admin, use Get-SIDProfileInfo (Registry-based) + if ($ENV:WinProfileOps_IsAdmin -eq $true) + { + Write-Verbose "User has administrator privileges, using registry-based method." + return Get-SIDProfileInfo -ComputerName $ComputerName + } + else + { + Write-Warning "User lacks administrator privileges. Switching to fallback method which excludes special accounts from the results." + return Get-SIDProfileInfoFallback -ComputerName $ComputerName + } + } catch { diff --git a/source/prefix.ps1 b/source/prefix.ps1 new file mode 100644 index 0000000..38443f8 --- /dev/null +++ b/source/prefix.ps1 @@ -0,0 +1,36 @@ +# Your functions + + +# Check if the current user is an administrator +$windowsIdentity = [Security.Principal.WindowsIdentity]::GetCurrent() +$windowsPrincipal = New-Object Security.Principal.WindowsPrincipal($windowsIdentity) +$isAdmin = $windowsPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + +# Set the environment variable based on whether the user is an admin +if ($isAdmin) +{ + $ENV:WinProfileOps_IsAdmin = $true +} +else +{ + $ENV:WinProfileOps_IsAdmin = $false +} + +Write-Verbose "User is an administrator: $ENV:WinProfileOps_IsAdmin" + +[scriptblock]$SB = { + if (Test-Path Env:\WinProfileOps_IsAdmin) + { + Remove-Item Env:\WinProfileOps_IsAdmin + Write-Verbose "WinProfileOps: Removed WinProfileOps_IsAdmin environment variable." + } +} + +Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action { + $sb.Invoke() +} + +# Define the OnRemove script block for the module +$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { + $sb.Invoke() +} diff --git a/tests/Unit/Private/Get-SIDProfileInfoFallback.tests.ps1 b/tests/Unit/Private/Get-SIDProfileInfoFallback.tests.ps1 new file mode 100644 index 0000000..e1edec5 --- /dev/null +++ b/tests/Unit/Private/Get-SIDProfileInfoFallback.tests.ps1 @@ -0,0 +1,144 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Get-SIDProfileInfoFallback' -Tag 'Private' { + + Context 'When retrieving user profiles via fallback' { + + BeforeEach { + InModuleScope -ScriptBlock { + # Mock Get-CimInstance to simulate returning non-special profiles + Mock Get-CimInstance { + return @( + [PSCustomObject]@{ SID = 'S-1-5-21-1001'; LocalPath = 'C:\Users\User1' }, + [PSCustomObject]@{ SID = 'S-1-5-21-1002'; LocalPath = 'C:\Users\User2' } + ) + } + } + } + + It 'Should return profiles from the local computer' { + $ComputerName = $env:COMPUTERNAME + + # Call the private function within the module scope + $result = InModuleScope -ScriptBlock { + Get-SIDProfileInfoFallback -ComputerName $ComputerName + } + + # Validate the results + $result | Should -HaveCount 2 + $result[0].SID | Should -Be 'S-1-5-21-1001' + $result[0].ProfilePath | Should -Be 'C:\Users\User1' + $result[1].SID | Should -Be 'S-1-5-21-1002' + $result[1].ProfilePath | Should -Be 'C:\Users\User2' + + # Assert that Get-CimInstance was called once + Assert-MockCalled Get-CimInstance -Exactly 1 + } + + It 'Should return profiles from a remote computer' { + $result = InModuleScope -ScriptBlock { + + $ComputerName = 'RemotePC' + + # Call the private function within the module scope + + Get-SIDProfileInfoFallback -ComputerName $ComputerName + } + + # Validate the results + $result | Should -HaveCount 2 + $result[0].SID | Should -Be 'S-1-5-21-1001' + $result[0].ProfilePath | Should -Be 'C:\Users\User1' + $result[1].SID | Should -Be 'S-1-5-21-1002' + $result[1].ProfilePath | Should -Be 'C:\Users\User2' + $result[0].ComputerName | Should -Be 'RemotePC' + $result[1].ComputerName | Should -Be 'RemotePC' + + # Assert that Get-CimInstance was called once + Assert-MockCalled Get-CimInstance -Exactly 1 + } + } + + Context 'When no profiles are returned' { + BeforeEach { + InModuleScope -ScriptBlock { + # Mock Get-CimInstance to return an empty list + Mock Get-CimInstance { + return @() + } + } + } + + It 'Should return an empty result when no profiles are found' { + $ComputerName = $env:COMPUTERNAME + + # Call the private function within the module scope + $result = InModuleScope -ScriptBlock { + Get-SIDProfileInfoFallback -ComputerName $ComputerName + } + + # Validate the result is empty + $result | Should -BeNullOrEmpty + + # Assert that Get-CimInstance was called once + Assert-MockCalled Get-CimInstance -Exactly 1 + } + } + + Context 'When Get-CimInstance throws an error' { + BeforeEach { + InModuleScope -ScriptBlock { + # Mock Get-CimInstance to throw an error + Mock Get-CimInstance { + throw "CIM query failed" + } + + # Mock Write-Error to capture the error + Mock Write-Error + } + } + + It 'Should log an error and return nothing when Get-CimInstance fails' { + $ComputerName = $env:COMPUTERNAME + + # Call the private function within the module scope + $result = InModuleScope -ScriptBlock { + try + { + Get-SIDProfileInfoFallback -ComputerName $ComputerName + } + catch + { + Write-Error "Error retrieving profiles via CIM" + } + } + + # The result should be empty + $result | Should -BeNullOrEmpty + + # Assert that Write-Error was called + Assert-MockCalled Write-Error -Exactly 1 + + # Assert that Get-CimInstance was called once + Assert-MockCalled Get-CimInstance -Exactly 1 + } + } + +} diff --git a/tests/Unit/Public/Get-UserProfilesFromRegistry.tests.ps1 b/tests/Unit/Public/Get-UserProfilesFromRegistry.tests.ps1 index 90f9eeb..5d18fe7 100644 --- a/tests/Unit/Public/Get-UserProfilesFromRegistry.tests.ps1 +++ b/tests/Unit/Public/Get-UserProfilesFromRegistry.tests.ps1 @@ -28,19 +28,25 @@ Describe 'Get-UserProfilesFromRegistry' -Tags 'Unit' { BeforeEach { InModuleScope -ScriptBlock { - - # Mock Get-SIDProfileInfo to return a list of profiles - Mock Get-SIDProfileInfo { - return @( - [PSCustomObject]@{ SID = 'S-1-5-21-1001'; ProfilePath = 'C:\Users\User1' }, - [PSCustomObject]@{ SID = 'S-1-5-21-1002'; ProfilePath = 'C:\Users\User2' } - ) + # Mock Test-ComputerPing to always return true + Mock Test-ComputerPing { + return $true } - } } - It 'Should return the user profiles from the local registry' { + It 'Should return the user profiles from the local registry when user has admin privileges' { + # Mock admin environment variable + $ENV:WinProfileOps_IsAdmin = $true + + # Mock Get-SIDProfileInfo to return a list of profiles + Mock Get-SIDProfileInfo { + return @( + [PSCustomObject]@{ SID = 'S-1-5-21-1001'; ProfilePath = 'C:\Users\User1' }, + [PSCustomObject]@{ SID = 'S-1-5-21-1002'; ProfilePath = 'C:\Users\User2' } + ) + } + $ComputerName = $env:COMPUTERNAME $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName @@ -56,13 +62,45 @@ Describe 'Get-UserProfilesFromRegistry' -Tags 'Unit' { Assert-MockCalled Get-SIDProfileInfo -Exactly 1 Assert-MockCalled Test-ComputerPing -Exactly 1 -Scope It } + + It 'Should return the user profiles from the local computer using fallback when user lacks admin privileges' { + # Mock non-admin environment variable + $ENV:WinProfileOps_IsAdmin = $false + + # Mock Get-SIDProfileInfoFallback to return a list of profiles + Mock Get-SIDProfileInfoFallback { + return @( + [PSCustomObject]@{ SID = 'S-1-5-21-1001'; ProfilePath = 'C:\Users\User1' }, + [PSCustomObject]@{ SID = 'S-1-5-21-1002'; ProfilePath = 'C:\Users\User2' } + ) + } + + $ComputerName = $env:COMPUTERNAME + + $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName + + # Validate result + $result | Should -HaveCount 2 + $result[0].SID | Should -Be 'S-1-5-21-1001' + $result[0].ProfilePath | Should -Be 'C:\Users\User1' + $result[1].SID | Should -Be 'S-1-5-21-1002' + $result[1].ProfilePath | Should -Be 'C:\Users\User2' + + # Assert that the fallback method was called + Assert-MockCalled Get-SIDProfileInfoFallback -Exactly 1 + Assert-MockCalled Test-ComputerPing -Exactly 1 -Scope It + } } # Test for profiles from a remote computer Context 'When retrieving profiles from a remote computer' { - BeforeEach { + + It 'Should return the user profiles from a remote registry when user has admin privileges' { InModuleScope -ScriptBlock { + # Mock admin environment variable + $ENV:WinProfileOps_IsAdmin = $true + # Mock Get-SIDProfileInfo to return a list of profiles Mock Get-SIDProfileInfo { return @( @@ -71,9 +109,7 @@ Describe 'Get-UserProfilesFromRegistry' -Tags 'Unit' { ) } } - } - It 'Should return the user profiles from the remote registry' { $ComputerName = 'RemotePC' $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName @@ -87,24 +123,57 @@ Describe 'Get-UserProfilesFromRegistry' -Tags 'Unit' { # Assert that Get-SIDProfileInfo was called Assert-MockCalled Get-SIDProfileInfo -Exactly 1 - Assert-MockCalled Test-ComputerPing -Exactly 1 -Scope It + Assert-MockCalled Test-ComputerPing -Exactly 1 + } + + It 'Should return the user profiles from a remote computer when user lacks admin privileges' { + + InModuleScope -ScriptBlock { + # Change the environment to simulate non-admin + $ENV:WinProfileOps_IsAdmin = $false + + # Mock the fallback method to return profiles + Mock Get-SIDProfileInfoFallback { + return @( + [PSCustomObject]@{ SID = 'S-1-5-21-2001'; ProfilePath = 'C:\Users\User1' }, + [PSCustomObject]@{ SID = 'S-1-5-21-2002'; ProfilePath = 'C:\Users\User2' } + ) + } + } + + $ComputerName = 'RemotePC' + + $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName + + # Validate result + $result | Should -HaveCount 2 + $result[0].SID | Should -Be 'S-1-5-21-2001' + $result[0].ProfilePath | Should -Be 'C:\Users\User1' + $result[1].SID | Should -Be 'S-1-5-21-2002' + $result[1].ProfilePath | Should -Be 'C:\Users\User2' + + # Assert that the fallback method was called + Assert-MockCalled Get-SIDProfileInfoFallback -Exactly 1 + Assert-MockCalled Test-ComputerPing -Exactly 1 } } + # Test when no profiles are found Context 'When no profiles are found' { BeforeEach { - InModuleScope -ScriptBlock { + # Mock admin environment variable + $ENV:WinProfileOps_IsAdmin = $true + # Mock Get-SIDProfileInfo to return an empty list Mock Get-SIDProfileInfo { return @() } - } } - It 'Should return an empty result when no profiles are found' { + It 'Should return an empty result when no profiles are found (admin)' { $ComputerName = $env:COMPUTERNAME $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName @@ -114,45 +183,146 @@ Describe 'Get-UserProfilesFromRegistry' -Tags 'Unit' { # Assert that Get-SIDProfileInfo was called Assert-MockCalled Get-SIDProfileInfo -Exactly 1 - Assert-MockCalled Test-ComputerPing -Exactly 1 -Scope It + Assert-MockCalled Test-ComputerPing -Exactly 1 + } + + It 'Should return an empty result when no profiles are found (non-admin)' { + # Change the environment to simulate non-admin + $ENV:WinProfileOps_IsAdmin = $false + + # Mock the fallback method to return an empty list + Mock Get-SIDProfileInfoFallback { + return @() + } -ModuleName $Script:dscModuleName + + $ComputerName = $env:COMPUTERNAME + + $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName + + # The result should be empty + $result | Should -BeNullOrEmpty + + # Assert that the fallback method was called + Assert-MockCalled Get-SIDProfileInfoFallback -Exactly 1 + Assert-MockCalled Test-ComputerPing -Exactly 1 } } + # Test when Get-SIDProfileInfo fails - Context 'When Get-SIDProfileInfo fails' { + Context 'When both registry and fallback profile retrieval methods fail' { BeforeEach { + InModuleScope -ScriptBlock { + # Mock Test-ComputerPing to simulate the computer being online + Mock Test-ComputerPing { + return $true + } + + # Mock Write-Error to capture the error + Mock Write-Error + } + } + + It 'Should log an error and return nothing when Get-SIDProfileInfo fails (admin)' { + # Simulate user has admin privileges + $ENV:WinProfileOps_IsAdmin = $true InModuleScope -ScriptBlock { + + # Mock Write-Error to capture the error + Mock Write-Error + + # Mock Test-ComputerPing to simulate the computer being online + Mock Test-ComputerPing { + return $true + } + + # Mock Get-SIDProfileInfoFallback to simulate an error + Mock Get-SIDProfileInfoFallback + # Mock Get-SIDProfileInfo to simulate an error Mock Get-SIDProfileInfo { throw "Failed to retrieve profiles from the registry" } + } + + $ComputerName = $env:COMPUTERNAME + + $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName + + # The result should be empty + $result | Should -BeNullOrEmpty + + # Assert that Write-Error was called to log the error + Assert-MockCalled Write-Error -Exactly 1 + + # Assert that Get-SIDProfileInfo was called once + Assert-MockCalled Get-SIDProfileInfo -Exactly 1 + + # Assert that Test-ComputerPing was called once + Assert-MockCalled Test-ComputerPing -Exactly 1 -Scope It + + # Ensure the fallback function was not called + Assert-MockCalled Get-SIDProfileInfoFallback -Exactly 0 + } + + It 'Should log an error and return nothing when Get-SIDProfileInfoFallback fails (non-admin)' { + # Simulate user lacks admin privileges + $ENV:WinProfileOps_IsAdmin = $false + + InModuleScope -ScriptBlock { + # Mock Write-Error to capture the error Mock Write-Error + + # Mock Test-ComputerPing to simulate the computer being online + Mock Test-ComputerPing { + return $true + } + + # Mock Get-SIDProfileInfoFallback to simulate an error + Mock Get-SIDProfileInfoFallback { + throw "Failed to retrieve profiles via fallback method" + } + + mock Get-SIDProfileInfo { + return @() + } + } - } - It 'Should log an error and return nothing' { $ComputerName = $env:COMPUTERNAME $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName # The result should be empty $result | Should -BeNullOrEmpty - # Assert that Write-Error was called + + # Assert that Write-Error was called to log the error Assert-MockCalled Write-Error -Exactly 1 - Assert-MockCalled Get-SIDProfileInfo -Exactly 1 + + # Assert that Get-SIDProfileInfoFallback was called once + Assert-MockCalled Get-SIDProfileInfoFallback -Exactly 1 + + # Assert that Test-ComputerPing was called once Assert-MockCalled Test-ComputerPing -Exactly 1 -Scope It + + # Ensure the admin function was not called + Assert-MockCalled Get-SIDProfileInfo -Exactly 0 } } + Context 'When partial profile data is returned' { BeforeEach { InModuleScope -ScriptBlock { - # Mock Get-SIDProfileInfo to return incomplete profile data + # Mock admin environment variable + $ENV:WinProfileOps_IsAdmin = $true + + # Mock Get-SIDProfileInfo to return partial data Mock Get-SIDProfileInfo { return @( [PSCustomObject]@{ SID = 'S-1-5-21-1001'; ProfilePath = '' }, # Missing profile path @@ -160,11 +330,9 @@ Describe 'Get-UserProfilesFromRegistry' -Tags 'Unit' { ) } } - - } - It 'Should still return the profiles with partial data' { + It 'Should still return the profiles with partial data when user has admin privileges' { $ComputerName = $env:COMPUTERNAME $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName @@ -178,7 +346,34 @@ Describe 'Get-UserProfilesFromRegistry' -Tags 'Unit' { # Assert that Get-SIDProfileInfo was called Assert-MockCalled Get-SIDProfileInfo -Exactly 1 - Assert-MockCalled Test-ComputerPing -Exactly 1 -Scope It + Assert-MockCalled Test-ComputerPing -Exactly 1 + } + + It 'Should still return the profiles with partial data when user lacks admin privileges' { + # Change the environment to simulate non-admin + $ENV:WinProfileOps_IsAdmin = $false + + # Mock the fallback method to return partial data + Mock Get-SIDProfileInfoFallback { + return @( + [PSCustomObject]@{ SID = 'S-1-5-21-1001'; ProfilePath = '' }, + [PSCustomObject]@{ SID = ''; ProfilePath = 'C:\Users\User2' } + ) + } -ModuleName $script:dscModuleName + + $ComputerName = $env:COMPUTERNAME + $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName + + # Validate result, even with partial data + $result | Should -HaveCount 2 + $result[0].SID | Should -Be 'S-1-5-21-1001' + $result[0].ProfilePath | Should -BeNullOrEmpty + $result[1].SID | Should -BeNullOrEmpty + $result[1].ProfilePath | Should -Be 'C:\Users\User2' + + # Assert that the fallback method was called + Assert-MockCalled Get-SIDProfileInfoFallback -Exactly 1 + Assert-MockCalled Test-ComputerPing -Exactly 1 } } @@ -188,6 +383,11 @@ Describe 'Get-UserProfilesFromRegistry' -Tags 'Unit' { Mock Test-ComputerPing { return $true } + } + + It 'Should return profiles when the computer is online and user has admin privileges' { + # Mock admin environment variable + $ENV:WinProfileOps_IsAdmin = $true # Mock Get-SIDProfileInfo to return a list of profiles Mock Get-SIDProfileInfo { @@ -197,70 +397,108 @@ Describe 'Get-UserProfilesFromRegistry' -Tags 'Unit' { ) } + $result = Get-UserProfilesFromRegistry -ComputerName $env:COMPUTERNAME + + # Validate result + $result | Should -HaveCount 2 + $result[0].SID | Should -Be 'S-1-5-21-1001' + $result[1].SID | Should -Be 'S-1-5-21-1002' + + # Assert that Test-ComputerPing and Get-SIDProfileInfo were called + Assert-MockCalled Test-ComputerPing -Exactly 1 + Assert-MockCalled Get-SIDProfileInfo -Exactly 1 } - It 'Should return profiles when the computer is online' { + It 'Should return profiles when the computer is online and user lacks admin privileges' { + # Mock non-admin environment variable + $ENV:WinProfileOps_IsAdmin = $false + + # Mock Get-SIDProfileInfoFallback to return a list of profiles + Mock Get-SIDProfileInfoFallback { + return @( + [PSCustomObject]@{ SID = 'S-1-5-21-1001'; ProfilePath = 'C:\Users\User1' }, + [PSCustomObject]@{ SID = 'S-1-5-21-1002'; ProfilePath = 'C:\Users\User2' } + ) + } -ModuleName $Script:dscModuleName + $result = Get-UserProfilesFromRegistry -ComputerName $env:COMPUTERNAME + # Validate result $result | Should -HaveCount 2 $result[0].SID | Should -Be 'S-1-5-21-1001' $result[1].SID | Should -Be 'S-1-5-21-1002' + # Assert that Test-ComputerPing and Get-SIDProfileInfoFallback were called Assert-MockCalled Test-ComputerPing -Exactly 1 - Assert-MockCalled Get-SIDProfileInfo -Exactly 1 + Assert-MockCalled Get-SIDProfileInfoFallback -Exactly 1 } } + # Test when the computer is offline/unreachable Context 'When the computer is offline or unreachable' { BeforeEach { - - InModuleScope -ScriptBlock { - # Mock Get-SIDProfileInfo to return incomplete profile data + # Mock Get-SIDProfileInfo to ensure it is not called Mock Get-SIDProfileInfo - } + # Mock Get-SIDProfileInfoFallback to ensure it is not called + Mock Get-SIDProfileInfoFallback - # Mock Test-ComputerPing to simulate the computer being offline - Mock Test-ComputerPing { - return $false - } + # Mock Test-ComputerPing to simulate the computer being offline + Mock Test-ComputerPing { + return $false + } - # Mock Write-Warning to capture the warning - Mock Write-Warning + # Mock Write-Warning to capture the warning + Mock Write-Warning + } } It 'Should log a warning and return nothing when the computer is unreachable' { $ComputerName = 'OfflinePC' + + $env:WinProfileOps_IsAdmin = $true + $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName + # Validate that the result is empty since the computer is unreachable $result | Should -BeNullOrEmpty + + # Assert that Test-ComputerPing was called Assert-MockCalled Test-ComputerPing -Exactly 1 + + # Assert that Write-Warning was called to log the unreachable computer Assert-MockCalled Write-Warning -Exactly 1 + + # Assert that neither Get-SIDProfileInfo nor Get-SIDProfileInfoFallback was called Assert-MockCalled Get-SIDProfileInfo -Exactly 0 + Assert-MockCalled Get-SIDProfileInfoFallback -Exactly 0 } } # Test for profiles when Test-ComputerPing fails Context 'When Test-ComputerPing fails' { - BeforeEach { + It 'Should log an error and return nothing when ping fails' { InModuleScope -ScriptBlock { # Mock Get-SIDProfileInfo to return incomplete profile data Mock Get-SIDProfileInfo - } - # Mock Test-ComputerPing to simulate an error or failure - Mock Test-ComputerPing { - throw "Ping failed" + + # Mock Test-ComputerPing to simulate an error or failure + Mock Test-ComputerPing { + throw "Ping failed" + } + } + $env:WinProfileOps_IsAdmin = $true + # Mock Write-Error to capture the error Mock Write-Error - } - It 'Should log an error and return nothing when ping fails' { + $ComputerName = 'RemotePC' $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName @@ -272,4 +510,138 @@ Describe 'Get-UserProfilesFromRegistry' -Tags 'Unit' { } } + Context 'When user has administrator privileges' { + BeforeEach { + InModuleScope -ScriptBlock { + # Mock admin environment variable + $ENV:WinProfileOps_IsAdmin = $true + + # Mock Get-SIDProfileInfo to return a list of profiles + Mock Get-SIDProfileInfo { + return @( + [PSCustomObject]@{ SID = 'S-1-5-21-1001'; ProfilePath = 'C:\Users\User1' }, + [PSCustomObject]@{ SID = 'S-1-5-21-1002'; ProfilePath = 'C:\Users\User2' } + ) + } + } + } + + It 'Should use registry-based method when user has admin privileges' { + $ComputerName = $env:COMPUTERNAME + + $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName + + # Validate result + $result | Should -HaveCount 2 + $result[0].SID | Should -Be 'S-1-5-21-1001' + $result[0].ProfilePath | Should -Be 'C:\Users\User1' + $result[1].SID | Should -Be 'S-1-5-21-1002' + $result[1].ProfilePath | Should -Be 'C:\Users\User2' + + # Assert that Get-SIDProfileInfo was called + Assert-MockCalled Get-SIDProfileInfo -Exactly 1 + Assert-MockCalled Test-ComputerPing -Exactly 1 -Scope It + } + } + + # Test for profiles when the user does not have admin privileges + Context 'When user lacks administrator privileges' { + BeforeEach { + InModuleScope -ScriptBlock { + # Mock non-admin environment variable + $ENV:WinProfileOps_IsAdmin = $false + + # Mock Get-SIDProfileInfoFallback to return a list of profiles + Mock Get-SIDProfileInfoFallback { + return @( + [PSCustomObject]@{ SID = 'S-1-5-21-2001'; ProfilePath = 'C:\Users\User1' }, + [PSCustomObject]@{ SID = 'S-1-5-21-2002'; ProfilePath = 'C:\Users\User2' } + ) + } + } + } + + It 'Should use fallback method when user lacks admin privileges' { + $ComputerName = $env:COMPUTERNAME + + $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName + + # Validate result + $result | Should -HaveCount 2 + $result[0].SID | Should -Be 'S-1-5-21-2001' + $result[0].ProfilePath | Should -Be 'C:\Users\User1' + $result[1].SID | Should -Be 'S-1-5-21-2002' + $result[1].ProfilePath | Should -Be 'C:\Users\User2' + + # Assert that Get-SIDProfileInfoFallback was called + Assert-MockCalled Get-SIDProfileInfoFallback -Exactly 1 + Assert-MockCalled Test-ComputerPing -Exactly 1 -Scope It + } + } + + # Test when Get-SIDProfileInfoFallback fails + Context 'When Get-SIDProfileInfoFallback fails' { + BeforeEach { + InModuleScope -ScriptBlock { + # Mock non-admin environment variable + $ENV:WinProfileOps_IsAdmin = $false + + # Mock Get-SIDProfileInfoFallback to simulate an error + Mock Get-SIDProfileInfoFallback { + throw "Failed to retrieve profiles via fallback method" + } + + # Mock Write-Error to capture the error + Mock Write-Error + } + } + + It 'Should log an error and return nothing when fallback method fails' { + $ComputerName = $env:COMPUTERNAME + + $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName + + # The result should be empty + $result | Should -BeNullOrEmpty + # Assert that Write-Error was called + Assert-MockCalled Write-Error -Exactly 1 + Assert-MockCalled Get-SIDProfileInfoFallback -Exactly 1 + Assert-MockCalled Test-ComputerPing -Exactly 1 -Scope It + } + } + + # Test for when the computer is unreachable + Context 'When the computer is unreachable' { + BeforeEach { + InModuleScope -ScriptBlock { + # Mock Test-ComputerPing to simulate the computer being unreachable + Mock Test-ComputerPing { + return $false + } + + # Mock Write-Warning to capture the warning + Mock Write-Warning + + Mock Get-SIDProfileInfoFallback + + Mock Get-SIDProfileInfo + } + } + + It 'Should log a warning and return nothing when the computer is unreachable' { + $ComputerName = 'OfflinePC' + + $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName + + # The result should be empty + $result | Should -BeNullOrEmpty + + # Assert that Test-ComputerPing and Write-Warning were called + Assert-MockCalled Test-ComputerPing -Exactly 1 + Assert-MockCalled Write-Warning -Exactly 1 + Assert-MockCalled Get-SIDProfileInfoFallback -Exactly 0 + Assert-MockCalled Get-SIDProfileInfo -Exactly 0 + } + } + }