Modules/xWebAdministration.Common/xWebAdministration.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 } <# .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 } <# .SYNOPSIS Assert if the role specific module is installed or not and optionally import it. .PARAMETER ModuleName The name of the module to assert is installed. .PARAMETER ImportModule This switch causes the module to be imported if it is installed. #> function Assert-Module { [CmdletBinding()] param ( [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $ModuleName = 'WebAdministration', [Parameter()] [System.Management.Automation.SwitchParameter] $ImportModule ) if (-not (Get-Module -Name $ModuleName -ListAvailable)) { $errorMessage = $script:localizedData.ModuleNotFoundError -f $moduleName New-ObjectNotFoundException -Message $errorMessage } if ($ImportModule) { Import-Module -Name $ModuleName } } #end function Assert-Module <# .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, from the keys provided in DesiredValues, that will be compared. If this parameter is left out, all the keys in the DesiredValues will be compared. #> function Compare-ResourcePropertyState { [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' 'UInt32' '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 This returns a new MSFT_Credential CIM instance credential object to be used when returning credential objects from Get-TargetResource. This returns a credential object without the password. .PARAMETER Credential The PSCredential object to return as a MSFT_Credential CIM instance credential object. .NOTES When returning a PSCredential object from Get-TargetResource, the credential object does not contain the username. The object is empty. Password UserName PSComputerName -------- -------- -------------- localhost When the MSFT_Credential CIM instance credential object is returned by the Get-TargetResource then the credential object contains the values provided in the object. Password UserName PSComputerName -------- -------- -------------- COMPANY\TestAccount localhost #> function New-CimCredentialInstance { [CmdletBinding()] [OutputType([Microsoft.Management.Infrastructure.CimInstance])] param ( [Parameter(Mandatory = $true)] [System.Management.Automation.PSCredential] $Credential ) $newCimInstanceParameters = @{ ClassName = 'MSFT_Credential' ClientOnly = $true Namespace = 'root/microsoft/windows/desiredstateconfiguration' Property = @{ UserName = [System.String] $Credential.UserName Password = [System.String] $null } } return New-CimInstance @newCimInstanceParameters } <# .SYNOPSIS This is used to get the current user context when the resource script runs. .NOTES We are putting this in a function so we can mock it with pester #> function Get-CurrentUser { [CmdletBinding()] [OutputType([System.String])] param () return [System.Security.Principal.WindowsIdentity]::GetCurrent() } <# .SYNOPSIS Locates one or more certificates using the passed certificate selector parameters. If more than one certificate is found matching the selector criteria, they will be returned in order of descending expiration date. .PARAMETER Thumbprint The thumbprint of the certificate to find. .PARAMETER FriendlyName The friendly name of the certificate to find. .PARAMETER Subject The subject of the certificate to find. .PARAMETER DNSName The subject alternative name of the certificate to export must contain these values. .PARAMETER Issuer The issuer of the certiicate to find. .PARAMETER KeyUsage The key usage of the certificate to find must contain these values. .PARAMETER EnhancedKeyUsage The enhanced key usage of the certificate to find must contain these values. .PARAMETER Store The Windows Certificate Store Name to search for the certificate in. Defaults to 'My'. .PARAMETER AllowExpired Allows expired certificates to be returned. #> function Find-Certificate { [CmdletBinding()] [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2[]])] param ( [Parameter()] [String] $Thumbprint, [Parameter()] [String] $FriendlyName, [Parameter()] [String] $Subject, [Parameter()] [String[]] $DNSName, [Parameter()] [String] $Issuer, [Parameter()] [String[]] $KeyUsage, [Parameter()] [String[]] $EnhancedKeyUsage, [Parameter()] [String] $Store = 'My', [Parameter()] [Boolean] $AllowExpired = $false ) $certPath = Join-Path -Path 'Cert:\LocalMachine' -ChildPath $Store if (-not (Test-Path -Path $certPath)) { # The Certificate Path is not valid New-InvalidArgumentException ` -Message ($script:localizedData.CertificatePathError -f $certPath) ` -ArgumentName 'Store' } # if # Assemble the filter to use to select the certificate $certFilters = @() if ($PSBoundParameters.ContainsKey('Thumbprint')) { $certFilters += @('($_.Thumbprint -eq $Thumbprint)') } # if if ($PSBoundParameters.ContainsKey('FriendlyName')) { $certFilters += @('($_.FriendlyName -eq $FriendlyName)') } # if if ($PSBoundParameters.ContainsKey('Subject')) { $certFilters += @('(@(Compare-Object ` -ReferenceObject (($_.Subject -split ", ").trim()|sort-object) ` -DifferenceObject (($subject -split ",").trim()|sort-object)| ` Where-Object -Property SideIndicator -eq "=>").Count -eq 0)') } # if if ($PSBoundParameters.ContainsKey('Issuer')) { $certFilters += @('($_.Issuer -eq $Issuer)') } # if if (-not $AllowExpired) { $certFilters += @('(((Get-Date) -le $_.NotAfter) -and ((Get-Date) -ge $_.NotBefore))') } # if if ($PSBoundParameters.ContainsKey('DNSName')) { $certFilters += @('(@(Compare-Object ` -ReferenceObject $_.DNSNameList.Unicode ` -DifferenceObject $DNSName | ` Where-Object -Property SideIndicator -eq "=>").Count -eq 0)') } # if if ($PSBoundParameters.ContainsKey('KeyUsage')) { $certFilters += @('(@(Compare-Object ` -ReferenceObject ($_.Extensions.KeyUsages -split ", ") ` -DifferenceObject $KeyUsage | ` Where-Object -Property SideIndicator -eq "=>").Count -eq 0)') } # if if ($PSBoundParameters.ContainsKey('EnhancedKeyUsage')) { $certFilters += @('(@(Compare-Object ` -ReferenceObject ($_.EnhancedKeyUsageList.FriendlyName) ` -DifferenceObject $EnhancedKeyUsage | ` Where-Object -Property SideIndicator -eq "=>").Count -eq 0)') } # if # Join all the filters together $certFilterScript = '(' + ($certFilters -join ' -and ') + ')' Write-Verbose -Message ($script:localizedData.SearchingForCertificateUsingFilters ` -f $store, $certFilterScript) $certs = Get-ChildItem -Path $certPath | Where-Object -FilterScript ([ScriptBlock]::Create($certFilterScript)) # Sort the certificates if ($certs.count -gt 1) { $certs = $certs | Sort-Object -Descending -Property 'NotAfter' } # if return $certs } # end function Find-Certificate <# .SYNOPSIS Internal function to throw terminating error with specified errorCategory, errorId and errorMessage .PARAMETER ErrorId Specifies the Id error message. .PARAMETER ErrorMessage Specifies full Error Message to be returned. .PARAMETER ErrorCategory Specifies Error Category. #> function New-TerminatingError { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String] $ErrorId, [Parameter(Mandatory = $true)] [String] $ErrorMessage, [Parameter(Mandatory = $true)] [System.Management.Automation.ErrorCategory] $ErrorCategory ) $exception = New-Object System.InvalidOperationException $ErrorMessage $errorRecord = New-Object System.Management.Automation.ErrorRecord ` $exception, $ErrorId, $ErrorCategory, $null throw $errorRecord } <# .SYNOPSIS Returns the value of a WebConfigurationProperty. .PARAMETER WebConfigurationPropertyObject Specifies the WebConfigurationProperty to return the value for. .NOTES This is a helper function because the type are not mocked. #> function Get-WebConfigurationPropertyValue { [CmdletBinding()] [OutputType([PSObject])] param ( [Parameter()] [PSObject] $WebConfigurationPropertyObject ) if ($WebConfigurationPropertyObject -is [Microsoft.IIS.PowerShell.Framework.ConfigurationAttribute]) { return $WebConfigurationPropertyObject.Value } else { return $WebConfigurationPropertyObject } } $script:localizedData = Get-LocalizedData -ResourceName 'xWebAdministration.Common' -ScriptRoot $PSScriptRoot |