From 4d44b4a783c305c9a3c64efe5564b9de24afd276 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Wed, 10 Jul 2019 13:18:44 +0200 Subject: [PATCH] Changes to xActiveDirectory - Added new helper functions in xActiveDirectory.Common. - New-CimCredentialInstance - Add-TypeAssembly - New-ADDirectoryContext --- .../xActiveDirectory.Common.strings.psd1 | 9 +- .../xActiveDirectory.Common.psm1 | 189 +++++++++++++++++- Tests/Unit/xActiveDirectory.Common.Tests.ps1 | 131 ++++++++++++ 3 files changed, 322 insertions(+), 7 deletions(-) diff --git a/Modules/xActiveDirectory.Common/en-US/xActiveDirectory.Common.strings.psd1 b/Modules/xActiveDirectory.Common/en-US/xActiveDirectory.Common.strings.psd1 index 079f6ffa4..c9290a4b1 100644 --- a/Modules/xActiveDirectory.Common/en-US/xActiveDirectory.Common.strings.psd1 +++ b/Modules/xActiveDirectory.Common/en-US/xActiveDirectory.Common.strings.psd1 @@ -10,7 +10,7 @@ ConvertFrom-StringData @' ArrayValueThatDoesNotMatch = {0} - {1} (ADCOMMON0007) PropertyValueOfTypeDoesNotMatch = {0} value does not match. Current value is '{1}', but expected the value '{2}'. (ADCOMMON0008) UnableToCompareType = Unable to compare the type {0} as it is not handled by the Test-DscPropertyState cmdlet. (ADCOMMON0009) - RoleNotFoundError = Please ensure that the PowerShell module for role '{0}' is installed. (ADCOMMON0010) + ModuleNotFoundError = Please ensure that the PowerShell module for role '{0}' is installed. (ADCOMMON0010) MembersAndIncludeExcludeError = The '{0}' and '{1}' and/or '{2}' parameters conflict. The '{0}' parameter should not be used in any combination with the '{1}' and '{2}' parameters. (ADCOMMON0011) MembersIsNullError = The Members parameter value is null. The '{0}' parameter must be provided if neither '{1}' nor '{2}' is provided. (ADCOMMON0012) IncludeAndExcludeConflictError = The member '{0}' is included in both '{1}' and '{2}' parameter values. The same member must not be included in both '{1}' and '{2}' parameter values. (ADCOMMON0014) @@ -39,4 +39,11 @@ ConvertFrom-StringData @' ValueOfTypeDoesNotMatch = {0} value for property {1} does not match. Current state is '{2}' and desired state is '{3}'. (ADCOMMON0039) UnableToCompareProperty = Unable to compare property {0} as the type {1} is not handled by the Test-DscParameterState cmdlet. (ADCOMMON0040) StartProcess = Started the process with id {0} using the path '{1}', and with a timeout value of {2} seconds. (ADCOMMON0041) + CouldNotLoadAssembly = The assembly '{0}' could not be loaded into the PowerShell session. (ADCOMMON0042) + TypeAlreadyExistInSession = The type '{0}' is already loaded into the PowerShell session. (ADCOMMON0043) + TypeDoesNotExistInSession = Missing the type '{0}' from the PowerShell session. (ADCOMMON0044) + AddingAssemblyToSession = Adding the assembly '{0}' into the PowerShell session. (ADCOMMON0045) + NewDirectoryContext = Get a new Active Directory context of the type '{0}'. (ADCOMMON0046) + NewDirectoryContextTarget = The Active Directory context will target '{0}'. (ADCOMMON0047) + NewDirectoryContextCredential = The Active Directory context will be accessed using the '{0}' credentials. (ADCOMMON0048) '@ diff --git a/Modules/xActiveDirectory.Common/xActiveDirectory.Common.psm1 b/Modules/xActiveDirectory.Common/xActiveDirectory.Common.psm1 index 605f738a5..37d04e830 100644 --- a/Modules/xActiveDirectory.Common/xActiveDirectory.Common.psm1 +++ b/Modules/xActiveDirectory.Common/xActiveDirectory.Common.psm1 @@ -489,7 +489,7 @@ function Assert-Module if (-not (Get-Module -Name $ModuleName -ListAvailable)) { $errorId = '{0}_ModuleNotFound' -f $ModuleName - $errorMessage = $script:localizedData.RoleNotFoundError -f $moduleName + $errorMessage = $script:localizedData.ModuleNotFoundError -f $moduleName ThrowInvalidOperationError -ErrorId $errorId -ErrorMessage $errorMessage } @@ -1546,10 +1546,9 @@ function Convert-PropertyMapToObjectProperties values. Normally set to $PSBoundParameters. .PARAMETER Properties - An array of property names to filter out from the keys provided in - DesiredValues. If left out, only those keys in the DesiredValues will - be compared. This parameter can be used to remove certain keys from - the comparison. + An array of property names, from the keys provided in DesiredValues, that + will be compared. If this parameter is left out, all the keys in the + DesiredValues will be compared. #> function Compare-ResourcePropertyState { @@ -1776,7 +1775,8 @@ function Assert-ADPSDrive if ($null -eq $activeDirectoryPSDrive) { - Write-Verbose -Message $script:localizedData.CreatingNewADPSDrive + Write-Verbose -Message $script:localizedData.CreatingNewADPSDrive -Verbose + try { New-PSDrive -Name AD -PSProvider 'ActiveDirectory' -Root $Root -Scope Script -ErrorAction 'Stop' | @@ -1814,6 +1814,180 @@ function Set-DscADComputer Set-ADComputer @Parameters | Out-Null } +<# + .SYNOPSIS + This returns a new MSFT_Credential CIM instance credential object to be + used when returning credential objects from Get-TargetResource. + This returns a credential object without the password. + + .PARAMETER Credential + The PSCredential object to return as a MSFT_Credential CIM instance + credential object. + + .NOTES + When returning a PSCredential object from Get-TargetResource, the + credential object does not contain the username. The object is empty. + + Password UserName PSComputerName + -------- -------- -------------- + localhost + + When the MSFT_Credential CIM instance credential object is returned by + the Get-TargetResource then the credential object contains the values + provided in the object. + + Password UserName PSComputerName + -------- -------- -------------- + COMPANY\TestAccount localhost +#> +function New-CimCredentialInstance +{ + [CmdletBinding()] + [OutputType([Microsoft.Management.Infrastructure.CimInstance])] + param + ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $Credential + ) + + $newCimInstanceParameters = @{ + ClassName = 'MSFT_Credential' + ClientOnly = $true + Namespace = 'root/microsoft/windows/desiredstateconfiguration' + Property = @{ + UserName = [System.String] $Credential.UserName + Password = [System.String] $null + } + } + + return New-CimInstance @newCimInstanceParameters +} + +<# + .SYNOPSIS + This loads the assembly type, optionally after a check + if the type is missing in the PowerShell session. + + .PARAMETER AssemblyName + The assembly to load into the PowerShell session. + + .PARAMETER TypeName + An optional parameter to check if the type exist, if it exist then the + assembly is not loaded again. +#> +function Add-TypeAssembly +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $AssemblyName, + + [Parameter()] + [System.String] + $TypeName + ) + + if ($PSBoundParameters.ContainsKey('TypeName')) + { + if ($TypeName -as [Type]) + { + Write-Verbose -Message ($script:localizedData.TypeAlreadyExistInSession -f $TypeName) -Verbose + + # The type already exist so no need to load the type again. + return + } + else + { + Write-Verbose -Message ($script:localizedData.TypeDoesNotExistInSession -f $TypeName) -Verbose + } + } + + try + { + Write-Verbose -Message ($script:localizedData.AddingAssemblyToSession -f $AssemblyName) -Verbose + + Add-Type -AssemblyName $AssemblyName + } + catch + { + $missingRoleMessage = $script:localizedData.CouldNotLoadAssembly -f $AssemblyName + New-ObjectNotFoundException -Message $missingRoleMessage -ErrorRecord $_ + } +} + +<# + .SYNOPSIS + This returns a new object of the type System.DirectoryServices.ActiveDirectory.DirectoryContext. + + .PARAMETER DirectoryContextType + The context type of the object to return. Valid values are 'Domain', 'Forest', + 'ApplicationPartition', 'ConfigurationSet' or 'DirectoryServer'. + + .PARAMETER Name + An optional parameter for the target of the directory context. + For the correct format for this parameter depending on context type, see + the article https://docs.microsoft.com/en-us/dotnet/api/system.directoryservices.activedirectory.directorycontext?view=netframework-4.8 +#> +function Get-ADDirectoryContext +{ + [CmdletBinding()] + [OutputType([System.DirectoryServices.ActiveDirectory.DirectoryContext])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Domain', 'Forest', 'ApplicationPartition', 'ConfigurationSet', 'DirectoryServer')] + [System.String] + $DirectoryContextType, + + [Parameter()] + [System.String] + $Name, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential + ) + + $typeName = 'System.DirectoryServices.ActiveDirectory.DirectoryContext' + + Add-TypeAssembly -AssemblyName 'System.DirectoryServices' -TypeName $typeName + + Write-Verbose -Message ($script:localizedData.NewDirectoryContext -f $DirectoryContextType) -Verbose + + $newObjectArgumentList = @( + $DirectoryContextType + ) + + if ($PSBoundParameters.ContainsKey('Name')) + { + Write-Verbose -Message ($script:localizedData.NewDirectoryContextTarget -f $Name) -Verbose + + $newObjectArgumentList += @( + $Name + ) + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + Write-Verbose -Message ($script:localizedData.NewDirectoryContextCredential -f $Credential.UserName) -Verbose + + $newObjectArgumentList += @( + $Credential.UserName + $Credential.GetNetworkCredential().Password + ) + } + + $newObjectParameters = @{ + TypeName = $typeName + ArgumentList = $newObjectArgumentList + } + + return New-Object @newObjectParameters +} + $script:localizedData = Get-LocalizedData -ResourceName 'xActiveDirectory.Common' -ScriptRoot $PSScriptRoot Export-ModuleMember -Function @( @@ -1850,4 +2024,7 @@ Export-ModuleMember -Function @( 'Test-DscPropertyState' 'Assert-ADPSDrive' 'Set-DscADComputer' + 'New-CimCredentialInstance' + 'Add-TypeAssembly' + 'Get-ADDirectoryContext' ) diff --git a/Tests/Unit/xActiveDirectory.Common.Tests.ps1 b/Tests/Unit/xActiveDirectory.Common.Tests.ps1 index b9822f278..e04e75ecc 100644 --- a/Tests/Unit/xActiveDirectory.Common.Tests.ps1 +++ b/Tests/Unit/xActiveDirectory.Common.Tests.ps1 @@ -2115,4 +2115,135 @@ InModuleScope 'xActiveDirectory.Common' { } } } + + Describe 'xActiveDirectory.CommonNew-CimCredentialInstance' { + Context 'When creating a new MSFT_Credential CIM instance credential object' { + BeforeAll { + $mockAdministratorUser = 'admin@contoso.com' + $mockAdministratorPassword = 'P@ssw0rd-12P@ssw0rd-12' + $mockAdministratorCredential = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList @( + $mockAdministratorUser, + ($mockAdministratorPassword | ConvertTo-SecureString -AsPlainText -Force) + ) + } + + It 'Should return the correct values' { + $newCimCredentialInstanceResult = New-CimCredentialInstance -Credential $mockAdministratorCredential + $newCimCredentialInstanceResult | Should -BeOfType 'Microsoft.Management.Infrastructure.CimInstance' + $newCimCredentialInstanceResult.CimClass.CimClassName | Should -Be 'MSFT_Credential' + $newCimCredentialInstanceResult.UserName | Should -Be $mockAdministratorUser + $newCimCredentialInstanceResult.Password | Should -BeNullOrEmpty + } + } + } + + Describe 'xActiveDirectory.Common\Add-TypeAssembly' { + Context 'When assembly fails to load' { + BeforeAll { + Mock -CommandName Add-Type -MockWith { + throw + } + + $mockAssembly = 'MyAssembly' + } + + It 'Should throw the correct error' { + { Add-TypeAssembly -AssemblyName $mockAssembly } | Should -Throw ($script:localizedData.CouldNotLoadAssembly -f $mockAssembly) + } + } + + Context 'When loading an assembly into the session' { + BeforeAll { + Mock -CommandName Add-Type + + $mockAssembly = 'MyAssembly' + } + + It 'Should not throw and call the correct mocks' { + { Add-TypeAssembly -AssemblyName $mockAssembly } | Should -Not -Throw + + Assert-MockCalled -CommandName Add-Type -ParameterFilter { + $AssemblyName -eq $mockAssembly + } -Exactly -Times 1 -Scope It + } + + Context 'When the type is already loaded into the session' { + It 'Should not throw and not call any mocks' { + { Add-TypeAssembly -AssemblyName $mockAssembly -TypeName 'System.String' } | Should -Not -Throw + + Assert-MockCalled -CommandName Add-Type -Exactly -Times 0 -Scope It + } + } + + Context 'When the type is missing from the session' { + It 'Should not throw and call the correct mocks' { + { Add-TypeAssembly -AssemblyName $mockAssembly -TypeName 'My.Type' } | Should -Not -Throw + + Assert-MockCalled -CommandName Add-Type -ParameterFilter { + $AssemblyName -eq $mockAssembly + } -Exactly -Times 1 -Scope It + } + } + } + } + + Describe 'xActiveDirectory.Common\New-ADDirectoryContext' { + Context 'When creating a new Active Directory context' { + BeforeAll { + # This credential object must be created before we mock New-Object. + $mockAdministratorUser = 'admin@contoso.com' + $mockAdministratorPassword = 'P@ssw0rd-12P@ssw0rd-12' + $mockAdministratorCredential = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList @( + $mockAdministratorUser, + ($mockAdministratorPassword | ConvertTo-SecureString -AsPlainText -Force) + ) + + Mock -CommandName Add-TypeAssembly -Verifiable + Mock -CommandName New-Object + } + + Context 'When the calling with only parameter DirectoryContextType' { + It 'Should not throw and call the correct mocks' { + { Get-ADDirectoryContext -DirectoryContextType 'Domain' } | Should -Not -Throw + + Assert-MockCalled -CommandName New-Object -ParameterFilter { + $ArgumentList.Count -eq 1 ` + -and $ArgumentList[0] -eq 'Domain' + } -Exactly -Times 1 -Scope It + } + } + + Context 'When the calling with parameters DirectoryContextType and Name' { + It 'Should not throw and call the correct mocks' { + { + Get-ADDirectoryContext -DirectoryContextType 'Domain' -Name 'my.domain' + } | Should -Not -Throw + + Assert-MockCalled -CommandName New-Object -ParameterFilter { + $ArgumentList.Count -eq 2 ` + -and $ArgumentList[0] -eq 'Domain' ` + -and $ArgumentList[1] -eq 'my.domain' + } -Exactly -Times 1 -Scope It + } + } + + Context 'When the calling with parameters DirectoryContextType, Name and Credential' { + It 'Should not throw and call the correct mocks' { + { + Get-ADDirectoryContext -DirectoryContextType 'Domain' -Name 'my.domain' -Credential $mockAdministratorCredential + } | Should -Not -Throw + + Assert-MockCalled -CommandName New-Object -ParameterFilter { + $ArgumentList.Count -eq 4 ` + -and $ArgumentList[0] -eq 'Domain' ` + -and $ArgumentList[1] -eq 'my.domain' ` + -and $ArgumentList[2] -eq $mockAdministratorUser ` + -and $ArgumentList[3] -eq $mockAdministratorPassword + } -Exactly -Times 1 -Scope It + } + } + + Assert-VerifiableMock + } + } }