Modules/AdfsDsc.Common/AdfsDsc.Common.psm1
Set-StrictMode -Version 2.0 function Get-LocalizedData { <# .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). #> [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 } function New-InvalidArgumentException { <# .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. #> [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 } function New-InvalidOperationException { <# .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. #> [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 } function New-ObjectNotFoundException { <# .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. #> [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 } function New-InvalidResultException { <# .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. #> [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 New-NotImplementedException { <# .SYNOPSIS Creates and throws a not implemented 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. #> [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.NotImplementedException' ` -ArgumentList @($Message) } else { $exception = New-Object -TypeName 'System.NotImplementedException' ` -ArgumentList @($Message, $ErrorRecord.Exception) } $newObjectParameters = @{ TypeName = 'System.Management.Automation.ErrorRecord' ArgumentList = @( $exception.ToString(), 'MachineStateIncorrect', 'NotImplemented', $null ) } $errorRecordToThrow = New-Object @newObjectParameters throw $errorRecordToThrow } function ConvertTo-TimeSpan { <# .SYNOPSIS Convert a specified time period in seconds, minutes, hours or days into a time span object. .PARAMETER TimeSpan The length of time to use for the time span. .PARAMETER TimeSpanType The units of measure in the TimeSpan parameter. #> [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) } function ConvertFrom-TimeSpan { <# .SYNOPSIS Converts a System.TimeSpan into the number of seconds, minutes, 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 ConvertFrom-TimeSpan -TimeSpan (New-TimeSpan -Days 15) -TimeSpanType Seconds Returns the number of seconds in 15 days. #> [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] } } } function Compare-ResourcePropertyState { <# .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. .PARAMETER IgnoreProperties An array of property names, from the keys provided in DesiredValues, that will be ignored. #> [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) $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) $parameterState['InDesiredState'] = $true } else { Write-Verbose -Message ($script:localizedData.PropertyNotInDesiredState -f $parameterName) $parameterState['InDesiredState'] = $false } $compareTargetResourceStateReturnValue += $parameterState } return $compareTargetResourceStateReturnValue } function Test-DscPropertyState { <# .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 } #> [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [System.Collections.Hashtable] $Values ) if ($null -eq $Values.CurrentValue -and $null -eq $Values.DesiredValue) { # Both values are $null so return $true $returnValue = $true } elseif ($null -eq $Values.CurrentValue -or $null -eq $Values.DesiredValue) { # Either CurrentValue or DesiredValue are $null so return $false $returnValue = $false } elseif ($Values.DesiredValue.GetType().IsArray -or $Values.CurrentValue.GetType().IsArray) { $compareObjectParameters = @{ ReferenceObject = $Values.CurrentValue DifferenceObject = $Values.DesiredValue } $arrayCompare = Compare-Object @compareObjectParameters -SyncWindow 0 if ($null -ne $arrayCompare) { Write-Verbose -Message $script:localizedData.ArrayDoesNotMatch -Verbose:$VerbosePreference $arrayCompare | ForEach-Object -Process { Write-Verbose -Message ($script:localizedData.ArrayValueThatDoesNotMatch -f ` $_.InputObject, $_.SideIndicator) -Verbose:$VerbosePreference } $returnValue = $false } else { $returnValue = $true } } elseif ($Values.CurrentValue -ne $Values.DesiredValue) { $desiredType = $Values.DesiredValue.GetType() $returnValue = $false $supportedTypes = @( 'String' 'Int16' 'UInt16' 'Int32' 'UInt32' 'Single' 'Boolean' 'DateTime' ) if ($desiredType.Name -notin $supportedTypes) { Write-Warning -Message ($script:localizedData.UnableToCompareType ` -f $desiredType.Name) } else { Write-Verbose -Message ( $script:localizedData.PropertyValueOfTypeDoesNotMatch ` -f $desiredType.Name, $Values.CurrentValue, $Values.DesiredValue ) -Verbose:$VerbosePreference } } else { $returnValue = $true } return $returnValue } function New-CimCredentialInstance { <# .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 an MSFT_Credential CIM instance credential object. .PARAMETER UserName The Username to return as an 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 #> [CmdletBinding()] [OutputType([Microsoft.Management.Infrastructure.CimInstance])] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Credential')] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $true, ParameterSetName = 'Username')] [System.String] $UserName ) if ($PSCmdlet.ParameterSetName -eq 'Credential') { $MSFT_UserName = $Credential.UserName } else { $MSFT_UserName = $UserName } $newCimInstanceParameters = @{ ClassName = 'MSFT_Credential' ClientOnly = $true Namespace = 'root/microsoft/windows/desiredstateconfiguration' Property = @{ UserName = [System.String] $MSFT_UserName Password = [System.String] $null } } return New-CimInstance @newCimInstanceParameters } function Assert-Module { <# .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. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ModuleName, [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 } } function Assert-DomainMember { <# .SYNOPSIS Asserts if the computer is a member of a domain. #> [CmdletBinding()] param() if (-not ((Get-CimInstance -ClassName Win32_ComputerSystem -Verbose:$false).PartOfDomain)) { New-InvalidOperationException -Message $script:localizedData.NotDomainMemberError } } function Assert-AdfsService { <# .SYNOPSIS Assert that the ADFS Service exists and is running. .DESCRIPTION The ADFS service is configured to have a startup type of 'Automatic (Delayed Start)' which means that it will be one or two minutes after the server has started before the ADFS service will start. Because of this, we need to wait and retry the ADFS service running check if we are within a bootup retry window. .PARAMETER RetryInterval Specified the time in seconds to wait between each check of the service. .PARAMETER MaxRetries Specifies the maximum number of times to retry before raising an exception. .PARAMETER RetryWindow Specifies the window in minutes after bootup to retry checking the service. #> [CmdletBinding()] param ( [Parameter()] [Int16] $RetryInterval = 30, [Parameter()] [Int16] $MaxRetries = 10, [Parameter()] [Int16] $RetryWindow = 30 ) $lastBootUpTime = (Get-CimInstance -ClassName Win32_OperatingSystem -Verbose:$false).LastBootUpTime $retryCount = 0 do { $upTime = (Get-Date) - $lastBootUpTime $insideRetryWindow = $upTime.TotalMinutes -lt $RetryWindow try { # Check ADFS service is running $adfsService = Get-Service -Name $script:adfsServiceName } catch { New-InvalidOperationException -Message $script:localizedData.GetAdfsServiceError -ErrorRecord $_ } if ($adfsService.Status -ne 'Running' -and $insideRetryWindow) { $retryCount++ Write-Verbose -Message ($script:localizedData.WaitingForAdfsServiceMessage -f ` $RetryInterval, $retryCount, $MaxRetries) Start-Sleep -Seconds $RetryInterval } } until ($adfsService.Status -eq 'Running' -or $retryCount -ge $MaxRetries -or $insideRetryWindow -eq $false) if ($adfsService.Status -ne 'Running') { New-InvalidOperationException -Message $script:localizedData.AdfsServiceNotRunningError } } function Assert-Command { <# .SYNOPSIS Assert that the specified command exists in the specified module. .PARAMETER Module Specifies the module name. .PARAMETER Command Specifies the command name. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $Command, [Parameter(Mandatory = $true)] [System.String] $Module ) if (!(Get-Command -Name $Command -Module $Module -ErrorAction SilentlyContinue)) { New-NotImplementedException -Message ( $script:localizedData.ResourceNotImplementedError -f $Module, $Command) } } Function Get-ADObjectByQualifiedName { <# .SYNOPSIS Gets an Active Directory object by qualified name. .PARAMETER Name Specifies the qualified name to search for. #> [CmdletBinding()] [OutputType([System.DirectoryServices.SearchResult])] param ( [Parameter(Mandatory = $true)] [System.String] $Name ) $root = [System.DirectoryServices.DirectoryEntry]::new('LDAP://RootDSE') $searchRoot = [System.DirectoryServices.DirectoryEntry]::new( "LDAP://CN=Partitions," + $root.configurationNamingContext) if ($Name -like '*\*') { # Legacy name format $netBiosName = ($Name.Split('\'))[0] $samAccountName = ($Name.Split('\'))[1] $domain = [System.DirectoryServices.DirectorySearcher]::new( $searchRoot, "(&(objectcategory=crossRef)(nETBIOSName=$netBiosName))").FindOne() if ($domain) { $searchResult = [System.DirectoryServices.DirectorySearcher]::new( "LDAP://$($domain.Properties.ncname)", "SamAccountName=$samAccountName", 'ObjectCategory').FindOne() } else { $errorMessage = $script:localizedData.UnknownNetBiosNameError New-InvalidArgumentException -Message $errorMessage -ArgumentName $netBiosName } } elseif ($Name -like '*@*') { # UPN format $searchResult = [System.DirectoryServices.DirectorySearcher]::new( "userPrincipalName=$Name", 'ObjectCategory').FindOne() } else { $errorMessage = $script:localizedData.UnknownNameFormatError New-InvalidArgumentException -Message $errorMessage -ArgumentName $Name } $searchResult } function Assert-GroupServiceAccount { <# .SYNOPSIS Assert if the service account is a Group Managed Service Account. .PARAMETER Name Specifies the service account name. #> [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [System.String] $Name ) $adObject = Get-ADObjectbyQualifiedName -Name $Name if ($adObject) { switch -Wildcard ($adObject.Properties.objectcategory) { 'CN=ms-DS-Group-Managed-Service-Account,CN=Schema,CN=Configuration*' { $isGroupServiceAccount = $true Break } 'CN=ms-DS-Managed-Service-Account*' { $isGroupServiceAccount = $false Break } 'CN=Person,CN=Schema,CN=Configuration*' { $isGroupServiceAccount = $false Break } Default { New-InvalidResultException -Message ( ` $script:localizedData.UnexpectedServiceAccountCategoryError -f ` $adObject.Properties.ObjectCategory, $Name) } } } else { New-ObjectNotFoundException -Message ( ` $script:localizedData.ServiceAccountNotFoundError -f $Name) } $isGroupServiceAccount } function Get-AdfsConfigurationStatus { <# .SYNOPSIS Get the configuration status of the ADFS Service #> [CmdletBinding()] [OutputType([System.String])] param () try { $fsConfigurationStatus = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ADFS').FSConfigurationStatus } catch { New-InvalidResultException -Message $script:localizedData.ConfigurationStatusNotFoundError } switch ($fsConfigurationStatus) { '0' { $ReturnValue = 'NotConfigured' } '1' { $ReturnValue = 'NotConfigured' } '2' { $ReturnValue = 'Configured' } default { New-InvalidResultException -Message ($script:localizedData.UnknownConfigurationStatusError -f $_) } } $ReturnValue } function Get-ObjectType { <# .SYNOPSIS Returns the type name of the input object #> [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [AllowNull()] [System.Management.Automation.PSObject] $Object ) $Object.GetType().FullName } function ConvertTo-IssuanceTransformRule { <# .SYNOPSIS Convert a CIMInstance MSFT_AdfsIssuanceTransformRule object to a Claims Rule string .NOTES https://blogs.technet.microsoft.com/askds/2011/10/07/ad-fs-2-0-claims-rule-language-primer/ Example LDAPClaims Transform Claims Rule string: @RuleTemplate = "LdapClaims" @RuleName = "test" c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"] => issue(store = "Active Directory", types = ("test", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"), query = ";test,mail,givenName,sn;{0}", param = c.Value); Example EmitGroupClaims Transform Claims Rule string: @RuleTemplate = "EmitGroupClaims" @RuleName = "IDscan Users SRV EU-West-1" c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid", Value == "S-1-5-21-2624039266-918686060-4041204886-1128", Issuer == "AD AUTHORITY"] => issue(Type = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", Value = "IDScan User", Issuer = c.Issuer, OriginalIssuer = c.OriginalIssuer, ValueType = c.ValueType); IssuanceTransformRules = @( @{ TemplateName = 'LdapClaims' Name = 'Test' AtttributeStore = 'ActiveDirectory' LdapMapping = @( @{ LdapAttribute = 'mail' OutgoingClaimType = 'emailaddress' } @{ LdapAttribute = 'givenName' OutgoingCliamType = 'givenname' } ) } @{ TemplateName = 'EmitGroupClaims' Name = 'Group Membership' GroupName = 'Test' OutgoingClaimType = 'http://schemas.microsoft.com/ws/2008/06/identity/claims/role' OutgoingNameIDFormat = '' OutgoingClaimValue = 'App1 User' } @{ TemplateName = 'CustomClaims' Name = 'Test' CustomRule = '' } ) #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [AllowEmptyCollection()] [AllowNull()] [Microsoft.Management.Infrastructure.CimInstance[]] $InputObject ) begin { $ldapClaimsTransformRule = @( '@RuleTemplate = "LdapClaims"' '@RuleName = "{1}"' 'c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"]' '=> issue(store = "{2}", types = ({3}), query = ";{4};{0}", param = c.Value);' ) | Out-String $emitGroupClaimsTransformRule = @( '@RuleTemplate = "EmitGroupClaims"' '@RuleName = "{0}"' 'c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid", Value == "{1}", Issuer == "AD AUTHORITY"]' '=> issue(Type = "{2}", Value = "{3}", Issuer = c.Issuer, OriginalIssuer = c.OriginalIssuer, ValueType = c.ValueType);' ) | Out-String $customTransformRule = @( '@RuleName = "{0}"' '{1}' ) | Out-String $output = '' } process { foreach ($rule in $InputObject) { if ($rule.TemplateName -eq 'LdapClaims') { Write-Debug 'Processing LdapClaims Template Rule' $claimTypes = '"' + ($rule.LdapMapping.OutGoingClaimType -join '", "') + '"' $ldapAttributes = $rule.LdapMapping.LdapAttribute -join ',' $output += ($ldapClaimsTransformRule -f '{0}', $rule.Name, $rule.AttributeStore, $claimTypes, $ldapAttributes) } elseif ($rule.TemplateName -eq 'EmitGroupClaims') { Write-Debug 'Processing EmitGroupClaims Template Rule' $groupSid = Get-AdGroupSid -GroupName $rule.GroupName $output += ($emitGroupClaimsTransformRule -f $rule.Name, $groupSid, $rule.OutgoingClaimType, $rule.OutgoingClaimValue) } elseif ($rule.TemplateName -eq 'CustomClaims') { Write-Debug 'Processing CustomClaims Template Rule' $output += $customTransformRule -f $rule.Name, $rule.CustomRule } else { $errorMessage = $script:localizedData.UnknownIssuanceTransformRuleTemplateError -f $rule.TemplateName New-InvalidOperationException -Message $errorMessage } } } end { Write-Debug "ConvertTo-IssuanceTransformRule output: $output" $output } } function ConvertFrom-IssuanceTransformRule { <# .SYNOPSIS Convert a Claims Rule string to a CIMInstance MSFT_AdfsIssuanceTransformRule object .NOTES IssuanceTransformRules = @( MSFT_AdfsIssuanceTransformRule { TemplateName = 'LdapClaims' Name = 'Test' AtttributeStore = 'ActiveDirectory' LdapMapping = @( MSFT_AdfsLdapMapping { LdapAttribute = 'emailaddress' OutgoingClaimType = 'mail' } ) } #> [CmdletBinding()] [OutputType([Microsoft.Management.Infrastructure.CimInstance])] param ( [Parameter(Mandatory = $true)] [AllowEmptyString()] [System.String[]] $Rule ) $ruleLines = $Rule -split '\r?\n' $individualRules = [System.Collections.ArrayList]@() $individualRule = @() foreach ($ruleLine in $ruleLines) { if ($ruleLine -eq '') { if ($individualRule) { $individualRules.Add($individualRule) | Out-Null $individualRule = @() } } else { $individualRule += $ruleLine } } $MSFT_AdfsIssuanceTransformRule = @() if ($individualRules) { foreach ($individualRule in $individualRules) { if ($individualRule[0] -eq '@RuleTemplate = "LdapClaims"') { Write-Debug 'Processing LdapClaims Template Rule' $outgoingClaimTypes = @(($individualRule[3].Split('(').Split(')')[2]).Split(',').Trim().Trim('"')) $ldapAttributes = @(($individualRule[3].Split(';')[1]).Split(',')) $MSFT_AdfsLdapMapping = @() for ($index = 0; $index -lt $ldapAttributes.Count; $index++) { $ldapMapping = @{ LdapAttribute = $ldapAttributes[$index] OutgoingClaimType = $outgoingClaimTypes[$index] } $MSFT_AdfsLdapMapping += New-CimInstance -ClassName MSFT_AdfsLdapMapping ` -Namespace root/microsoft/Windows/DesiredStateConfiguration ` -Property $ldapMapping -ClientOnly } $issuanceTransformRule = @{ TemplateName = 'LdapClaims' Name = $individualRule[1].split('"')[1] AttributeStore = $individualRule[3].split('"')[1] LdapMapping = [CimInstance[]]$MSFT_AdfsLdapMapping } } elseif ($individualRule[0] -eq '@RuleTemplate = "EmitGroupClaims"') { Write-Debug 'Processing EmitGroupClaims Template Rule' $groupSid = $individualRule[2].Split('"')[3] $issuanceTransformRule = @{ TemplateName = 'EmitGroupClaims' Name = $individualRule[1].split('"')[1] GroupName = Get-AdGroupNameFromSid -Sid $groupSid OutgoingClaimType = $individualRule[3].split('"')[1] OutgoingClaimValue = $individualRule[3].split('"')[3] } } else { Write-Debug 'Processing CustomClaims Template Rule' $issuanceTransformRule = @{ TemplateName = 'CustomClaims' Name = $individualRule[0].split('"')[1] CustomRule = $individualRule[1..($individualRule.count)] | Out-String } } $MSFT_AdfsIssuanceTransformRule += New-CimInstance -ClassName MSFT_AdfsIssuanceTransformRule ` -Namespace root/microsoft/Windows/DesiredStateConfiguration ` -Property $issuanceTransformRule -ClientOnly } $MSFT_AdfsIssuanceTransformRule } } function Compare-IssuanceTransformRule { <# .SYNOPSIS Compare two Issuance Transform Rules #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Object[]] $CurrentValue, [Parameter(Mandatory = $true)] [System.Object[]] $DesiredValue ) $parameterState = @{ ParameterName = 'IssuanceTransformRules' Expected = $DesiredValue Actual = $CurrentValue InDesiredState = $true } if ([System.String]::IsNullOrEmpty($CurrentValue)) { $parameterState.InDesiredState = $false } else { for ($index = 0; $index -lt $DesiredValue.Count; $index++) { if ($DesiredValue[$index].TemplateName -eq $CurrentValue[$index].TemplateName) { if ($DesiredValue[$index].TemplateName -eq 'LdapClaims') { Write-Debug -Message "Comparing LdapClaims Rule $($CurrentValue[$index].Name)" if (Compare-Object -ReferenceObject $CurrentValue[$index] -DifferenceObject $DesiredValue[$index] ` -Property $CurrentValue[$index].CimInstanceProperties.Name) { Write-Debug -Message "Comparing LdapClaims False" $parameterState.InDesiredState = $false break } if (Compare-Object -ReferenceObject $CurrentValue[$index].LdapMapping -DifferenceObject $DesiredValue[$index].LdapMapping ` -Property $CurrentValue[$index].LdapMapping.CimInstanceProperties.Name) { Write-Debug -Message "Comparing LdapClaims LdapMapping False" $parameterState.InDesiredState = $false break } } elseif ($DesiredValue[$index].TemplateName -eq 'EmitGroupClaims') { Write-Debug -Message "Comparing EmitGroupClaims Rule $($CurrentValue[$index].Name)" if (Compare-Object -ReferenceObject $CurrentValue[$index] -DifferenceObject $DesiredValue[$index] ` -Property $CurrentValue[$index].CimInstanceProperties.Name) { Write-Debug -Message "Comparing EmitGroupClaims False" $parameterState.InDesiredState = $false break } } elseif ($DesiredValue[$index].TemplateName -eq 'CustomClaims') { Write-Debug -Message "Comparing CustomClaims Rule $($CurrentValue[$index].Name)" $CurrentCustomRule = ($CurrentValue[$index].CustomRule -split '\r?\n' | Out-String).Trim() $DesiredCustomRule = ($DesiredValue[$index].CustomRule -split '\r?\n' | Out-String).Trim() if ($CurrentCustomRule -ne $DesiredCustomRule -or $CurrentValue[$index].Name -ne $DesiredValue[$index].Name) { Write-Debug -Message "Comparing CustomClaims False" $parameterState.InDesiredState = $false break } } else { $errorMessage = ($script:LocalizedData.UnknownIssuanceTransformRuleTemplateError -f $DesiredValue[$index].TemplateName) New-InvalidOperationException -Message $errorMessage } } else { Write-Debug -Message "TemplateName Changed" $parameterState.InDesiredState = $false break } } } return $parameterState } function Get-AdGroupNameFromSid { <# .SYNOPSIS Get an Active Directory group name from a SID #> [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [System.String] $Sid ) $groupObject = ([System.DirectoryServices.DirectorySearcher]"(&(objectClass=group)(objectSid=$sid))").FindOne() if ($groupObject) { return $groupObject.GetDirectoryEntry().Name } else { $errorMessage = $script:localizedData.ActiveDirectoryGroupNotFoundFromSidError -f $Sid New-ObjectNotFoundException -Message $errorMessage } } function Get-AdGroupSid { <# .SYNOPSIS Get the SID of an Active Directory group #> [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [System.String] $GroupName ) Write-Debug "Getting SID for Active Directory group $GroupName" $adGroup = ([System.DirectoryServices.DirectorySearcher]"(&(objectClass=group)(name=$GroupName))").FindOne() if ($adGroup) { $binarySid = $adGroup.GetDirectoryEntry().ObjectSid.Value $stringSid = ([System.Security.Principal.SecurityIdentifier]::new($binarysid, 0)).Value return $stringSid } else { $errorMessage = $script:localizedData.ActiveDirectoryGroupNotFoundError -f $GroupName New-ObjectNotFoundException -Message $errorMessage } } $script:localizedData = Get-LocalizedData -ResourceName 'AdfsDsc.Common' -ScriptRoot $PSScriptRoot $script:adfsServiceName = 'adfssrv' Export-ModuleMember -Function @( 'Get-LocalizedData' 'New-InvalidArgumentException' 'New-InvalidOperationException' 'New-ObjectNotFoundException' 'New-InvalidResultException' 'New-NotImplementedException' 'ConvertTo-TimeSpan' 'ConvertFrom-TimeSpan' 'Compare-ResourcePropertyState' 'New-CimCredentialInstance' 'Assert-Module' 'Assert-DomainMember' 'Assert-AdfsService' 'Assert-Command' 'Assert-GroupServiceAccount' 'Get-AdfsConfigurationStatus' 'Get-ObjectType' 'ConvertTo-IssuanceTransformRule' 'ConvertFrom-IssuanceTransformRule' 'Compare-IssuanceTransformRule' ) |