Modules/ExchangeDscHelper/ExchangeDscHelper.psm1
$script:DSCExchangeModuleName = 'DSCExchangeModule' $script:DSCExchangeModulePath = "$env:Temp\DSCExchangeModule" <# .SYNOPSIS Gets the existing Remote PowerShell session to Exchange, if it exists and is in an Opened state. #> function Get-ExistingRemoteExchangeSession { [CmdletBinding()] [OutputType([System.Management.Automation.Runspaces.PSSession])] param () $session = Get-PSSession -Name "DSCExchangeSession" -ErrorAction SilentlyContinue # Attempt to reuse the session if we found one if ($null -ne $session) { if ($session.State -eq 'Opened') { Write-Verbose -Message 'Reusing existing Remote PowerShell Session to Exchange' } else # Session is in an unexpected state. Remove it so we can rebuild it { # Remove-RemoteExchangeModule Remove-RemoteExchangeSession $session = $null } } return $session } #Removes any Remote Sessions that have been setup by us function Remove-RemoteExchangeSession { [CmdletBinding()] param () $session = Get-PSSession -Name "DSCExchangeSession" -ErrorAction SilentlyContinue if ($session -ne $null) { Write-Verbose "Removing existing remote Powershell sessions" Get-PSSession -Name "DSCExchangeSession" -ErrorAction SilentlyContinue | Remove-PSSession } } <# .SYNOPSIS Establishes an Exchange remote PowerShell session to the local server, and imports the session. Reuses the session if it already exists. .PARAMETER Credential The Credentials to use when creating a remote PowerShell session to Exchange. .PARAMETER CommandsToLoad A list of the cmdlets that should be imported in the remote PowerShell session. .PARAMETER SetupProcessName The name of the primary Exchange Setup process. If this process is detected by this function, an exception will be thrown. #> function Get-RemoteExchangeSession { [CmdletBinding()] param ( [Parameter()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, [Parameter()] [System.String[]] $CommandsToLoad, [Parameter()] [System.String] $SetupProcessName = 'ExSetup*' ) # Check if Exchange Setup is running. If so, we need to throw an exception, as a running Exchange DSC resource will block Exchange Setup from working properly. if (Test-ExchangeSetupRunning -SetupProcessName $SetupProcessName) { throw 'Exchange Setup is currently running. Preventing creation of new Remote PowerShell session to Exchange.' } #See if there is already an Exchange Session $session = Get-ExistingRemoteExchangeSession -Verbose:$VerbosePreference # See if the Exchange Module is already exported to $env:Temp $exportedModule = Test-Path -Path $script:DSCExchangeModulePath # If the exported Exchange module does not exist, create a session and export it if ($exportedModule -eq $false) { $session = New-RemoteExchangeSession -Credential $Credential -Verbose:$VerbosePreference # If the session is still null here, things went wrong. Throw exception if ($null -eq $session) { throw 'Failed to establish remote PowerShell session to local server.' } else # Import the session globally { if ($CommandsToLoad) { $PSDefaultParameterValues = @{ 'Import-RemoteExchangeModule:CommandsToLoad' = $CommandsToLoad } } Import-RemoteExchangeModule -Session $session -Verbose:$VerbosePreference | Out-Null } } else { Write-Verbose -Message 'Importing the ExchangeDsc Remote PowerShell Module.' if ($null -eq $session) { $session = New-RemoteExchangeSession -Credential $Credential -Verbose:$VerbosePreference } if ($CommandsToLoad) { $PSDefaultParameterValues = @{ 'Import-Module:Function' = $CommandsToLoad } } Import-Module -Name $script:DSCExchangeModulePath\$script:DSCExchangeModuleName.psm1 -ArgumentList $session -Global -DisableNameChecking -Force } } <# .SYNOPSIS Creates a new Exchange remote PowerShell session to the local server. .PARAMETER Credential The Credentials to use when creating a remote PowerShell session to Exchange. #> function New-RemoteExchangeSession { [CmdletBinding()] [OutputType([System.Management.Automation.Runspaces.PSSession])] param ( [Parameter()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential ) # First make sure we are on a valid server version, and that Exchange is fully installed if (!(Test-ExchangeSetupComplete -Verbose:$VerbosePreference)) { throw 'A supported version of Exchange is either not present, or not fully installed on this machine.' } Write-Verbose -Message 'Creating new Remote PowerShell session to Exchange' # Get local server FQDN $machineDomain = (Get-CimInstance -ClassName Win32_ComputerSystem).Domain.ToLower() $serverName = $env:computername.ToLower() $serverFQDN = $serverName + '.' + $machineDomain # Override chatty banner, because chatty New-Alias Get-ExBanner Out-Null New-Alias Get-Tip Out-Null # Load built in Exchange functions, and create session $exbin = Join-Path -Path ((Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup).MsiInstallPath) -ChildPath 'bin' $remoteExchange = Join-Path -Path "$exbin" -ChildPath 'RemoteExchange.ps1' # Setup commands to run while the RemoteExchange.ps1 script is in scope $commandToExecuteAfterDotSourcing = @('_NewExchangeRunspace') $commandParamsToExecuteAfterDotSourcing = @{ '_NewExchangeRunspace' = @{ fqdn = $serverFQDN credential = $Credential UseWIA = $false AllowRedirection = $false } } $returnValues = Invoke-DotSourcedScript ` -ScriptPath $remoteExchange ` -CommandsToExecuteInScope $commandToExecuteAfterDotSourcing ` -ParamsForCommandsToExecuteInScope $commandParamsToExecuteAfterDotSourcing ` -Verbose:$VerbosePreference if ($null -ne $returnValues -and $returnValues.ContainsKey('_NewExchangeRunspace')) { $session = $returnValues['_NewExchangeRunspace'] } # Remove the aliases we created earlier Remove-Item Alias:Get-ExBanner Remove-Item Alias:Get-Tip if ($null -ne $session) { $session.Name = 'DSCExchangeSession' } return $session } <# .SYNOPSIS Imports an established remote PowerShell session to Exchange. .PARAMETER Session The remote PowerShell session to import. .PARAMETER CommandsToLoad A list of the cmdlets that should be imported in the remote PowerShell session. #> function Import-RemoteExchangeModule { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Object] $Session, [Parameter()] [System.String[]] $CommandsToLoad = @('*') ) New-Item -Path $script:DSCExchangeModulePath -Type Directory -Force | Out-Null Export-PSSession -Session $Session -OutputModule $script:DSCExchangeModulePath -Force Import-Module -Name $script:DSCExchangeModulePath -Global -DisableNameChecking -Function $CommandsToLoad } <# .SYNOPSIS Removes any Remote Exchange PowerShell Sessions that have been setup by ExchangeDsc. #> function Remove-RemoteExchangeModule { [CmdletBinding()] param () Remove-Module -Name $script:DSCExchangeModuleName -Force -ErrorAction SilentlyContinue } <# .SYNOPSIS Checks whether a supported version of Exchange is at least partially installed. #> function Test-ExchangePresent { [CmdletBinding()] [OutputType([System.Boolean])] param () $version = Get-ExchangeVersionYear if ($version -in '2013', '2016', '2019') { return $true } else { return $false } } <# .SYNOPSIS Gets the installed Exchange Version, and returns the number as a string. Returns Null if the version cannot be found, and will optionally throw an exception if ThrowIfUnknownVersion was set to $true. .PARAMETER ThrowIfUnknownVersion Whether the function should throw an exception if the version cannot be found. Defauls to $false. #> function Get-ExchangeVersionYear { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter()] [System.Boolean] $ThrowIfUnknownVersion = $false ) $version = $null $installedVersionDetails = Get-DetailedInstalledVersion if ($null -ne $installedVersionDetails) { switch ($installedVersionDetails.Major) { 15 { switch ($installedVersionDetails.Minor) { 0 { $version = '2013' } 1 { $version = '2016' } 2 { $version = '2019' } } } } } if ($null -eq $version -and $ThrowIfUnknownVersion) { throw 'Failed to discover a known Exchange Version' } return $version } <# .SYNOPSIS Function to read installed Exchange's Uninstall information from registry. The function returns with the Uninstall registry key object. #> function Get-ExchangeUninstallKey { [CmdletBinding()] [OutputType([Microsoft.Win32.RegistryKey])] param () # First try to get the Exchange 2016 / 2019 uninstall key. $uninstallKey = Get-Item -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{CD981244-E9B8-405A-9026-6AEB9DCEF1F1}' -ErrorAction SilentlyContinue # If the first key attempt is NULL, this may be a 2013 server. Try the 2013 key. if ($null -eq $uninstallKey) { $uninstallKey = Get-Item -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{4934D1EA-BE46-48B1-8847-F1AF20E892C1}' -ErrorAction SilentlyContinue } return $uninstallKey } <# .SYNOPSIS Gets installed Exchange's buildnumber, which refers to the installed updates, and returns a hashtable with Major, Minor, Update versions. Returns NULL if the version cannot be found, and will optionally throw an exception if ThrowIfUnknownVersion was set to $true. #> function Get-DetailedInstalledVersion { [CmdletBinding()] [OutputType([System.Management.Automation.PSCustomObject])] param () $installedVersionDetails = $null $uninstallKey = Get-ExchangeUninstallKey if ($null -ne $uninstallKey) { $uninstallKeyPath = $uninstallKey.Name.ToLower().Replace('hkey_local_machine', 'hklm:') $displayVersion = Get-ItemProperty -Path $uninstallKeyPath -Name 'DisplayVersion' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty DisplayVersion $versionBuild = $null if ($displayVersion -match '(?<VersionMajor>\d+).(?<VersionMinor>\d+).(?<VersionBuild>\d+).(?<VersionRevision>\d+)') { $versionBuild = $Matches['VersionBuild'] $versionRevision = $Matches['VersionRevision'] } $versionMajor = Get-ItemProperty -Path $uninstallKeyPath -Name 'VersionMajor' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty VersionMajor $versionMinor = Get-ItemProperty -Path $uninstallKeyPath -Name 'VersionMinor' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty VersionMinor $installedVersionDetails = [System.Version]::new($versionMajor, $versionMinor, $versionBuild, $versionRevision) } return $installedVersionDetails } <# .SYNOPSIS Returns whether Exchange Setup has fully and successfully completed. #> function Test-ExchangeSetupComplete { [CmdletBinding()] [OutputType([System.Boolean])] param () $exchangePresent = Test-ExchangePresent $setupPartiallyCompleted = Test-ExchangeSetupPartiallyCompleted -Verbose:$VerbosePreference if ($exchangePresent -eq $true -and $setupPartiallyCompleted -eq $false) { Write-Verbose -Message 'Exchange is present and setup is detected as being fully complete.' $isSetupComplete = $true } else { Write-Verbose -Message "Exchange setup detected as not being fully complete. Exchange Present: $exchangePresent. Setup Partially Complete: $setupPartiallyCompleted." $isSetupComplete = $false } return $isSetupComplete } <# .SYNOPSIS Checks whether any Setup watermark keys exist which means that a previous installation of setup had already started but not completed. #> function Test-ExchangeSetupPartiallyCompleted { [CmdletBinding()] [OutputType([System.Boolean])] param () Write-Verbose -Message 'Checking if setup is partially complete' $isPartiallyCompleted = $false # Now check if setup actually completed successfully [System.String[]] $roleKeys = @( 'CafeRole', 'ClientAccessRole', 'FrontendTransportRole', 'HubTransportRole', 'MailboxRole', 'UnifiedMessagingRole' ) foreach ($key in $roleKeys) { $values = $null $values = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\$key" -ErrorAction SilentlyContinue if ($null -ne $values) { Write-Verbose -Message "Checking values at key 'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\$key'" if ($null -ne $values.UnpackedVersion) { # If ConfiguredVersion is missing, or Action or Watermark or present, setup needs to be resumed if ($null -eq $values.ConfiguredVersion) { Write-Warning -Message "Registry value missing. Setup considered partially complete. Location: 'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\$key\ConfiguredVersion'." $isPartiallyCompleted = $true } if ($null -ne $values.Action) { Write-Warning -Message "Registry value present. Setup considered partially complete. Location: 'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\$key\Action'. Value: '$($values.Action)'." $isPartiallyCompleted = $true } if ($null -ne $values.Watermark) { Write-Warning -Message "Registry value present. Setup considered partially complete. Location: 'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\$key\Watermark'. Value: '$($values.Watermark)'." $isPartiallyCompleted = $true } } } } return $isPartiallyCompleted } <# .SYNOPSIS Gets Exchange's setup.exe file's version info. It will return a System.Version object or NULL if not readable. .PARAMETER Path The path of the setup.exe which is used within the Exchnstall DSC resource. #> function Get-SetupExeVersion { [CmdletBinding()] [OutputType([System.Management.Automation.PSCustomObject])] param ( [Parameter(Mandatory = $true)] [System.String] $Path ) $version = $null # Get Exchange setup.exe version if (Test-Path -Path $Path -ErrorAction SilentlyContinue) { $setupexeVersionInfo = (Get-ChildItem -Path $Path).VersionInfo $version = [System.Version]::new($setupexeVersionInfo.ProductMajorPart, $setupexeVersionInfo.ProductMinorPart, $setupexeVersionInfo.ProductBuildPart) } return $version } <# .SYNOPSIS Checks if installed Exchange version is older than the version of the setup.exe, which is used within the Exchnstall DSC Resource call. Will return Boolean. .PARAMETER Path The path of the setup.exe which is used within the Exchnstall DSC resource. .PARAMETER Arguments The commandline arguments of setup.exe. #> function Test-ShouldUpgradeExchange { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [System.String] $Path, [Parameter(Mandatory = $true)] [System.String] $Arguments ) $shouldUpgrade = $false if (($Arguments -notmatch '/mode:upgrade') -and ($Arguments -notmatch '/m:upgrade')) { return $shouldUpgrade } Write-Verbose -Message "Comparing setup.exe version and installed Exchange's version." $setupExeVersion = Get-SetupExeVersion -Path $Path if ($null -ne $setupExeVersion` -and $null -ne $setupExeVersion.Major` -and $null -ne $setupExeVersion.Minor` -and $null -ne $setupExeVersion.Build) { Write-Verbose -Message "Setup.exe version is: '$('Major: {0}, Minor: {1}, Build: {2}' -f $setupExeVersion.Major, $setupexeVersion.Minor, $setupexeVersion.Build)'" $exchangeDisplayVersion = Get-DetailedInstalledVersion if ($null -ne $exchangeDisplayVersion` -and $null -ne $exchangeDisplayVersion.Major` -and $null -ne $exchangeDisplayVersion.Minor` -and $null -ne $exchangeDisplayVersion.Build) { # If we have an exchange installed Write-Verbose -Message "Exchange version is: '$('Major: {0}, Minor: {1}, Build: {2}' -f $exchangeDisplayVersion.Major, $exchangeDisplayVersion.Minor, $exchangeDisplayVersion.Build)'" if (($exchangeDisplayVersion.Major -eq $setupExeVersion.Major)` -and ($exchangeDisplayVersion.Minor -eq $setupExeVersion.Minor)` -and ($exchangeDisplayVersion.Build -lt $setupExeVersion.Build) ) { # If server has lower version of CU installed Write-Verbose -Message 'Version upgrade is requested.' # Executing with the upgrade. $shouldUpgrade = $true } else { Write-Verbose -Message 'Exchange update is not possible. Version of installed Exchange cannot be updated with the version of setup.exe.' } } else { Write-Error -Message "Get-ExchangeInstallStatus: Script cannot determine installed Exchange's version. Please check if Exchange is installed." } } else { Write-Error -Message "Get-ExchangeInstallStatus: Script cannot determine setup.exe version. Please check the file '$Path'." } return $shouldUpgrade } <# .SYNOPSIS Checks for the exact status of Exchange setup and returns the results in a Hashtable. .PARAMETER Path The path of the setup.exe which is used within the Exchnstall DSC resource. .PARAMETER Arguments The command line arguments to be passed to Exchange Setup. #> function Get-ExchangeInstallStatus { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter()] [System.String] $Path, [Parameter()] [System.String] $Arguments ) Write-Verbose -Message 'Checking Exchange Install Status' $shouldStartInstall = $false $shouldInstallLanguagePack = Test-ShouldInstallUMLanguagePack -Arguments $Arguments $setupRunning = Test-ExchangeSetupRunning $setupComplete = Test-ExchangeSetupComplete -Verbose:$VerbosePreference $exchangePresent = Test-ExchangePresent # Exchange CU install / update support $shouldUpgrade = Test-ShouldUpgradeExchange -Path $Path -Arguments $Arguments -Verbose:$VerbosePreference if ($setupRunning -or $setupComplete) { if (($shouldInstallLanguagePack -or $shouldUpgrade) -and $setupComplete) { $shouldStartInstall = $true } else { # Do nothing. Either Install is already running, or it's already finished successfully } } elseif (!$setupComplete) { $shouldStartInstall = $true } Write-Verbose -Message "Finished Checking Exchange Install Status. ShouldInstallLanguagePack: $shouldInstallLanguagePack. SetupRunning: $setupRunning. SetupComplete: $setupComplete. ExchangePresent: $exchangePresent. ShouldStartInstall: $shouldStartInstall." $returnValue = @{ ShouldInstallLanguagePack = $shouldInstallLanguagePack SetupRunning = $setupRunning SetupComplete = $setupComplete ExchangePresent = $exchangePresent ShouldUpgrade = $shouldUpgrade ShouldStartInstall = $shouldStartInstall } $returnValue } <# .SYNOPSIS Check for missing registry keys that may cause Exchange setup to try to restart WinRM mid setup , which will in turn cause the DSC resource to fail. If any required keys are missing, configure WinRM, then force a reboot. #> function Set-WSManConfigStatus { [CmdletBinding()] [OutputType([System.Boolean])] param () $needReboot = $false $wsmanKey = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN' -ErrorAction SilentlyContinue if ($null -ne $wsmanKey) { if ($null -eq $wsmanKey.UpdatedConfig) { $needReboot = $true Write-Verbose -Message "Value 'UpdatedConfig' missing from registry key HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN. Running: winrm i restore winrm/config" Set-Location "$($env:windir)\System32\inetsrv" winrm i restore winrm/config | Out-Null Write-Verbose -Message 'Machine needs to be rebooted before Exchange setup can proceed' Set-DSCMachineStatus -NewDSCMachineStatus 1 } } else { throw 'Unable to find registry key: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN' } return $needReboot } <# .SYNOPSIS Given the specified Exchange Setup arguments, determines whether an Exchange UM Language Pack should be installed or not. .PARAMETER Arguments The command line arguments to be passed to Exchange Setup. #> function Test-ShouldInstallUMLanguagePack { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] [System.String] $Arguments ) if ($Arguments -match '(?<=/AddUMLanguagePack:)(([a-z]{2}-[A-Z]{2},?)+)(?=\s)') { $Cultures = $Matches[0] Write-Verbose -Message "AddUMLanguagePack parameters detected: $Cultures" $Cultures = $Cultures -split ',' foreach ($Culture in $Cultures) { if ((Test-UMLanguagePackInstalled -Culture $Culture) -eq $false) { Write-Verbose -Message "UM Language Pack: $Culture is not installed" return $true } } } return $false } <# .SYNOPSIS Checks whether Exchange Setup is running by checking if the ExSetup.exe process currently exists as a running process. .PARAMETER SetupProcessName The name of the process to check if running. Defaults to ExSetup*. #> function Test-ExchangeSetupRunning { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] [System.String] $SetupProcessName = 'ExSetup*' ) return ($null -ne (Get-Process -Name $SetupProcessName -ErrorAction SilentlyContinue)) } <# .SYNOPSIS Checks if two strings are equal, or are both either null or empty. If IgnoreCase is specified, returns true if both strings are like each other, regardless of case. Without IgnoreCase, only returns true if both strings are identical. Also returns true if both strings are either null or empty. Returns false for all other cases. .PARAMETER String1 The first System.String object to compare. .PARAMETER String2 The second System.String object to compare .PARAMETER IgnoreCase Whether case should be ignored when comparing the two strings. #> function Compare-StringToString { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] [System.String] $String1, [Parameter()] [System.String] $String2, [Parameter()] [System.Management.Automation.SwitchParameter] $IgnoreCase ) if (([System.String]::IsNullOrEmpty($String1) -and [System.String]::IsNullOrEmpty($String2))) { return $true } else { if ($IgnoreCase -eq $true) { return ($String1 -like $String2) } else { return ($String1 -clike $String2) } } } <# .SYNOPSIS Compares two Nullable Boolean objects and returns true if they are both set to true, both set to false, or both set to either null or false. .PARAMETER Bool1 The first System.Boolean object to compare. .PARAMETER Bool2 The second System.Boolean object to compare. #> function Compare-BoolToBool { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] [Nullable[System.Boolean]] $Bool1, [Parameter()] [Nullable[System.Boolean]] $Bool2 ) if ($Bool1 -ne $Bool2) { if (!(($null -eq $Bool1 -and $Bool2 -eq $false) -or ($null -eq $Bool2 -and $Bool1 -eq $false))) { return $false } } return $true } <# .SYNOPSIS Takes a string which should be in timespan format, and compares it to an actual EnhancedTimeSpan object. Returns true if they are equal. .PARAMETER TimeSpan The Microsoft.Exchange.Data.EnhancedTimeSpan object to compare. .PARAMETER String The System.String object to compare. #> function Compare-TimespanToString { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] [Microsoft.Exchange.Data.EnhancedTimeSpan] $TimeSpan, [Parameter()] [System.String] $String ) try { $converted = [Microsoft.Exchange.Data.EnhancedTimeSpan]::Parse($String) return ($TimeSpan.Equals($converted)) } catch { throw "String '$String' is not in a valid format for an EnhancedTimeSpan" } return $false } <# .SYNOPSIS Takes a string which should be in ByteQuantifiedSize format, and compares it to an actual ByteQuantifiedSize object. Returns true if they are equal. .PARAMETER ByteQuantifiedSize The Microsoft.Exchange.Data.ByteQuantifiedSize object to compare. .PARAMETER String The System.String object to compare. #> function Compare-ByteQuantifiedSizeToString { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] [Microsoft.Exchange.Data.ByteQuantifiedSize] $ByteQuantifiedSize, [Parameter()] [System.String] $String ) try { $converted = [Microsoft.Exchange.Data.ByteQuantifiedSize]::Parse($String) return ($ByteQuantifiedSize.Equals($converted)) } catch { throw "String '$String' is not in a valid format for a ByteQuantifiedSize" } } <# .SYNOPSIS Takes a string which should be in Microsoft.Exchange.Data.Unlimited format, and compares with an actual Unlimited object. Returns true if they are equal. .PARAMETER Unlimited The Microsoft.Exchange.Data.Unlimited object to compare. .PARAMETER String The System.String object to compare. #> function Compare-UnlimitedToString { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] [System.Object] $Unlimited, [Parameter()] [System.String] $String ) if ($Unlimited.IsUnlimited) { return (Compare-StringToString -String1 'Unlimited' -String2 $String -IgnoreCase) } elseif ((Compare-StringToString -String1 'Unlimited' -String2 $String -IgnoreCase) -and !$Unlimited.IsUnlimited) { return $false } elseif (($Unlimited.Value -is [System.Int32]) -and !$Unlimited.IsUnlimited) { return (Compare-StringToString -String1 $Unlimited.Value.ToString() -String2 $String -IgnoreCase) } else { return (Compare-ByteQuantifiedSizeToString -ByteQuantifiedSize $Unlimited -String $String) } } <# .SYNOPSIS Takes an ADObjectId, gets a recipient from it using Get-Recipient, and checks if the EmailAddresses property contains the given AddressString. The Get-Recipient cmdlet must be loaded for this function to succeed. .PARAMETER ADObjectId The ADObjectID to run Get-Recipient against and compare against the given AddressString. .PARAMETER AddressString The AddressString to compare against the EmailAddresses property of the Get-Recipient results. #> function Compare-ADObjectIdToSmtpAddressString { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] [System.Object] $ADObjectId, [Parameter()] [System.String] $AddressString ) if ($null -ne (Get-Command -Name 'Get-Recipient' -ErrorAction SilentlyContinue)) { if ($null -eq $ADObjectId -and ![System.String]::IsNullOrEmpty($AddressString)) { return $false } elseif ($null -ne $ADObjectId -and [System.String]::IsNullOrEmpty($AddressString)) { return $false } elseif ($null -eq $ADObjectId -and [System.String]::IsNullOrEmpty($AddressString)) { return $true } $recipient = Get-Recipient -Identity $ADObjectId.DistinguishedName -ErrorAction SilentlyContinue if ($null -eq $recipient) { throw "Failed to Get-Recipient for ADObjectID with distinguishedName: $($ADObjectId.DistinguishedName)" } return ($null -ne ($recipient.EmailAddresses | Where-Object { $_.AddressString -like $AddressString })) } else { throw 'Compare-ADObjectIdToSmtpAddressString requires the Get-Recipient cmdlet. Make sure this is in the RBAC scope of the executing user account.' } } <# .SYNOPSIS Takes a string containing a given separator, and breaks it into a string array. .PARAMETER StringIn The System.String object to split into an array. .PARAMETER Separator The System.Char object to use as a separater when splitting the given System.String object. #> function Convert-StringToArray { [CmdletBinding()] [OutputType([System.String[]])] param ( [Parameter()] [System.String] $StringIn, [Parameter()] [System.Char] $Separator ) [System.String[]] $array = $StringIn.Split($Separator) for ($i = 0; $i -lt $array.Length; $i++) { $array[$i] = $array[$i].Trim() } return $array } <# .SYNOPSIS Takes an array of strings and converts each element in the array to all lowercase characters. .PARAMETER Array The array of System.String objects to convert into lowercase strings. #> function Convert-StringArrayToLowerCase { [CmdletBinding()] [OutputType([System.String[]])] param ( [Parameter()] [System.String[]] $Array ) [System.String[]] $arrayOut = New-Object -TypeName 'System.String[]' -ArgumentList $Array.Count for ($i = 0; $i -lt $Array.Count; $i++) { $arrayOut[$i] = $Array[$i].ToLower() } return $arrayOut } <# .SYNOPSIS Returns whether two string arrays have the same contents, where element order doesn't matter. .PARAMETER Array1 The first System.String[] object to compare. .PARAMETER Array2 The second System.String[] object to compare. .PARAMETER IgnoreCase Specifies that case should be ignored when comparing array contents. #> function Compare-ArrayContent { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] [System.String[]] $Array1, [Parameter()] [System.String[]] $Array2, [Parameter()] [System.Management.Automation.SwitchParameter] $IgnoreCase ) $hasSameContents = $true if ($Array1.Length -ne $Array2.Length) { $hasSameContents = $false } elseif ($Array1.Count -gt 0 -and $Array2.Count -gt 0) { if ($IgnoreCase -eq $true) { $Array1 = Convert-StringArrayToLowerCase -Array $Array1 $Array2 = Convert-StringArrayToLowerCase -Array $Array2 } foreach ($str in $Array1) { if (!($Array2.Contains($str))) { $hasSameContents = $false break } } } return $hasSameContents } <# .SYNOPSIS Given two System.String[] objects, Array 1 and Array 2, returns whether Array2 contains all elements of Array1, in any order. Array2 may be larger than Array1, as long as it contains all elements of Array1. .PARAMETER Array1 The System.String[] object to check whether its elements exist in Array2. .PARAMETER Array2 The System.String[] object to check whether the elements of Array1 are a part of. .PARAMETER IgnoreCase Whether case should be ignored when comparing strings from each array. #> function Test-ArrayElementsInSecondArray { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] [System.String[]] $Array1, [Parameter()] [System.String[]] $Array2, [Parameter()] [System.Management.Automation.SwitchParameter] $IgnoreCase ) $hasContents = $true if ($Array1.Count -eq 0) # Do nothing, as Array2 at a minimum contains nothing { } elseif ($Array2.Count -eq 0) # Array2 is empty and Array1 is not. Return false { $hasContents = $false } elseif ($Array1.Count -gt $Array2.Count) # Array1 has more elements than Array2, so Array2 can't contain Array1 { $hasContents = $false } else { if ($IgnoreCase -eq $true) { $Array1 = Convert-StringArrayToLowerCase -Array $Array1 $Array2 = Convert-StringArrayToLowerCase -Array $Array2 } foreach ($str in $Array1) { if (!($Array2.Contains($str))) { $hasContents = $false break } } } return $hasContents } <# .SYNOPSIS Takes $PSBoundParameters from another function and adds in the keys and values from the given Hashtable. .PARAMETER PSBoundParametersIn The $PSBoundParameters Hashtable from the calling function. .PARAMETER ParamsToAdd A Hashtable containing new Key/Value pairs to add to the given PSBoundParametersIn Hashtable. #> function Add-ToPSBoundParametersFromHashtable { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $PSBoundParametersIn, [Parameter()] [System.Collections.Hashtable] $ParamsToAdd ) foreach ($key in $ParamsToAdd.Keys) { if (!($PSBoundParametersIn.ContainsKey($key))) # Key doesn't exist, so add it with value { $PSBoundParametersIn.Add($key, $ParamsToAdd[$key]) | Out-Null } else # Key already exists, so just replace the value { $PSBoundParametersIn[$key] = $ParamsToAdd[$key] } } } <# .SYNOPSIS Takes $PSBoundParameters from another function, and modifies it based on the contents of the ParamsToRemove or ParamsToKeep parameters. If ParamsToRemove is specified, it will remove each param. If ParamsToKeep is specified, everything but those params will be removed. If both ParamsToRemove and ParamsToKeep are specified, the function will throw an exception. .PARAMETER PSBoundParametersIn The $PSBoundParameters Hashtable from the calling function. .PARAMETER ParamsToKeep A String array containing the list of parameter names to keep in the given PSBoundParametersIn HashTable. .PARAMETER ParamsToRemove A String array containing the list of parameter names to remove in the given PSBoundParametersIn HashTable. #> function Remove-FromPSBoundParametersUsingHashtable { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $PSBoundParametersIn, [Parameter()] [System.String[]] $ParamsToKeep, [Parameter()] [System.String[]] $ParamsToRemove ) if ($ParamsToKeep.Count -gt 0 -and $ParamsToRemove.Count -gt 0) { throw 'Remove-FromPSBoundParametersUsingHashtable does not support using both ParamsToKeep and ParamsToRemove' } if ($ParamsToKeep.Count -gt 0) { $ParamsToKeep = $ParamsToKeep.ToLower() $lowerParamsToKeep = Convert-StringArrayToLowerCase -Array $ParamsToKeep foreach ($key in $PSBoundParametersIn.Keys) { if (!($lowerParamsToKeep.Contains($key.ToLower()))) { $ParamsToRemove += $key } } } if ($ParamsToRemove.Count -gt 0) { foreach ($param in $ParamsToRemove) { $PSBoundParametersIn.Remove($param) | Out-Null } } } <# .SYNOPSIS Inspects the input $PSBoundParametersIn hashtable, and removes any parameters that do not work with the version of Exchange on this server. .PARAMETER PSBoundParametersIn The $PSBoundParameters hashtable from the calling function. .PARAMETER ParamName The parameter to check for and remove if not applicable to this server version. .PARAMETER ResourceName The name of the DSC resource from which parameters are being checked. .PARAMETER ParamExistsInVersion The parameter to check for and remove if not applicable to this server version. #> function Remove-NotApplicableParamsForVersion { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Collections.Hashtable] $PSBoundParametersIn, [Parameter(Mandatory = $true)] [System.String] $ParamName, [Parameter(Mandatory = $true)] [System.String] $ResourceName, [Parameter(Mandatory = $true)] [ValidateSet('2013', '2016', '2019')] [System.String[]] $ParamExistsInVersion ) if ($PSBoundParametersIn.ContainsKey($ParamName)) { $serverVersion = Get-ExchangeVersionYear if ($serverVersion -notin $ParamExistsInVersion) { Write-Warning "$ParamName is not a valid parameter for $ResourceName in Exchange $serverVersion. Skipping usage." Remove-FromPSBoundParametersUsingHashtable -PSBoundParametersIn $PSBoundParametersIn -ParamsToRemove $ParamName } } } <# .SYNOPSIS Takes a Hashtable object (generally PSBoundParameters), looks for any values that are of type System.String and are empty strings (""), and sets them to a value of $null instead. .PARAMETER PSBoundParametersIn The $PSBoundParameters hashtable from the calling function. #> function Set-EmptyStringParamsToNull { [CmdletBinding()] param ( [Parameter()] $PSBoundParametersIn ) [System.String[]] $emptyStringKeys = @() # First find all parameters that are a string, and are an empty string ("") foreach ($key in $PSBoundParametersIn.Keys) { if ($null -ne $PSBoundParametersIn[$key] -and $PSBoundParametersIn[$key].GetType().Name -eq 'String' -and $PSBoundParametersIn[$key] -eq '') { $emptyStringKeys += $key } } # Now that we have the keys, set their values to null foreach ($key in $emptyStringKeys) { $PSBoundParametersIn[$key] = $null } } <# .SYNOPSIS Takes an expected setting value and an actual setting value, and returns true if they are both comparable. .PARAMETER Name The name of the setting that is being compared. .PARAMETER Type The object type of the setting that is being compared. .PARAMETER ExpectedValue The expected value of the setting that is being compared. .PARAMETER ActualValue The actual value of the setting that is being compared. .PARAMETER PSBoundParametersIn The PSBoundParameters Hashtable of the calling function. #> function Test-ExchangeSetting { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [System.String] $Name, [Parameter(Mandatory = $true)] [System.String] $Type, [Parameter()] [System.Object] $ExpectedValue, [Parameter()] [System.Object] $ActualValue, [Parameter(Mandatory = $true)] [System.Collections.Hashtable] $PSBoundParametersIn ) $returnValue = $true if ($PSBoundParametersIn.ContainsKey($Name)) { if ($Type -like 'String') { $returnValue = Compare-StringToString -String1 $ExpectedValue -String2 $ActualValue -IgnoreCase } elseif ($Type -like 'Boolean') { $returnValue = Compare-BoolToBool -Bool1 $ExpectedValue -Bool2 $ActualValue } elseif ($Type -like 'Array') { $returnValue = Compare-ArrayContent -Array1 $ExpectedValue -Array2 $ActualValue -IgnoreCase } elseif ($Type -like 'Int') { $returnValue = $ExpectedValue -eq $ActualValue } elseif ($Type -like 'Unlimited') { $returnValue = Compare-UnlimitedToString -Unlimited $ActualValue -String $ExpectedValue } elseif ($Type -like 'Timespan') { $returnValue = Compare-TimespanToString -TimeSpan $ActualValue -String $ExpectedValue } elseif ($Type -like 'ADObjectID') { $returnValue = Compare-ADObjectIdToSmtpAddressString -ADObjectId $ActualValue -AddressString $ExpectedValue } elseif ($Type -like 'ByteQuantifiedSize') { $returnValue = Compare-ByteQuantifiedSizeToString -ByteQuantifiedSize $ActualValue -String $ExpectedValue } elseif ($Type -like 'IPAddress') { $returnValue = Compare-IPAddressToString -IPAddress $ActualValue -String $ExpectedValue } elseif ($Type -like 'IPAddresses') { $returnValue = Compare-IPAddressesToArray -IPAddressObjects $ActualValue -IPAddressStrings $ExpectedValue } elseif ($Type -like 'SMTPAddress') { $returnValue = Compare-SmtpAddressToString -SmtpAddress $ActualValue -String $ExpectedValue } elseif ($Type -like 'PSCredential') { $returnValue = Compare-PSCredential -Cred1 $ActualValue -Cred2 $ExpectedValue } elseif ($Type -like 'ExtendedProtection') { if ((Convert-StringArrayToLowerCase -Array $ExpectedValue).Contains('none')) { $returnValue = [System.String]::IsNullOrEmpty($ActualValue) } else { $returnValue = Compare-ArrayContent -Array1 $ExpectedValue -Array2 $ActualValue -IgnoreCase } } else { throw "Type not found: $Type" } } if ($returnValue -eq $false) { Write-InvalidSettingVerbose -SettingName $Name -ExpectedValue $ExpectedValue -ActualValue $ActualValue -Verbose:$VerbosePreference } return $returnValue } <# .SYNOPSIS Writes to the Verbose output stream that an invalid setting was detected. .PARAMETER SettingName The name of the setting being reported as Invalid. .PARAMETER ExpectedValue The expected value of the Invalid setting. .PARAMETER ActualValue The actual value of the Invalid setting. #> function Write-InvalidSettingVerbose { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $SettingName, [Parameter()] [System.Object] $ExpectedValue, [Parameter()] [System.Object] $ActualValue ) Write-Verbose -Message "Invalid setting '$SettingName'. Expected value: '$ExpectedValue'. Actual value: '$ActualValue'" } <# .SYNOPSIS Writes to the Verbose output stream the name of the calling function, as well as any relevant parameter names and values. .PARAMETER Parameters A Hashtable containing relevant parameter names and values to include in the output. #> function Write-FunctionEntry { [CmdletBinding()] param ( [Parameter()] [System.Collections.Hashtable] $Parameters ) $callingFunction = (Get-PSCallStack)[1].FunctionName if ($Parameters.Count -gt 0) { $parametersString = "" foreach ($key in $Parameters.Keys) { $value = $Parameters[$key] if ($parametersString -ne "") { $parametersString += ", " } $parametersString += "$($key) = '$($value)'" } Write-Verbose -Message "Entering function '$callingFunction'. Notable parameters: $parametersString" } else { Write-Verbose -Message "Entering function '$callingFunction'." } } <# .SYNOPSIS Creates and starts a new Scheduled Task using the specified parameters. .PARAMETER Path Specifies the path to an executable file. .PARAMETER Arguments Specifies arguments for the command-line operation. .PARAMETER Credential Specifies the name <run as> credentials to use when running the task. .PARAMETER TaskName Specifies the name of a scheduled task. .PARAMETER WorkingDirectory Specifies a directory where Task Scheduler will run the task. If you do not specify a working directory, Task Scheduler runs the task in the %windir%\system32 directory. .PARAMETER MaxWaitMinutes The amount of time in minutes that is allowed to complete the task. If set to 0 (the default), there is no time limit. .PARAMETER TaskPriority The priority level (0-10) of the task. Defaults to 4. Priority level 0 is the highest priority, and priority level 10 is the lowest priority. Priority levels 7 and 8 are used for background tasks, and priority levels 4, 5, and 6 are used for interactive tasks. #> function Start-ExchangeScheduledTask { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $Path, [Parameter()] [System.String] $Arguments, [Parameter()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, [Parameter()] [System.String] $TaskName, [Parameter()] [System.String] $WorkingDirectory, [Parameter()] [System.UInt32] $MaxWaitMinutes = 0, [Parameter()] [System.UInt32] $TaskPriority = 4 ) $tName = "$([System.Guid]::NewGuid().ToString())" if ($PSBoundParameters.ContainsKey('TaskName')) { $tName = "$TaskName $tName" } $action = New-ScheduledTaskAction -Execute "$Path" -Argument "$Arguments" if ($PSBoundParameters.ContainsKey('WorkingDirectory')) { $action.WorkingDirectory = $WorkingDirectory } Write-Verbose -Message "Created Scheduled Task with name: $tName" Write-Verbose -Message "Task Action: $Path $Arguments" # Use 'NT AUTHORITY\SYSTEM' as the run as account unless a specific Credential was provided $credParams = @{ User = 'NT AUTHORITY\SYSTEM' } if ($PSBoundParameters.ContainsKey('Credential')) { $credParams['User'] = $Credential.UserName $credParams.Add('Password', $Credential.GetNetworkCredential().Password) } $previousError = Get-PreviousError $task = Register-ScheduledTask @credParams -TaskName "$tName" -Action $action -RunLevel Highest -ErrorVariable errRegister -ErrorAction SilentlyContinue Assert-NoNewError -CmdletBeingRun 'Register-ScheduledTask' -PreviousError $previousError -Verbose:$VerbosePreference if ($null -ne $task -and $task.State -eq 'Ready') { # Set a time limit on the task $taskSettings = $task.Settings $taskSettings.ExecutionTimeLimit = "PT$($MaxWaitMinutes)M" $taskSettings.Priority = $TaskPriority Set-ScheduledTask @credParams -TaskName "$($task.TaskName)" -Settings $taskSettings Write-Verbose -Message "Starting task at: $([DateTime]::Now)" $task | Start-ScheduledTask } else { throw 'Failed to register Scheduled Task' } } <# .SYNOPSIS Returns whether or not the specified cmdlet has the given parameter name as an available parameter. .PARAMETER CmdletName The cmdlet name to check for parameters. .PARAMETER ParameterName The name of the parameter to check for. #> function Test-CmdletHasParameter { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [System.String] $CmdletName, [Parameter(Mandatory = $true)] [System.String] $ParameterName ) [System.Boolean] $hasParameter = $false $command = Get-Command -Name $CmdletName -ErrorAction SilentlyContinue if ($null -ne $command -and $null -ne $command.Parameters) { if ($command.Parameters.ContainsKey($ParameterName)) { $hasParameter = $true } } return $hasParameter } <# .SYNOPSIS Returns the most recent error in the $Global:Error Variable #> function Get-PreviousError { # Suppressing this rule to allow use of the built-in Global:Error variable [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] param () $previousError = $null if ($Global:Error.Count -gt 0) { $previousError = $Global:Error[0] } return $previousError } <# .SYNOPSIS Compares the most recent error in the $Global:Error variable to the input $PreviousError variable. If they are not the same, throws an exception. .PARAMETER CmdletBeingRun The name of the cmdlet that was run immediately prior to calling this function. .PARAMETER PreviousError The previous known error variable to compare against the most recent error that has occurred. #> function Assert-NoNewError { # Suppressing this rule to allow use of the built-in Global:Error variable [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] [CmdletBinding()] param ( [Parameter()] [System.String] $CmdletBeingRun, [Parameter()] [System.Management.Automation.ErrorRecord] $PreviousError ) # Throw an exception if errors were encountered if ($Global:Error.Count -gt 0 -and $PreviousError -ne $Global:Error[0]) { throw "Failed to run $CmdletBeingRun with: $($Global:Error[0])" } } <# .SYNOPSIS Checks whether the IIS Application Pool with the given name exists, and if so, restarts it. Does nothing if the Application Pool does not exist. .PARAMETER Name The name of the IIS Application Pool to check for and restart. #> function Restart-ExistingAppPool { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $Name ) $state = Get-WebAppPoolState -Name $Name -ErrorAction SilentlyContinue if ($null -ne $state) { Restart-WebAppPool -Name $Name } else { Write-Verbose -Message "Application pool with name '$Name' does not exist. Skipping application pool restart." } } <# .SYNOPSIS Returns whether the UM language pack for the specified culture is installed. .PARAMETER Culture The Culture of the UM language pack to check for. #> function Test-UMLanguagePackInstalled { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $Culture ) return [System.Boolean] (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\UnifiedMessagingRole\LanguagePacks').$Culture } <# .SYNOPSIS Returns whether the given IPAddress object is comparable to the given string. .PARAMETER IPAddress The System.Net.IPAddress object to compare. .PARAMETER String The System.String object to compare. #> function Compare-IPAddressToString { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] [System.Net.IPAddress] $IPAddress, [Parameter()] [System.String] $String ) if (($null -eq $IPAddress -and !([System.String]::IsNullOrEmpty($String))) -or ($null -ne $IPAddress -and [System.String]::IsNullOrEmpty($String))) { $returnValue = $false } elseif ($null -eq $IPAddress -and [System.String]::IsNullOrEmpty($String)) { $returnValue = $true } else { $returnValue = ($IPAddress.Equals([System.Net.IPAddress]::Parse($string))) } return $returnValue } <# .SYNOPSIS Returns whether the given SmtpAddress object is comparable to the given string. .PARAMETER SmtpAddress The Microsoft.Exchange.Data.SmtpAddress object to compare. .PARAMETER String The System.String object to compare. #> function Compare-SmtpAddressToString { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] [Nullable[Microsoft.Exchange.Data.SmtpAddress]] $SmtpAddress, [Parameter()] [System.String] $String ) if (($null -eq $SmtpAddress) -and ([System.String]::IsNullOrEmpty($String))) { Write-Verbose -Message 'Expected and actual value is empty, therefore equal!' return $true } elseif (($null -eq $SmtpAddress) -and -not ([System.String]::IsNullOrEmpty($String))) { return $false } elseif ($SmtpAddress.Gettype() -eq [Microsoft.Exchange.Data.SmtpAddress]) { if ([System.String]::IsNullOrEmpty($String)) { return $false } else { return ($SmtpAddress.Equals([Microsoft.Exchange.Data.SmtpAddress]::Parse($string))) } } else { Write-Verbose -Message 'No type of [Microsoft.Exchange.Data.SmtpAddress]!' return $false } } <# .SYNOPSIS Returns whether the given array of IPAddress objects is comparable to the given Array. .PARAMETER IPAddresses The array of IPAddress objects to compare. .PARAMETER Array The other array of objects to compare. #> function Compare-IPAddressesToArray { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] [System.Net.IPAddress[]] $IPAddressObjects, [Parameter()] [System.String[]] $IPAddressStrings ) [System.String[]] $validIPStrings = $IPAddressStrings | Where-Object -FilterScript { ![String]::IsNullOrEmpty($_) } if ($IPAddressObjects.Count -ne $validIPStrings.Count) { $returnValue = $false } elseif ($IPAddressObjects.Count -eq 0 -and $validIPStrings.Count -eq 0) { $returnValue = $true } else { $returnValue = $true foreach ($ipString in $validIPStrings) { if (!$IPAddressObjects.Contains([System.Net.IPAddress]::Parse($ipString))) { $returnValue = $false break } } } return $returnValue } <# .SYNOPSIS Returns whether the two PSCredential objects are equal. .PARAMETER Cred1 The first PSCredential object to compare. .PARAMETER Cred2 The second PSCredential object to compare. #> function Compare-PSCredential { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] [System.Management.Automation.PSCredential] $Cred1, [Parameter()] [System.Management.Automation.PSCredential] $Cred2 ) begin { $returnValue = $false if ($null -ne $Cred1) { $Cred1User = $Cred1.UserName $Cred1Password = $Cred1.GetNetworkCredential().Password } if ($null -ne $Cred2) { $Cred2User = $Cred2.UserName $Cred2Password = $Cred2.GetNetworkCredential().Password } } process { if (($Cred1User -like $Cred2User) -and ($Cred1Password -ceq $Cred2Password)) { Write-Verbose -Message 'Credentials match' $returnValue = $true } else { Write-Verbose -Message "Credentials don't match" Write-Verbose -Message "Cred1:$Cred1User Cred2:$Cred2User" } } end { return $returnValue } } <# .SYNOPSIS Returns whether the list of Service Principal Names is valid given the list of SPN Flags being used. .PARAMETER SPNList The list of Service Principal Names to inspect. .PARAMETER Flags The SPN Flags to use when inspecting the SPN List. #> function Test-ExtendedProtectionSPNList { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] [System.String[]] $SPNList, [Parameter()] [System.String[]] $Flags ) begin { # Initialize variable [System.Boolean] $IsDotless = $false [System.Boolean] $returnValue = $true [System.Boolean] $InvalidFlags = $false $flagsLower = Convert-StringArrayToLowerCase -Array $Flags # Check for invalid ExtendedProtectionFlags if (-not [System.String]::IsNullOrEmpty($Flags)) { if ($flagsLower.Contains('none') -and $Flags.Count -gt 1) { Write-Verbose -Message "Invalid combination of ExtendedProtectionFlags detected! Flag 'None' cannot be use with other flags." $InvalidFlags = $true $returnValue = $false } elseif ($flagsLower.Contains('proxy') -and $SPNList.Count -eq 0) { Write-Verbose -Message "Invalid combination of ExtendedProtectionFlags detected! Flag 'Proxy' requires one or more valid SPNs to be specified with ExtendedProtectionSPNList" $InvalidFlags = $true $returnValue = $false } } # Check for invalid formatted and Dotless SPNs if ((-not [System.String]::IsNullOrEmpty($SPNList)) -and (-not $InvalidFlags)) { # Check for Dotless SPN foreach ($S in $SPNList) { $Name = $S.Split('/')[1] if ([System.String]::IsNullOrEmpty($Name)) { Write-Verbose -Message "Invalid SPN: $S" $returnValue = $false break } else { if (-not $Name.Contains('.')) { Write-Verbose -Message "Found Dotless SPN: $Name" $IsDotless = $true break } } } } } process { # Check if AllowDotless is set in Flags if ($IsDotless) { if (!$flagsLower.Contains('allowdotlessspn')) { Write-Verbose -Message 'Dotless SPN found, but ExtendedProtectionFlags does not contain AllowDotlessSPN!' $returnValue = $false } } } end { $returnValue } } <# .SYNOPSIS Checks if the current Exchange Server version is contained within the $SupportedVersions parameter. If it is not, throws an exception. .PARAMETER ObjectOrOperationName The name of object type or operation name that is about to be utilized if the call to this function does not throw an exception. .PARAMETER SupportedVersions The allowed Exchange Server versions that the object or operation is allowed to be utilized on. #> function Assert-IsSupportedWithExchangeVersion { [CmdletBinding()] param ( [Parameter()] [System.String] $ObjectOrOperationName, [Parameter()] [System.String[]] $SupportedVersions ) $serverVersion = Get-ExchangeVersionYear if ($serverVersion -notin $SupportedVersions) { throw "$ObjectOrOperationName is not supported in Exchange Server $serverVersion" } } <# .SYNOPSIS Function used for invoking a dot-sourced script file or cmdlet. .PARAMETER ScriptPath The path of the script, or cmdlet, to execute via dot-sourcing. .PARAMETER ScriptParams Parameters to pass, if any, to the dot-sourced script or cmdlet. .PARAMETER SnapinsToRemove An optional list of PowerShell Snapins to check for and remove after executing the script or cmdlet. #> function Invoke-DotSourcedScript { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory = $true)] [System.String] $ScriptPath, [Parameter()] [System.Collections.Hashtable] $ScriptParams = @{ }, [Parameter()] [System.String[]] $SnapinsToRemove, [Parameter()] [System.String[]] $CommandsToExecuteInScope, [Parameter()] [System.Collections.Hashtable] $ParamsForCommandsToExecuteInScope ) [System.Collections.Hashtable] $returnValues = @{ } . $ScriptPath @ScriptParams for ($i = 0; $i -lt $CommandsToExecuteInScope.Count; $i++) { [System.String] $commandToExecute = $CommandsToExecuteInScope[$i] [System.Collections.Hashtable] $commandParams = @{ } if ($ParamsForCommandsToExecuteInScope.ContainsKey($commandToExecute)) { [System.Collections.Hashtable] $commandParams = $ParamsForCommandsToExecuteInScope[$commandToExecute] } $returnValue = . $commandToExecute @commandParams if (!$returnValues.ContainsKey($commandToExecute)) { $returnValues.Add($commandToExecute, $null) } $returnValues[$commandToExecute] = $returnValue } if ($SnapinsToRemove.Count -gt 0) { Remove-HelperSnapin -SnapinsToRemove $SnapinsToRemove -Verbose:$VerbosePreference } return $returnValues } <# .SYNOPSIS Detects whether the specified PowerShell snapins have been loaded, and if so, removes them. .PARAMETER SnapinsToRemove A list of PowerShell Snapins to check for and remove if loaded. #> function Remove-HelperSnapin { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String[]] $SnapinsToRemove ) foreach ($snapin in $SnapinsToRemove) { if ($null -ne (Get-PSSnapin -Name $snapin -ErrorAction SilentlyContinue)) { Write-Verbose -Message "'$snapin' snapin is currently loaded. Removing." Remove-PSSnapin -Name $snapin -ErrorAction SilentlyContinue -Confirm:$false } } } <# .SYNOPSIS Waits a specified amount of time for a process to start, and returns whether or not the process started. .PARAMETER ProcessName The process name to wait for to start. .PARAMETER SecondsPerSleep How many seconds to sleep between process checks. .PARAMETER MaxSleepCycles The maximum number of times to sleep without detecting the process before returning. #> function Wait-ForProcessStart { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [System.String] $ProcessName, [Parameter()] [System.Int32] $SecondsPerSleep = 1, [Parameter()] [System.Int32] $MaxSleepCycles = 60 ) $detectedProcess = $false Write-Verbose -Message "Waiting up to $($SecondsPerSleep * $MaxSleepCycles) seconds before exiting to give time for $ProcessName to start" for ($i = 0; $i -lt $MaxSleepCycles; $i++) { if ($null -eq (Get-Process -Name $ProcessName -ErrorAction SilentlyContinue)) { Start-Sleep -Seconds $SecondsPerSleep } else { Write-Verbose -Message "Detected that $ProcessName is running" $detectedProcess = $true break } } return $detectedProcess } <# .SYNOPSIS Waits a specified amount of time to detect that the given process is not running. .PARAMETER ProcessName The process name to wait for to stop. .PARAMETER SecondsPerSleep How many seconds to sleep between process checks. .PARAMETER MaxSleepCycles The maximum number of times to sleep without detecting the process stopping before returning. #> function Wait-ForProcessStop { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [System.String] $ProcessName, [Parameter()] [System.Int32] $SecondsPerSleep = 60, [Parameter()] [System.Int32] $MaxSleepCycles = 1440 ) $processStopped = $false for ($i = 0; $i -lt $MaxSleepCycles; $i++) { if ($null -ne (Get-Process -Name $ProcessName -ErrorAction SilentlyContinue)) { Write-Verbose -Message "$ProcessName is still running at $([DateTime]::Now). Sleeping for $SecondsPerSleep seconds." Start-Sleep -Seconds $SecondsPerSleep } else { $processStopped = $true break } } return $processStopped } <# .SYNOPSIS Checks whether Exchange Setup has completed successfully according to the input Setup Arguments, and throws an exception if it has not. .PARAMETER Path Path to the setup.exe of Exchange. .PARAMETER Arguments The command line arguments passed to Exchange Setup. #> function Assert-ExchangeSetupArgumentsComplete { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $Path, [Parameter(Mandatory = $true)] [System.String] $Arguments ) if (-Not (Test-Path -Path $Path -ErrorAction SilentlyContinue)) { throw "Path to Exchange setup '$Path' does not exists." } $installStatus = Get-ExchangeInstallStatus -Path $Path -Arguments $Arguments -Verbose:$VerbosePreference if ($installStatus.SetupComplete) { Write-Verbose -Message 'Exchange setup completed successfully' } else { throw 'Exchange setup did not complete successfully. See "<system drive>\ExchangeSetupLogs\ExchangeSetup.log" for details.' } } <# .SYNOPSIS Takes a Hashtable as input, and generates a string containing the keys and values of each Hashtable member. .PARAMETER Hashtable The Hashtable to convert to string form. .PARAMETER Separator The Separator character to use between key/value pairs. Defaults to ; . #> function Get-StringFromHashtable { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [System.Collections.Hashtable] $Hashtable, [Parameter()] [System.Char] $Separator = ';' ) $stringBuilder = New-Object -TypeName System.Text.StringBuilder foreach ($key in ($Hashtable.Keys | Sort-Object)) { if ($stringBuilder.Length -gt 0) { $stringBuilder.Append($Separator) | Out-Null } $stringBuilder.Append($key) | Out-Null $stringBuilder.Append('=') | Out-Null $stringBuilder.Append(([String] $Hashtable[$key])) | Out-Null } return $stringBuilder.ToString() } <# .SYNOPSIS Takes a domain name in FQDN format and returns it in distinguishedName format. .PARAMETER Fqdn The FQDN of the domain name to convert. #> function Get-DomainDNFromFQDN { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()] [System.String] $Fqdn ) return 'dc=' + $Fqdn.Replace('.', ',dc=') } <# .SYNOPSIS Sets the Global DSCMachineStatus variable to the desired value. .PARAMETER NewDSCMachineStatus The value to set $global:DSCMachineStatus to. #> function Set-DSCMachineStatus { # Suppressing this rule because $global:DSCMachineStatus is used to trigger a reboot. [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] <# Suppressing this rule because $global:DSCMachineStatus is only set, never used (by design of Desired State Configuration). #> [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Int32] $NewDSCMachineStatus ) $global:DSCMachineStatus = $NewDSCMachineStatus } <# .SYNOPSIS Checks if Extended rights on a AD object are correct. .PARAMETER ADPermissions The current permissions set on a AD object. .PARAMETER ExtendedRights The expected permissions to be present. .PARAMETER Deny Specifies if the permissions being checked have 'Deny' option set. #> function Test-ExtendedRightsPresent { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter()] $ADPermissions, [Parameter()] [Microsoft.Management.Infrastructure.CimInstance[]] $ExtendedRights, [Parameter()] [System.Boolean] $Deny ) foreach ($Right in $ExtendedRights) { foreach ($Value in $($Right.Value.Split(','))) { $permissionsFound = $ADPermissions | Where-Object { ($_.User.RawIdentity -like "*$($Right.Key)*" -and ($_.ExtendedRights.RawIdentity -like "*$Value*")) } if ($null -ne $permissionsFound) { if ($Deny -eq $true -and $permissionsFound.Deny -eq $false -or $Deny -eq $false -and $permissionsFound.Deny -eq $true) { Write-InvalidSettingVerbose -SettingName 'ExtendedRight' -ExpectedValue "User:$($Right.Key) Value:$Value" -ActualValue 'Present' -Verbose:$VerbosePreference return $false } } else { Write-InvalidSettingVerbose -SettingName 'ExtendedRight' -ExpectedValue "User:$($Right.Key) Value:$Value" -ActualValue 'Absent' -Verbose:$VerbosePreference return $false } } } } <# .SYNOPSIS Verifies whether the required Extended permissions are correctly set. .DESCRIPTION This functions checkes where all ExtendedRightAllowEntries and ExtendedRightDenyEntries are corretly set on an AD object. .PARAMETER ExtendedRightAllowEntries The desired ExtendedRightAllowEntries. .PARAMETER ExtendedRightDenyEntries The desired ExtendedRightDenyEntries. .PARAMETER ADPermissions The currently present active directory permissions. .OUTPUTS Boolean #> function Test-ExtendedRights { [CmdletBinding()] param ( [Parameter()] [Microsoft.Management.Infrastructure.CimInstance[]] $ExtendedRightAllowEntries, [Parameter()] [Microsoft.Management.Infrastructure.CimInstance[]] $ExtendedRightDenyEntries, [Parameter(Mandatory = $true)] [System.Object] $ADPermissions ) if ($ExtendedRightAllowEntries -and $ADPermissions.Deny -contains $false) { $splat = @{ ADPermissions = $adPermissions ExtendedRights = $ExtendedRightAllowEntries Deny = $false Verbose = $VerbosePreference } $permissionsPresent = Test-ExtendedRightsPresent @splat if ($permissionsPresent -eq $false) { return $false } } if (-not $ExtendedRightAllowEntries -and $ADPermissions -and $ADPermissions.Deny -notcontains $false) { return $false } if ($ExtendedRightDenyEntries -and $ADPermissions.Deny -contains $true) { $splat = @{ ADPermissions = $ADPermissions ExtendedRights = $ExtendedRightDenyEntries Deny = $true Verbose = $VerbosePreference } $permissionsPresent = Test-ExtendedRightsPresent @splat if ($permissionsPresent -eq $false) { return $false } } if (-not $ExtendedRightDenyEntries -and $ADPermissions -and $ADPermissions.Deny -contains $true) { return $false } return $true } <# .SYNOPSIS This functions returns the extended permissions for an Exchnage object. .DESCRIPTION All non-inherited allow and deny permissions are chcked and returned in the form of CimInstance list. .OUTPUTS System.Hashtable #> function Get-ADExtendedPermissions { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $Identity ) $adPermissions = Get-ADPermission -Identity $Identity -ErrorAction SilentlyContinue | Where-Object { $_.IsInherited -eq $false -and $null -ne $_.ExtendedRights } if ($null -eq $adPermissions) { return @{ ExtendedRightAllowEntries = $null ExtendedRightDenyEntries = $null } } $userNames = $ADPermissions.User | Select-Object -Property RawIdentity -Unique | ForEach-Object -MemberName RawIdentity $ExtendedRightAllowEntries = [System.Collections.Generic.List[Microsoft.Management.Infrastructure.CimInstance]]::new() $ExtendedRightDenyEntries = [System.Collections.Generic.List[Microsoft.Management.Infrastructure.CimInstance]]::new() foreach ($user in $userNames) { $allowPermissions = ($ADPermissions | Where-Object -FilterScript { $_.User.RawIdentity -eq $user -and $_.Deny -eq $false } | ForEach-Object -MemberName ExtendedRights | ForEach-Object -MemberName RawIdentity) -join ',' $denyPermissions = ($ADPermissions | Where-Object -FilterScript { $_.User.RawIdentity -eq $user -and $_.Deny -eq $true } | ForEach-Object -MemberName ExtendedRights | ForEach-Object -MemberName RawIdentity) -join ',' if ($allowPermissions) { $ExtendedRightAllowEntries.Add( ( New-CimInstance -ClassName MSFT_KeyValuePair -Property @{ key = $user value = $allowPermissions } -ClientOnly ) ) } if ($denyPermissions) { $ExtendedRightDenyEntries.Add( ( New-CimInstance -ClassName MSFT_KeyValuePair -Property @{ key = $user value = $denyPermissions } -ClientOnly ) ) } } return @{ ExtendedRightAllowEntries = $ExtendedRightAllowEntries ExtendedRightDenyEntries = $ExtendedRightDenyEntries } } <# .SYNOPSIS Set AD extendend permissions on an Exchange object. .DESCRIPTION Set AD extendend permissions on an Exchange object. .PARAMETER ExtendedRightAllowEntries Additional allow permissions. .PARAMETER ExtendedRightDenyEntries Additional deny permissions. .PARAMETER DomainController Specifies the domain controller that's used by this cmdlet to read data from or write data to Active Directory. .PARAMETER NewObject Specfies whether the object was just created by Set-TargetRessource #> function Set-ADExtendedPermissions { [CmdletBinding()] param ( [Parameter()] [Microsoft.Management.Infrastructure.CimInstance[]] $ExtendedRightAllowEntries = @(), [Parameter()] [Microsoft.Management.Infrastructure.CimInstance[]] $ExtendedRightDenyEntries = @(), [Parameter()] [System.String] $DomainController, [Parameter(Mandatory = $true)] [System.String] $Identity, [Parameter(Mandatory = $true)] [System.Management.Automation.SwitchParameter] $NewObject ) if ($NewObject -and (-not $DomainController)) { $adObject = Get-ADPermission -Identity $Identity -ErrorAction SilentlyContinue $itt = 0 while ($null -eq $adObject -and $itt -le 3) { Write-Verbose -Message 'Extended AD permissions were specified and the new connector is still not found in AD. Sleeping for 30 seconds.' Start-Sleep -Seconds 30 $itt++ $adObject = Get-ADPermission -Identity $Identity -ErrorAction SilentlyContinue } if ($null -eq $adObject) { throw "The AD Object $Identity was not found after 2 minutes of wait time. Please check AD replication!" } } if ($DomainController) { Write-Verbose -Message 'Setting domain controller as default parameter.' $PSDefaultParameterValues = @{ 'Add-ADPermission:DomainController' = $DomainController } } if ($ExtendedRightAllowEntries) { Write-Verbose -Message "Setting ExtendedRightAllowEntries for AD Object: $Identity." foreach ($ExtendedRightAllowEntry in $ExtendedRightAllowEntries) { foreach ($Value in $($ExtendedRightAllowEntry.Value.Split(','))) { Add-ADPermission -Identity $Identity -User $ExtendedRightAllowEntry.Key -ExtendedRights $Value } } } if ($ExtendedRightDenyEntries) { Write-Verbose -Message "Setting ExtendedRightDenyEntries AD Object: $Identity." foreach ($ExtendedRightDenyEntry in $ExtendedRightDenyEntries) { foreach ($Value in $($ExtendedRightDenyEntry.Value.Split(','))) { Add-ADPermission -Identity $Identity -User $ExtendedRightDenyEntry.Key -ExtendedRights $Value -Deny -Confirm:$false } } } } Export-ModuleMember -Function * |