Modules/xActiveDirectory.Common/xActiveDirectory.Common.psm1
<#
.SYNOPSIS Retrieves the localized string data based on the machine's culture. Falls back to en-US strings if the machine's culture is not supported. .PARAMETER ResourceName The name of the resource as it appears before '.strings.psd1' of the localized string file. For example: For WindowsOptionalFeature: MSFT_WindowsOptionalFeature For Service: MSFT_ServiceResource For Registry: MSFT_RegistryResource For Helper: SqlServerDscHelper .PARAMETER ScriptRoot Optional. The root path where to expect to find the culture folder. This is only needed for localization in helper modules. This should not normally be used for resources. .NOTES To be able to use localization in the helper function, this function must be first in the file, before Get-LocalizedData is used by itself to load localized data for this helper module (see directly after this function). #> function Get-LocalizedData { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ResourceName, [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $ScriptRoot ) if (-not $ScriptRoot) { $dscResourcesFolder = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'DSCResources' $resourceDirectory = Join-Path -Path $dscResourcesFolder -ChildPath $ResourceName } else { $resourceDirectory = $ScriptRoot } $localizedStringFileLocation = Join-Path -Path $resourceDirectory -ChildPath $PSUICulture if (-not (Test-Path -Path $localizedStringFileLocation)) { # Fallback to en-US $localizedStringFileLocation = Join-Path -Path $resourceDirectory -ChildPath 'en-US' } Import-LocalizedData ` -BindingVariable 'localizedData' ` -FileName "$ResourceName.strings.psd1" ` -BaseDirectory $localizedStringFileLocation return $localizedData } <# .SYNOPSIS Creates and throws an invalid argument exception. .PARAMETER Message The message explaining why this error is being thrown. .PARAMETER ArgumentName The name of the invalid argument that is causing this error to be thrown. #> function New-InvalidArgumentException { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Message, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ArgumentName ) $argumentException = New-Object -TypeName 'ArgumentException' ` -ArgumentList @($Message, $ArgumentName) $newObjectParameters = @{ TypeName = 'System.Management.Automation.ErrorRecord' ArgumentList = @($argumentException, $ArgumentName, 'InvalidArgument', $null) } $errorRecord = New-Object @newObjectParameters throw $errorRecord } <# .SYNOPSIS Creates and throws an invalid operation exception. .PARAMETER Message The message explaining why this error is being thrown. .PARAMETER ErrorRecord The error record containing the exception that is causing this terminating error. #> function New-InvalidOperationException { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Message, [Parameter()] [ValidateNotNull()] [System.Management.Automation.ErrorRecord] $ErrorRecord ) if ($null -eq $ErrorRecord) { $invalidOperationException = New-Object -TypeName 'InvalidOperationException' ` -ArgumentList @($Message) } else { $invalidOperationException = New-Object -TypeName 'InvalidOperationException' ` -ArgumentList @($Message, $ErrorRecord.Exception) } $newObjectParameters = @{ TypeName = 'System.Management.Automation.ErrorRecord' ArgumentList = @( $invalidOperationException.ToString(), 'MachineStateIncorrect', 'InvalidOperation', $null ) } $errorRecordToThrow = New-Object @newObjectParameters throw $errorRecordToThrow } <# .SYNOPSIS Creates and throws an object not found exception. .PARAMETER Message The message explaining why this error is being thrown. .PARAMETER ErrorRecord The error record containing the exception that is causing this terminating error. #> function New-ObjectNotFoundException { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Message, [Parameter()] [ValidateNotNull()] [System.Management.Automation.ErrorRecord] $ErrorRecord ) if ($null -eq $ErrorRecord) { $exception = New-Object -TypeName 'System.Exception' ` -ArgumentList @($Message) } else { $exception = New-Object -TypeName 'System.Exception' ` -ArgumentList @($Message, $ErrorRecord.Exception) } $newObjectParameters = @{ TypeName = 'System.Management.Automation.ErrorRecord' ArgumentList = @( $exception.ToString(), 'MachineStateIncorrect', 'ObjectNotFound', $null ) } $errorRecordToThrow = New-Object @newObjectParameters throw $errorRecordToThrow } <# .SYNOPSIS Creates and throws an invalid result exception. .PARAMETER Message The message explaining why this error is being thrown. .PARAMETER ErrorRecord The error record containing the exception that is causing this terminating error. #> function New-InvalidResultException { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Message, [Parameter()] [ValidateNotNull()] [System.Management.Automation.ErrorRecord] $ErrorRecord ) if ($null -eq $ErrorRecord) { $exception = New-Object -TypeName 'System.Exception' ` -ArgumentList @($Message) } else { $exception = New-Object -TypeName 'System.Exception' ` -ArgumentList @($Message, $ErrorRecord.Exception) } $newObjectParameters = @{ TypeName = 'System.Management.Automation.ErrorRecord' ArgumentList = @( $exception.ToString(), 'MachineStateIncorrect', 'InvalidResult', $null ) } $errorRecordToThrow = New-Object @newObjectParameters throw $errorRecordToThrow } function Test-DscParameterState { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Collections.Hashtable] $CurrentValues, [Parameter(Mandatory = $true)] [System.Object] $DesiredValues, [Parameter()] [System.Array] $ValuesToCheck ) $returnValue = $true if (($DesiredValues.GetType().Name -ne 'HashTable') ` -and ($DesiredValues.GetType().Name -ne 'CimInstance') ` -and ($DesiredValues.GetType().Name -ne 'PSBoundParametersDictionary')) { $errorMessage = $script:localizedData.PropertyTypeInvalidForDesiredValues -f $($DesiredValues.GetType().Name) New-InvalidArgumentException -ArgumentName 'DesiredValues' -Message $errorMessage } if (($DesiredValues.GetType().Name -eq 'CimInstance') -and ($null -eq $ValuesToCheck)) { $errorMessage = $script:localizedData.PropertyTypeInvalidForValuesToCheck New-InvalidArgumentException -ArgumentName 'ValuesToCheck' -Message $errorMessage } if (($null -eq $ValuesToCheck) -or ($ValuesToCheck.Count -lt 1)) { $keyList = $DesiredValues.Keys } else { $keyList = $ValuesToCheck } $keyList | ForEach-Object -Process { if (($_ -ne 'Verbose')) { if (($CurrentValues.ContainsKey($_) -eq $false) ` -or ($CurrentValues.$_ -ne $DesiredValues.$_) ` -or (($DesiredValues.GetType().Name -ne 'CimInstance' -and $DesiredValues.ContainsKey($_) -eq $true) -and ($null -ne $DesiredValues.$_ -and $DesiredValues.$_.GetType().IsArray))) { if ($DesiredValues.GetType().Name -eq 'HashTable' -or ` $DesiredValues.GetType().Name -eq 'PSBoundParametersDictionary') { $checkDesiredValue = $DesiredValues.ContainsKey($_) } else { # If DesiredValue is a CimInstance. $checkDesiredValue = $false if (([System.Boolean]($DesiredValues.PSObject.Properties.Name -contains $_)) -eq $true) { if ($null -ne $DesiredValues.$_) { $checkDesiredValue = $true } } } if ($checkDesiredValue) { $desiredType = $DesiredValues.$_.GetType() $fieldName = $_ if ($desiredType.IsArray -eq $true) { if (($CurrentValues.ContainsKey($fieldName) -eq $false) ` -or ($null -eq $CurrentValues.$fieldName)) { Write-Verbose -Message ($script:localizedData.PropertyValidationError -f $fieldName) -Verbose $returnValue = $false } else { $arrayCompare = Compare-Object -ReferenceObject $CurrentValues.$fieldName ` -DifferenceObject $DesiredValues.$fieldName if ($null -ne $arrayCompare) { Write-Verbose -Message ($script:localizedData.PropertiesDoesNotMatch -f $fieldName) -Verbose $arrayCompare | ForEach-Object -Process { Write-Verbose -Message ($script:localizedData.PropertyThatDoesNotMatch -f $_.InputObject, $_.SideIndicator) -Verbose } $returnValue = $false } } } else { switch ($desiredType.Name) { 'String' { if (-not [System.String]::IsNullOrEmpty($CurrentValues.$fieldName) -or ` -not [System.String]::IsNullOrEmpty($DesiredValues.$fieldName)) { Write-Verbose -Message ($script:localizedData.ValueOfTypeDoesNotMatch ` -f $desiredType.Name, $fieldName, $($CurrentValues.$fieldName), $($DesiredValues.$fieldName)) -Verbose $returnValue = $false } } 'Int32' { if (-not ($DesiredValues.$fieldName -eq 0) -or ` -not ($null -eq $CurrentValues.$fieldName)) { Write-Verbose -Message ($script:localizedData.ValueOfTypeDoesNotMatch ` -f $desiredType.Name, $fieldName, $($CurrentValues.$fieldName), $($DesiredValues.$fieldName)) -Verbose $returnValue = $false } } { $_ -eq 'Int16' -or $_ -eq 'UInt16' -or $_ -eq 'Single' } { if (-not ($DesiredValues.$fieldName -eq 0) -or ` -not ($null -eq $CurrentValues.$fieldName)) { Write-Verbose -Message ($script:localizedData.ValueOfTypeDoesNotMatch ` -f $desiredType.Name, $fieldName, $($CurrentValues.$fieldName), $($DesiredValues.$fieldName)) -Verbose $returnValue = $false } } 'Boolean' { if ($CurrentValues.$fieldName -ne $DesiredValues.$fieldName) { Write-Verbose -Message ($script:localizedData.ValueOfTypeDoesNotMatch ` -f $desiredType.Name, $fieldName, $($CurrentValues.$fieldName), $($DesiredValues.$fieldName)) -Verbose $returnValue = $false } } default { Write-Warning -Message ($script:localizedData.UnableToCompareProperty ` -f $fieldName, $desiredType.Name) $returnValue = $false } } } } } } } return $returnValue } <# .SYNOPSIS Starts a process with a timeout. .PARAMETER FilePath String containing the path to the executable to start. .PARAMETER ArgumentList The arguments that should be passed to the executable. .PARAMETER Timeout The timeout in seconds to wait for the process to finish. #> function Start-ProcessWithTimeout { param ( [Parameter(Mandatory = $true)] [System.String] $FilePath, [Parameter()] [System.String[]] $ArgumentList, [Parameter(Mandatory = $true)] [System.UInt32] $Timeout ) $startProcessParameters = @{ FilePath = $FilePath ArgumentList = $ArgumentList PassThru = $true NoNewWindow = $true ErrorAction = 'Stop' } $sqlSetupProcess = Start-Process @startProcessParameters Write-Verbose -Message ($script:localizedData.StartProcess -f $sqlSetupProcess.Id, $startProcessParameters.FilePath, $Timeout) -Verbose Wait-Process -InputObject $sqlSetupProcess -Timeout $Timeout -ErrorAction 'Stop' return $sqlSetupProcess.ExitCode } # Internal function to assert if the role specific module is installed or not function Assert-Module { [CmdletBinding()] param ( [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $ModuleName = 'ActiveDirectory', [Parameter()] [System.Management.Automation.SwitchParameter] $ImportModule ) if (-not (Get-Module -Name $ModuleName -ListAvailable)) { $errorId = '{0}_ModuleNotFound' -f $ModuleName $errorMessage = $script:localizedData.RoleNotFoundError -f $moduleName ThrowInvalidOperationError -ErrorId $errorId -ErrorMessage $errorMessage } if ($ImportModule) { Import-Module -Name $ModuleName } } #end function Assert-Module # Internal function to test whether computer is a member of a domain function Test-DomainMember { [CmdletBinding()] [OutputType([System.Boolean])] param ( ) $isDomainMember = [System.Boolean] (Get-CimInstance -ClassName Win32_ComputerSystem -Verbose:$false).PartOfDomain return $isDomainMember } # Internal function to get the domain name of the computer function Get-DomainName { [CmdletBinding()] [OutputType([System.String])] param ( ) $domainName = [System.String] (Get-CimInstance -ClassName Win32_ComputerSystem -Verbose:$false).Domain return $domainName } # function Get-DomainName # Internal function to build domain FQDN function Resolve-DomainFQDN { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [OutputType([System.String])] [System.String] $DomainName, [Parameter()] [AllowNull()] [System.String] $ParentDomainName ) $domainFQDN = $DomainName if ($ParentDomainName) { $domainFQDN = '{0}.{1}' -f $DomainName, $ParentDomainName } return $domainFQDN } # Internal function to get an Active Directory object's parent Distinguished Name function Get-ADObjectParentDN { <# Copyright (c) 2016 The University Of Vermont All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the University nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. http://www.uvm.edu/~gcd/code-license/ #> [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [System.String] $DN ) # https://www.uvm.edu/~gcd/2012/07/listing-parent-of-ad-object-in-powershell/ $distinguishedNameParts = $DN -split '(?<![\\]),' $distinguishedNameParts[1..$($distinguishedNameParts.Count - 1)] -join ',' } #end function GetADObjectParentDN # Internal function that validates the Members, MembersToInclude and MembersToExclude combination # is valid. If the combination is invalid, an InvalidArgumentError is raised. function Assert-MemberParameters { [CmdletBinding()] param ( [Parameter()] [ValidateNotNull()] [System.String[]] $Members, [Parameter()] [ValidateNotNull()] [System.String[]] $MembersToInclude, [Parameter()] [ValidateNotNull()] [System.String[]] $MembersToExclude, [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $ModuleName = 'xActiveDirectory' ) if ($PSBoundParameters.ContainsKey('Members')) { if ($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude')) { # If Members are provided, Include and Exclude are not allowed. $errorId = '{0}_MembersPlusIncludeOrExcludeConflict' -f $ModuleName $errorMessage = $script:localizedData.MembersAndIncludeExcludeError -f 'Members', 'MembersToInclude', 'MembersToExclude' ThrowInvalidArgumentError -ErrorId $errorId -ErrorMessage $errorMessage } if ($Members.Length -eq 0) { $errorId = '{0}_MembersIsNull' -f $ModuleName $errorMessage = $script:localizedData.MembersIsNullError -f 'Members', 'MembersToInclude', 'MembersToExclude' ThrowInvalidArgumentError -ErrorId $errorId -ErrorMessage $errorMessage } } if ($PSBoundParameters.ContainsKey('MembersToInclude')) { $MembersToInclude = [System.String[]] @(Remove-DuplicateMembers -Members $MembersToInclude) } if ($PSBoundParameters.ContainsKey('MembersToExclude')) { $MembersToExclude = [System.String[]] @(Remove-DuplicateMembers -Members $MembersToExclude) } if (($PSBoundParameters.ContainsKey('MembersToInclude')) -and ($PSBoundParameters.ContainsKey('MembersToExclude'))) { if (($MembersToInclude.Length -eq 0) -and ($MembersToExclude.Length -eq 0)) { $errorId = '{0}_EmptyIncludeAndExclude' -f $ModuleName $errorMessage = $script:localizedData.IncludeAndExcludeAreEmptyError -f 'MembersToInclude', 'MembersToExclude' ThrowInvalidArgumentError -ErrorId $errorId -ErrorMessage $errorMessage } # Both MembersToInclude and MembersToExlude were provided. Check if they have common principals. foreach ($member in $MembersToInclude) { if ($member -in $MembersToExclude) { $errorId = '{0}_IncludeAndExcludeConflict' -f $ModuleName $errorMessage = $script:localizedData.IncludeAndExcludeConflictError -f $member, 'MembersToInclude', 'MembersToExclude' ThrowInvalidArgumentError -ErrorId $errorId -ErrorMessage $errorMessage } } } } #end function Assert-MemberParameters # Internal function to remove duplicate strings (members) from a string array function Remove-DuplicateMembers { [CmdletBinding()] [OutputType([System.String[]])] param ( [Parameter()] [System.String[]] $Members ) Set-StrictMode -Version Latest $destIndex = 0 for ([int] $sourceIndex = 0 ; $sourceIndex -lt $Members.Count; $sourceIndex++) { $matchFound = $false for ([int] $matchIndex = 0; $matchIndex -lt $destIndex; $matchIndex++) { if ($Members[$sourceIndex] -eq $Members[$matchIndex]) { # A duplicate is found. Discard the duplicate. Write-Verbose -Message ($script:localizedData.RemovingDuplicateMember -f $Members[$sourceIndex]) $matchFound = $true continue } } if (!$matchFound) { $Members[$destIndex++] = $Members[$sourceIndex].ToLowerInvariant() } } # Create the output array. $destination = New-Object -TypeName 'System.String[]' -ArgumentList $destIndex # Copy only distinct elements from the original array to the destination array. [System.Array]::Copy($Members, $destination, $destIndex) return $destination } #end function RemoveDuplicateMembers # Internal function to test whether the existing array members match the defined explicit array # members, the included members are present and the exlcuded members are not present. function Test-Members { [CmdletBinding()] [OutputType([System.Boolean])] param ( # Existing array members [Parameter()] [AllowNull()] [System.String[]] $ExistingMembers, # Explicit array members [Parameter()] [AllowNull()] [System.String[]] $Members, # Compulsory array members [Parameter()] [AllowNull()] [System.String[]] $MembersToInclude, # Excluded array members [Parameter()] [AllowNull()] [System.String[]] $MembersToExclude ) if ($PSBoundParameters.ContainsKey('Members')) { if ($null -eq $Members -or (($Members.Count -eq 1) -and ($Members[0].Length -eq 0))) { $Members = @() } Write-Verbose ($script:localizedData.CheckingMembers -f 'Explicit') $Members = [System.String[]] @(Remove-DuplicateMembers -Members $Members) if ($ExistingMembers.Count -ne $Members.Count) { Write-Verbose -Message ($script:localizedData.MembershipCountMismatch -f $Members.Count, $ExistingMembers.Count) return $false } $isInDesiredState = $true foreach ($member in $Members) { if ($member -notin $ExistingMembers) { Write-Verbose -Message ($script:localizedData.MemberNotInDesiredState -f $member) $isInDesiredState = $false } } if (-not $isInDesiredState) { Write-Verbose -Message ($script:localizedData.MembershipNotDesiredState -f $member) return $false } } #end if $Members if ($PSBoundParameters.ContainsKey('MembersToInclude')) { if ($null -eq $MembersToInclude -or (($MembersToInclude.Count -eq 1) -and ($MembersToInclude[0].Length -eq 0))) { $MembersToInclude = @() } Write-Verbose -Message ($script:localizedData.CheckingMembers -f 'Included') $MembersToInclude = [System.String[]] @(Remove-DuplicateMembers -Members $MembersToInclude) $isInDesiredState = $true foreach ($member in $MembersToInclude) { if ($member -notin $ExistingMembers) { Write-Verbose -Message ($script:localizedData.MemberNotInDesiredState -f $member) $isInDesiredState = $false } } if (-not $isInDesiredState) { Write-Verbose -Message ($script:localizedData.MembershipNotDesiredState -f $member) return $false } } #end if $MembersToInclude if ($PSBoundParameters.ContainsKey('MembersToExclude')) { if ($null -eq $MembersToExclude -or (($MembersToExclude.Count -eq 1) -and ($MembersToExclude[0].Length -eq 0))) { $MembersToExclude = @() } Write-Verbose -Message ($script:localizedData.CheckingMembers -f 'Excluded') $MembersToExclude = [System.String[]] @(Remove-DuplicateMembers -Members $MembersToExclude) $isInDesiredState = $true foreach ($member in $MembersToExclude) { if ($member -in $ExistingMembers) { Write-Verbose -Message ($script:localizedData.MemberNotInDesiredState -f $member) $isInDesiredState = $false } } if (-not $isInDesiredState) { Write-Verbose -Message ($script:localizedData.MembershipNotDesiredState -f $member) return $false } } #end if $MembersToExclude Write-Verbose -Message $script:localizedData.MembershipInDesiredState return $true } #end function Test-Membership function ConvertTo-TimeSpan { [CmdletBinding()] [OutputType([System.TimeSpan])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.UInt32] $TimeSpan, [Parameter(Mandatory = $true)] [ValidateSet('Seconds', 'Minutes', 'Hours', 'Days')] [System.String] $TimeSpanType ) $newTimeSpanParams = @{ } switch ($TimeSpanType) { 'Seconds' { $newTimeSpanParams['Seconds'] = $TimeSpan } 'Minutes' { $newTimeSpanParams['Minutes'] = $TimeSpan } 'Hours' { $newTimeSpanParams['Hours'] = $TimeSpan } 'Days' { $newTimeSpanParams['Days'] = $TimeSpan } } return (New-TimeSpan @newTimeSpanParams) } #end function ConvertTo-TimeSpan <# .SYNOPSIS Converts a System.TimeSpan into the number of seconds, mintutes, hours or days. .PARAMETER TimeSpan TimeSpan to convert into an integer .PARAMETER TimeSpanType Convert timespan into the total number of seconds, minutes, hours or days. .EXAMPLE $Get-ADDefaultDomainPasswordPolicy ConvertFrom-TimeSpan #> function ConvertFrom-TimeSpan { [CmdletBinding()] [OutputType([System.Int32])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.TimeSpan] $TimeSpan, [Parameter(Mandatory = $true)] [ValidateSet('Seconds', 'Minutes', 'Hours', 'Days')] [System.String] $TimeSpanType ) switch ($TimeSpanType) { 'Seconds' { return $TimeSpan.TotalSeconds -as [System.UInt32] } 'Minutes' { return $TimeSpan.TotalMinutes -as [System.UInt32] } 'Hours' { return $TimeSpan.TotalHours -as [System.UInt32] } 'Days' { return $TimeSpan.TotalDays -as [System.UInt32] } } } #end function ConvertFrom-TimeSpan <# .SYNOPSIS Returns common AD cmdlet connection parameter for splatting .PARAMETER CommonName When specified, a CommonName overrides theUsed by the xADUser cmdletReturns the Identity as the Name key. For example, the Get-ADUser, Set-ADUser and Remove-ADUser cmdlets take an Identity parameter, but the New-ADUser cmdlet uses the Name parameter. .PARAMETER UseNameParameter Returns the Identity as the Name key. For example, the Get-ADUser, Set-ADUser and Remove-ADUser cmdlets take an Identity parameter, but the New-ADUser cmdlet uses the Name parameter. .EXAMPLE $getADUserParams = Get-CommonADParameters @PSBoundParameters Returns connection parameters suitable for Get-ADUser using the splatted cmdlet parameters. .EXAMPLE $newADUserParams = Get-CommonADParameters @PSBoundParameters -UseNameParameter Returns connection parameters suitable for New-ADUser using the splatted cmdlet parameters. #> function Get-ADCommonParameters { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [Alias('UserName', 'GroupName', 'ComputerName', 'ServiceAccountName')] [System.String] $Identity, [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $CommonName, [Parameter()] [ValidateNotNull()] [Alias('DomainAdministratorCredential')] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $Credential, [Parameter()] [ValidateNotNullOrEmpty()] [Alias('DomainController')] [System.String] $Server, [Parameter()] [System.Management.Automation.SwitchParameter] $UseNameParameter, [Parameter()] [System.Management.Automation.SwitchParameter] $PreferCommonName, # Catch all to enable splatted $PSBoundParameters [Parameter(ValueFromRemainingArguments)] $RemainingArguments ) if ($UseNameParameter) { if ($PreferCommonName -and ($PSBoundParameters.ContainsKey('CommonName'))) { $adConnectionParameters = @{ Name = $CommonName } } else { $adConnectionParameters = @{ Name = $Identity } } } else { if ($PreferCommonName -and ($PSBoundParameters.ContainsKey('CommonName'))) { $adConnectionParameters = @{ Identity = $CommonName } } else { $adConnectionParameters = @{ Identity = $Identity } } } if ($Credential) { $adConnectionParameters['Credential'] = $Credential } if ($Server) { $adConnectionParameters['Server'] = $Server } return $adConnectionParameters } #end function Get-ADCommonParameters function ThrowInvalidOperationError { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ErrorId, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ErrorMessage ) $exception = New-Object -TypeName 'System.InvalidOperationException' -ArgumentList $ErrorMessage $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation $errorRecord = New-Object -TypeName 'System.Management.Automation.ErrorRecord' -ArgumentList @($exception, $ErrorId, $errorCategory, $null) throw $errorRecord } function ThrowInvalidArgumentError { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ErrorId, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ErrorMessage ) $exception = New-Object -TypeName 'System.ArgumentException' -ArgumentList $ErrorMessage $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument $errorRecord = New-Object -TypeName 'System.Management.Automation.ErrorRecord' -ArgumentList @($exception, $ErrorId, $errorCategory, $null) throw $errorRecord } #end function ThrowInvalidArgumentError # Internal function to test site availability function Test-ADReplicationSite { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [System.String] $SiteName, [Parameter(Mandatory = $true)] [System.String] $DomainName, [Parameter()] [System.Management.Automation.PSCredential] $Credential ) Write-Verbose -Message ($script:localizedData.CheckingSite -f $SiteName) $existingDC = "$((Get-ADDomainController -Discover -DomainName $DomainName -ForceDiscover).HostName)" try { $site = Get-ADReplicationSite -Identity $SiteName -Server $existingDC -Credential $Credential } catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] { return $false } return ($null -ne $site) } function ConvertTo-DeploymentForestMode { [CmdletBinding()] [OutputType([Microsoft.DirectoryServices.Deployment.Types.ForestMode])] param ( [Parameter( Mandatory = $true, ParameterSetName = 'ById')] [System.UInt16] $ModeId, [Parameter( Mandatory = $true, ParameterSetName = 'ByName')] [AllowNull()] [System.Nullable``1[Microsoft.ActiveDirectory.Management.ADForestMode]] $Mode, [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $ModuleName = 'xActiveDirectory' ) $convertedMode = $null if ($PSCmdlet.ParameterSetName -eq 'ByName' -and $Mode) { $convertedMode = $Mode -as [Microsoft.DirectoryServices.Deployment.Types.ForestMode] } if ($PSCmdlet.ParameterSetName -eq 'ById') { $convertedMode = $ModeId -as [Microsoft.DirectoryServices.Deployment.Types.ForestMode] } if ([enum]::GetValues([Microsoft.DirectoryServices.Deployment.Types.ForestMode]) -notcontains $convertedMode) { return $null } return $convertedMode } function ConvertTo-DeploymentDomainMode { [CmdletBinding()] [OutputType([Microsoft.DirectoryServices.Deployment.Types.DomainMode])] param ( [Parameter( Mandatory = $true, ParameterSetName = 'ById')] [System.UInt16] $ModeId, [Parameter( Mandatory = $true, ParameterSetName = 'ByName')] [AllowNull()] [System.Nullable``1[Microsoft.ActiveDirectory.Management.ADDomainMode]] $Mode, [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $ModuleName = 'xActiveDirectory' ) $convertedMode = $null if ($PSCmdlet.ParameterSetName -eq 'ByName' -and $Mode) { $convertedMode = $Mode -as [Microsoft.DirectoryServices.Deployment.Types.DomainMode] } if ($PSCmdlet.ParameterSetName -eq 'ById') { $convertedMode = $ModeId -as [Microsoft.DirectoryServices.Deployment.Types.DomainMode] } if ([enum]::GetValues([Microsoft.DirectoryServices.Deployment.Types.DomainMode]) -notcontains $convertedMode) { return $null } return $convertedMode } function Restore-ADCommonObject { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [Alias('UserName', 'GroupName', 'ComputerName', 'ServiceAccountName')] [System.String] $Identity, [Parameter(Mandatory = $true)] [ValidateSet('Computer', 'OrganizationalUnit', 'User', 'Group')] [System.String] $ObjectClass, [Parameter()] [ValidateNotNull()] [Alias('DomainAdministratorCredential')] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $Credential, [Parameter()] [ValidateNotNullOrEmpty()] [Alias('DomainController')] [System.String] $Server ) $restoreFilter = 'msDS-LastKnownRDN -eq "{0}" -and objectClass -eq "{1}" -and isDeleted -eq $true' -f $Identity, $ObjectClass Write-Verbose -Message ($script:localizedData.FindInRecycleBin -f $restoreFilter) -Verbose <# Using IsDeleted and IncludeDeletedObjects will mean that the cmdlet does not throw any more, and simply returns $null instead #> $commonParams = Get-ADCommonParameters @PSBoundParameters $getAdObjectParams = $commonParams.Clone() $getAdObjectParams.Remove('Identity') $getAdObjectParams['Filter'] = $restoreFilter $getAdObjectParams['IncludeDeletedObjects'] = $true $getAdObjectParams['Properties'] = @('whenChanged') # If more than one object is returned, we pick the one that was changed last. $restorableObject = Get-ADObject @getAdObjectParams | Sort-Object -Descending -Property 'whenChanged' | Select-Object -First 1 $restoredObject = $null if ($restorableObject) { Write-Verbose -Message ($script:localizedData.FoundRestoreTargetInRecycleBin -f $Identity, $ObjectClass, $restorableObject.DistinguishedName) -Verbose try { $restoreParams = $commonParams.Clone() $restoreParams['PassThru'] = $true $restoreParams['ErrorAction'] = 'Stop' $restoreParams['Identity'] = $restorableObject.DistinguishedName $restoredObject = Restore-ADObject @restoreParams Write-Verbose -Message ($script:localizedData.RecycleBinRestoreSuccessful -f $Identity, $ObjectClass) -Verbose } catch [Microsoft.ActiveDirectory.Management.ADException] { # After Get-TargetResource is through, only one error can occur here: Object parent does not exist ThrowInvalidOperationError -ErrorId "$($Identity)_RecycleBinRestoreFailed" -ErrorMessage ($script:localizedData.RecycleBinRestoreFailed -f $Identity, $ObjectClass, $_.Exception.Message) } } return $restoredObject } <# .SYNOPSIS Author: Robert D. Biddle (https://github.com/RobBiddle) Created: December.20.2017 .DESCRIPTION Takes an Active Directory DistinguishedName as input, returns the domain FQDN .EXAMPLE Get-ADDomainNameFromDistinguishedName -DistinguishedName 'CN=ExampleObject,OU=ExampleOU,DC=example,DC=com' #> function Get-ADDomainNameFromDistinguishedName { [CmdletBinding()] param ( [Parameter()] [System.String] $DistinguishedName ) if ($DistinguishedName -notlike '*DC=*') { return } $splitDistinguishedName = ($DistinguishedName -split 'DC=') $splitDistinguishedNameParts = $splitDistinguishedName[1..$splitDistinguishedName.Length] $domainFqdn = "" foreach ($part in $splitDistinguishedNameParts) { $domainFqdn += "DC=$part" } $domainName = $domainFqdn -replace 'DC=', '' -replace ',', '.' return $domainName } #end function Get-ADDomainNameFromDistinguishedName <# .SYNOPSIS Add group member from current or different domain .NOTES Author original code: Robert D. Biddle (https://github.com/RobBiddle) Author refactored code: Jan-Hendrik Peters (https://github.com/nyanhp) #> function Add-ADCommonGroupMember { [CmdletBinding()] param ( [Parameter()] [System.String[]] $Members, [Parameter()] [hashtable] $Parameters, [Parameter()] [System.Management.Automation.SwitchParameter] $MembersInMultipleDomains ) Assert-Module -ModuleName ActiveDirectory if ($MembersInMultipleDomains.IsPresent) { foreach ($member in $Members) { $memberDomain = Get-ADDomainNameFromDistinguishedName -DistinguishedName $member if (-not $memberDomain) { ThrowInvalidArgumentError -ErrorId "$($member)_EmptyDomainError" -ErrorMessage ($script:localizedData.EmptyDomainError -f $member, $Parameters.GroupName) } Write-Verbose -Message ($script:localizedData.AddingGroupMember -f $member, $memberDomain, $Parameters.GroupName) $memberObjectClass = (Get-ADObject -Identity $member -Server $memberDomain -Properties ObjectClass).ObjectClass if ($memberObjectClass -eq 'computer') { $memberObject = Get-ADComputer -Identity $member -Server $memberDomain } elseif ($memberObjectClass -eq 'group') { $memberObject = Get-ADGroup -Identity $member -Server $memberDomain } elseif ($memberObjectClass -eq 'user') { $memberObject = Get-ADUser -Identity $member -Server $memberDomain } Add-ADGroupMember @Parameters -Members $memberObject } } else { Add-ADGroupMember @Parameters -Members $Members } } <# .SYNOPSIS Returns the domain controller object if the node is a domain controller, otherwise it return $null. .PARAMETER DomainName The name of the domain that should contain the domain controller. .PARAMETER ComputerName The name of the node to return the domain controller object for. Defaults to $env:COMPUTERNAME. .OUTPUTS If the domain controller is not found, an empty object ($null) is returned. .NOTES Throws an exception of Microsoft.ActiveDirectory.Management.ADServerDownException if the domain cannot be contacted. #> function Get-DomainControllerObject { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $DomainName, [Parameter()] [System.String] $ComputerName = $env:COMPUTERNAME, [Parameter()] [System.Management.Automation.PSCredential] $Credential ) <# It is not possible to use `-ErrorAction 'SilentlyContinue` on the cmdlet Get-ADDomainController, it will throw an error regardless. #> try { $getADDomainControllerParameters = @{ Filter = 'Name -eq "{0}"' -f $ComputerName Server = $DomainName } if ($PSBoundParameters.ContainsKey('Credential')) { $getADDomainControllerParameters['Credential'] = $Credential } $domainControllerObject = Get-ADDomainController @getADDomainControllerParameters if (-not $domainControllerObject -and (Test-IsDomainController) -eq $true) { $errorMessage = $script:localizedData.WasExpectingDomainController New-InvalidResultException -Message $errorMessage } } catch { $errorMessage = $script:localizedData.FailedEvaluatingDomainController New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ } return $domainControllerObject } <# .SYNOPSIS Returns $true if the node is a domain controller, otherwise it returns $false #> function Test-IsDomainController { [CmdletBinding()] param ( ) $operatingSystemInformation = Get-CimInstance -ClassName 'Win32_OperatingSystem' return $operatingSystemInformation.ProductType -eq 2 } <# .SYNOPSIS Converts a hashtable containing the parameter to property mappings to an array of properties that can be used to call cmdlets that supports the parameter Properties. .PARAMETER PropertyMap The property map, as an array of hashtables, to convert to a properties array. .EXAMPLE $computerObjectPropertyMap = @( @{ ParameterName = 'ComputerName' PropertyName = 'cn' }, @{ ParameterName = 'Location' } ) $computerObjectProperties = Convert-PropertyMapToObjectProperties $computerObjectPropertyMap $getADComputerResult = Get-ADComputer -Identity 'APP01' -Properties $computerObjectProperties #> function Convert-PropertyMapToObjectProperties { [CmdletBinding()] [OutputType([System.Array])] param ( [Parameter(Mandatory = $true)] [System.Array] $PropertyMap ) $objectProperties = @() # Create an array of the AD property names to retrieve from the property map foreach ($property in $PropertyMap) { if ($property -isnot [System.Collections.Hashtable]) { $errorMessage = $script:localizedData.PropertyMapArrayIsWrongType New-InvalidOperationException -Message $errorMessage } if ($property.ContainsKey('PropertyName')) { $objectProperties += @($property.PropertyName) } else { $objectProperties += $property.ParameterName } } return $objectProperties } <# .SYNOPSIS This function is used to compare current and desired values for any DSC resource, and return a hashtable with the result from the comparison. .PARAMETER CurrentValues The current values that should be compared to to desired values. Normally the values returned from Get-TargetResource. .PARAMETER DesiredValues The values set in the configuration and is provided in the call to the functions *-TargetResource, and that will be compared against current 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. #> function Compare-ResourcePropertyState { [CmdletBinding()] [OutputType([System.Collections.Hashtable[]])] param ( [Parameter(Mandatory = $true)] [System.Collections.Hashtable] $CurrentValues, [Parameter(Mandatory = $true)] [System.Collections.Hashtable] $DesiredValues, [Parameter()] [System.String[]] $Properties, [Parameter()] [System.String[]] $IgnoreProperties ) if ($PSBoundParameters.ContainsKey('Properties')) { # Filter out the parameters (keys) not specified in Properties $desiredValuesToRemove = $DesiredValues.Keys | Where-Object -FilterScript { $_ -notin $Properties } $desiredValuesToRemove | ForEach-Object -Process { $DesiredValues.Remove($_) } } else { <# Remove any common parameters that might be part of DesiredValues, if it $PSBoundParameters was used to pass the desired values. #> $commonParametersToRemove = $DesiredValues.Keys | Where-Object -FilterScript { $_ -in [System.Management.Automation.PSCmdlet]::CommonParameters ` -or $_ -in [System.Management.Automation.PSCmdlet]::OptionalCommonParameters } $commonParametersToRemove | ForEach-Object -Process { $DesiredValues.Remove($_) } } # Remove any properties that should be ignored. if ($PSBoundParameters.ContainsKey('IgnoreProperties')) { $IgnoreProperties | ForEach-Object -Process { if ($DesiredValues.ContainsKey($_)) { $DesiredValues.Remove($_) } } } $compareTargetResourceStateReturnValue = @() foreach ($parameterName in $DesiredValues.Keys) { Write-Verbose -Message ($script:localizedData.EvaluatePropertyState -f $parameterName) -Verbose $parameterState = @{ ParameterName = $parameterName Expected = $DesiredValues.$parameterName Actual = $CurrentValues.$parameterName } # Check if the parameter is in compliance. $isPropertyInDesiredState = Test-DscPropertyState -Values @{ CurrentValue = $CurrentValues.$parameterName DesiredValue = $DesiredValues.$parameterName } if ($isPropertyInDesiredState) { Write-Verbose -Message ($script:localizedData.PropertyInDesiredState -f $parameterName) -Verbose $parameterState['InDesiredState'] = $true } else { Write-Verbose -Message ($script:localizedData.PropertyNotInDesiredState -f $parameterName) -Verbose $parameterState['InDesiredState'] = $false } $compareTargetResourceStateReturnValue += $parameterState } return $compareTargetResourceStateReturnValue } <# .SYNOPSIS This function is used to compare the current and the desired value of a property. .PARAMETER Values This is set to a hash table with the current value (the CurrentValue key) and desired value (the DesiredValue key). .EXAMPLE Test-DscPropertyState -Values @{ CurrentValue = 'John' DesiredValue = 'Alice' } .EXAMPLE Test-DscPropertyState -Values @{ CurrentValue = 1 DesiredValue = 2 } #> function Test-DscPropertyState { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [System.Collections.Hashtable] $Values ) $returnValue = $true if ($Values.CurrentValue -ne $Values.DesiredValue -or $Values.DesiredValue.GetType().IsArray) { $desiredType = $Values.DesiredValue.GetType() if ($desiredType.IsArray -eq $true) { if ($Values.CurrentValue -and $Values.DesiredValue) { $compareObjectParameters = @{ ReferenceObject = $Values.CurrentValue DifferenceObject = $Values.DesiredValue } $arrayCompare = Compare-Object @compareObjectParameters if ($null -ne $arrayCompare) { Write-Verbose -Message $script:localizedData.ArrayDoesNotMatch -Verbose $arrayCompare | ForEach-Object -Process { Write-Verbose -Message ($script:localizedData.ArrayValueThatDoesNotMatch -f $_.InputObject, $_.SideIndicator) -Verbose } $returnValue = $false } } else { $returnValue = $false } } else { $returnValue = $false $supportedTypes = @( 'String' 'Int32' 'Int16' 'UInt16' 'Single' 'Boolean' ) if ($desiredType.Name -notin $supportedTypes) { Write-Warning -Message ($script:localizedData.UnableToCompareType ` -f $fieldName, $desiredType.Name) } else { Write-Verbose -Message ( $script:localizedData.PropertyValueOfTypeDoesNotMatch ` -f $desiredType.Name, $Values.CurrentValue, $Values.DesiredValue ) -Verbose } } } return $returnValue } <# .SYNOPSIS Asserts if the AD PS Drive has been created, and creates one if not. .PARAMETER Root Specifies the AD path to which the drive is mapped. .NOTES Throws an exception if the PS Drive cannot be created. #> function Assert-ADPSDrive { [CmdletBinding()] param ( [Parameter()] [System.String] $Root = '//RootDSE/' ) Assert-Module -ModuleName 'ActiveDirectory' -ImportModule $activeDirectoryPSDrive = Get-PSDrive -Name AD -ErrorAction SilentlyContinue if ($null -eq $activeDirectoryPSDrive) { Write-Verbose -Message $script:localizedData.CreatingNewADPSDrive try { New-PSDrive -Name AD -PSProvider 'ActiveDirectory' -Root $Root -Scope Script -ErrorAction 'Stop' | Out-Null } catch { $errorMessage = $script:localizedData.CreatingNewADPSDriveError New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ } } } <# .SYNOPSIS This is a wrapper for Set-ADComputer. .PARAMETER Parameters A hash table containing all parameters that will be passed trough to Set-ADComputer. .NOTES This is needed because of how Pester is unable to handle mocking the cmdlet Set-ADComputer. Therefor there are no unit test for this function. #> function Set-DscADComputer { param ( [Parameter(Mandatory = $true)] [System.Collections.Hashtable] $Parameters ) Set-ADComputer @Parameters | Out-Null } $script:localizedData = Get-LocalizedData -ResourceName 'xActiveDirectory.Common' -ScriptRoot $PSScriptRoot Export-ModuleMember -Function @( 'New-InvalidArgumentException' 'New-InvalidOperationException' 'New-ObjectNotFoundException' 'New-InvalidResultException' 'Get-LocalizedData' 'Test-DscParameterState' 'Start-ProcessWithTimeout' 'Assert-Module' 'Test-DomainMember' 'Get-DomainName' 'Resolve-DomainFQDN' 'Get-ADObjectParentDN' 'Assert-MemberParameters' 'Remove-DuplicateMembers' 'Test-Members' 'ConvertTo-TimeSpan' 'ConvertFrom-TimeSpan' 'Get-ADCommonParameters' 'ThrowInvalidOperationError' 'ThrowInvalidArgumentError' 'Test-ADReplicationSite' 'ConvertTo-DeploymentForestMode' 'ConvertTo-DeploymentDomainMode' 'Restore-ADCommonObject' 'Get-ADDomainNameFromDistinguishedName' 'Add-ADCommonGroupMember' 'Get-DomainControllerObject' 'Test-IsDomainController' 'Convert-PropertyMapToObjectProperties' 'Compare-ResourcePropertyState' 'Test-DscPropertyState' 'Assert-ADPSDrive' 'Set-DscADComputer' ) |