AADConnectDsc.psm1

#Region '.\Prefix.ps1' -1

$script:dscResourceCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath 'Modules/DscResource.Common'
Import-Module -Name $script:dscResourceCommonModulePath
#EndRegion '.\Prefix.ps1' 3
#Region '.\Enum\AttributeMappingFlowType.ps1' -1

enum AttributeMappingFlowType
{
    Direct
    Constant
    Expression
}
#EndRegion '.\Enum\AttributeMappingFlowType.ps1' 7
#Region '.\Enum\AttributeValueMergeType.ps1' -1

enum AttributeValueMergeType
{
    Update
    Replace
    MergeCaseInsensitive
    Merge
}
#EndRegion '.\Enum\AttributeValueMergeType.ps1' 8
#Region '.\Enum\ComparisonOperator.ps1' -1

enum ComparisonOperator {
    EQUAL
    NOTEQUAL
    LESSTHAN
    LESSTHAN_OR_EQUAL
    CONTAINS
    NOTCONTAINS
    STARTSWITH
    NOTSTARTSWITH
    ENDSWITH
    NOTENDSWITH
    GREATERTHAN
    GREATERTHAN_OR_EQUAL
    ISNULL
    ISNOTNULL
    ISIN
    ISNOTIN
    ISBITSET
    ISBITNOTSET
    ISMEMBEROF
    ISNOTMEMBEROF
}
#EndRegion '.\Enum\ComparisonOperator.ps1' 23
#Region '.\Enum\Ensure.ps1' -1

enum Ensure {
    Absent
    Present
    Unknown
}
#EndRegion '.\Enum\Ensure.ps1' 6
#Region '.\Classes\AADConnectDirectoryExtensionAttribute.ps1' -1

[DscResource()]
class AADConnectDirectoryExtensionAttribute
{
    [DscProperty(Key = $true)]
    [string]$Name

    [DscProperty(Key = $true)]
    [string]$AssignedObjectClass

    [DscProperty(Mandatory = $true)]
    [string]$Type

    [DscProperty(Mandatory = $true)]
    [bool]$IsEnabled

    [DscProperty()]
    [Ensure]
    $Ensure

    AADConnectDirectoryExtensionAttribute()
    {
        $this.Ensure = 'Present'
    }

    [bool]Test()
    {
        $currentState = Convert-ObjectToHashtable -Object $this.Get()
        $desiredState = Convert-ObjectToHashtable -Object $this

        if ($currentState.Ensure -ne $desiredState.Ensure)
        {
            return $false
        }
        if ($desiredState.Ensure -eq [Ensure]::Absent)
        {
            return $true
        }

        $compare = Test-DscParameterState -CurrentValues $currentState -DesiredValues $desiredState -TurnOffTypeChecking -SortArrayValues

        return $compare
    }

    [AADConnectDirectoryExtensionAttribute]Get()
    {
        $currentState = [AADConnectDirectoryExtensionAttribute]::new()

        $attribute = Get-AADConnectDirectoryExtensionAttribute -Name $this.Name -ErrorAction SilentlyContinue |
            Where-Object { $_.AssignedObjectClass -eq $this.AssignedObjectClass -and $_.Type -eq $this.Type }

        $currentState.Ensure = [Ensure][int][bool]$attribute
        $CurrentState.Name = $this.Name
        $currentState.AssignedObjectClass = $this.AssignedObjectClass
        $currentState.Type = $attribute.Type
        $currentState.IsEnabled = $attribute.IsEnabled

        return $currentState
    }

    [void]Set()
    {
        $param = Convert-ObjectToHashtable $this

        if ($this.Ensure -eq 'Present')
        {
            $cmdet = Get-Command -Name Add-AADConnectDirectoryExtensionAttribute
            $param = Sync-Parameter -Command $cmdet -Parameters $param
            Add-AADConnectDirectoryExtensionAttribute @param -Force
        }
        else
        {
            $cmdet = Get-Command -Name Remove-AADConnectDirectoryExtensionAttribute
            $param = Sync-Parameter -Command $cmdet -Parameters $param
            Remove-AADConnectDirectoryExtensionAttribute @param
        }

    }
}
#EndRegion '.\Classes\AADConnectDirectoryExtensionAttribute.ps1' 79
#Region '.\Classes\AADSyncRule.ps1' -1

[DscResource()]
class AADSyncRule
{
    [DscProperty(Key = $true)]
    [string]$Name

    [DscProperty()]
    [string]$Description

    [DscProperty()]
    [bool]$Disabled

    [DscProperty(NotConfigurable)]
    [string]$Identifier

    [DscProperty(NotConfigurable)]
    [string]$Version

    [DscProperty()]
    [ScopeConditionGroup[]]$ScopeFilter

    [DscProperty()]
    [JoinConditionGroup[]]$JoinFilter

    [DscProperty()]
    [AttributeFlowMapping[]]$AttributeFlowMappings

    [DscProperty(Key = $true)]
    [string]$ConnectorName

    [DscProperty(NotConfigurable)]
    [string]$Connector

    [DscProperty()]
    [int]$Precedence

    [DscProperty()]
    [string]$PrecedenceAfter

    [DscProperty()]
    [string]$PrecedenceBefore

    [DscProperty(Mandatory = $true)]
    [string]$TargetObjectType

    [DscProperty(Mandatory = $true)]
    [string]$SourceObjectType

    [DscProperty(Mandatory = $true)]
    [string]$Direction

    [DscProperty(Mandatory = $true)]
    [string]$LinkType

    [DscProperty()]
    [bool]$EnablePasswordSync

    [DscProperty()]
    [string]$ImmutableTag

    [DscProperty()]
    [bool]$IsStandardRule

    [DscProperty(NotConfigurable)]
    [bool]$IsLegacyCustomRule

    [DscProperty()]
    [Ensure]$Ensure

    AADSyncRule()
    {
        $this.Ensure = 'Present'
    }

    [bool]Test()
    {
        $currentState = $this.Get() | ConvertTo-Yaml | ConvertFrom-Yaml
        $desiredState = $this | ConvertTo-Yaml | ConvertFrom-Yaml

        #Remove all whitespace from expressions in AttributeFlowMappings, otherwise they will not match due to encoding differences
        foreach ($afm in $currentState.AttributeFlowMappings)
        {
            if (-not [string]::IsNullOrEmpty($afm.Expression))
            {
                $afm.Expression = $afm.Expression -replace '\s', ''
            }
        }

        foreach ($afm in $desiredState.AttributeFlowMappings)
        {
            if (-not [string]::IsNullOrEmpty($afm.Expression))
            {
                $afm.Expression = $afm.Expression -replace '\s', ''
            }
        }

        $param = @{
            CurrentValues       = $currentState
            DesiredValues       = $desiredState
            TurnOffTypeChecking = $true
            SortArrayValues     = $true
        }

        $param.ExcludeProperties = if ($this.IsStandardRule)
        {
            'Connector', 'Version', 'Identifier', 'Precedence'
        }
        else
        {
            'Connector', 'Version', 'Identifier'
        }

        $compare = if ($currentState.Ensure -eq $desiredState.Ensure)
        {
            if ($desiredState.Ensure -eq 'Present')
            {
                Write-Verbose "The sync rule '$($this.Name)' exists and should exist, comparing rule with 'Test-DscParameterState'."
                Test-DscParameterState @param -ReverseCheck
            }
            else
            {
                Write-Verbose "The sync rule '$($this.Name)' is absent and should be absent."
                $true
            }
        }
        else
        {
            if ($desiredState.Ensure -eq 'Present')
            {
                Write-Verbose "The sync rule '$($this.Name)' for connector '$($this.ConnectorName)' is absent, but should be present."
            }
            else
            {
                Write-Verbose "The sync rule '$($this.Name)' for connector '$($this.ConnectorName)' is present, but should be absent."
            }
            $false
        }

        return $compare
    }

    [AADSyncRule]Get()
    {
        $syncRule = Get-ADSyncRule -Name $this.Name -ConnectorName $this.ConnectorName

        $currentState = [AADSyncRule]::new()
        $currentState.Name = $this.Name

        if ($syncRule.Count -gt 1)
        {
            Write-Error "There is more than one sync rule with the name '$($this.Name)'."
            $currentState.Ensure = 'Unknown'
            return $currentState
        }

        $currentState.Ensure = [Ensure][int][bool]$syncRule

        $currentState.ConnectorName = (Get-ADSyncConnector | Where-Object Identifier -EQ $syncRule.Connector).Name
        $currentState.Connector = $syncRule.Connector

        $currentState.Description = $syncRule.Description
        $currentState.Disabled = $syncRule.Disabled
        $currentState.Direction = $syncRule.Direction
        $currentState.EnablePasswordSync = $syncRule.EnablePasswordSync
        $currentState.Identifier = $syncRule.Identifier
        $currentState.LinkType = $syncRule.LinkType
        $currentState.Precedence = $syncRule.Precedence

        $currentState.ScopeFilter = @()
        foreach ($scg in $syncRule.ScopeFilter)
        {
            $scg2 = [ScopeConditionGroup]::new()
            foreach ($sc in $scg.ScopeConditionList)
            {
                $sc2 = [ScopeCondition]::new($sc.Attribute, $sc.ComparisonValue, $sc.ComparisonOperator)
                $scg2.ScopeConditionList += $sc2
            }

            $currentState.ScopeFilter += $scg2
        }

        $currentState.JoinFilter = @()
        foreach ($jcg in $syncRule.JoinFilter)
        {
            $jcg2 = [JoinConditionGroup]::new()
            foreach ($jc in $jcg.JoinConditionList)
            {
                $jc2 = [JoinCondition]::new($jc.CSAttribute, $jc.MVAttribute, $jc.CaseSensitive)
                $jcg2.JoinConditionList += $jc2
            }

            $currentState.JoinFilter += $jcg2
        }

        $currentState.AttributeFlowMappings = @()
        foreach ($af in $syncRule.AttributeFlowMappings)
        {
            $af2 = [AttributeFlowMapping]::new()
            $af2.Source = $af.Source[0]
            $af2.Destination = $af.Destination
            $af2.ExecuteOnce = $af.ExecuteOnce
            $af2.FlowType = $af.FlowType
            $af2.ValueMergeType = $af.ValueMergeType
            if ($null -eq $af.Expression)
            {
                $af2.Expression = ''
            }
            else
            {
                $af2.Expression = $af.Expression
            }

            $currentState.AttributeFlowMappings += $af2
        }

        $currentState.SourceObjectType = $syncRule.SourceObjectType
        $currentState.TargetObjectType = $syncRule.TargetObjectType
        $currentState.Version = $syncRule.Version
        $currentState.IsStandardRule = $syncRule.IsStandardRule
        $currentState.IsLegacyCustomRule = $syncRule.IsLegacyCustomRule

        return $currentState
    }

    [void]Set()
    {
        $connectorObject = Get-ADSyncConnector -Name $this.ConnectorName -ErrorAction SilentlyContinue
        if ($null -eq $connectorObject)
        {
            Write-Error "The connector '$($this.ConnectorName)' does not exist."
            return
        }

        $this.Connector = $connectorObject.Identifier
        Write-Verbose "Got connector '$($this.ConnectorName)' for rule '$($this.Name)' with identifier '$($this.Connector)'."

        $existingRule = Get-ADSyncRule -Name $this.Name -ConnectorName $this.ConnectorName
        if ($existingRule)
        {
            Write-Verbose "Got existing rule '$($existingRule.Name)' with identifier '$($existingRule.Identifier)' for connector '$($this.ConnectorName)'."
            $this.Identifier = $existingRule.Identifier
        }
        else
        {
            $this.Identifier = New-Guid2 -InputString "$($this.Name)$($this.ConnectorName)"
            Write-Verbose "No existing rule found with the name '$($this.Name)'. Using identifier '$($this.Identifier)'."
        }

        $desiredState = Convert-ObjectToHashtable -Object $this

        if ($this.Ensure -eq 'Present')
        {
            Write-Verbose "The sync rule '$($this.Name)' should be present for connector '$($this.ConnectorName)'. Proceeding with creation or update."
            if ($this.IsStandardRule)
            {
                if ($null -eq $existingRule)
                {
                    Write-Error "A syncrule defined as 'IsStandardRule' does not exist. It cannot be enabled or disabled."
                    return
                }

                Write-Warning "The only property changed on a standard rule is 'Disabled'. All other configuration drifts will not be corrected."
                $existingRule.Disabled = $this.Disabled
                Write-Verbose "Setting the 'Disabled' property of the rule '$($this.Name)' to '$($this.Disabled)' and calling 'Add-ADSyncRule'."
                $existingRule | Add-ADSyncRule
            }
            else
            {
                if ($existingRule.IsStandardRule)
                {
                    Write-Error 'It is not allowed to modify a standard rule. It can only be enabled or disabled.'
                    return
                }

                $cmdet = Get-Command -Name New-ADSyncRule
                $param = Sync-Parameter -Command $cmdet -Parameters $desiredState
                $rule = New-ADSyncRule @param

                if ($this.ScopeFilter)
                {
                    $i = 0
                    foreach ($scg in $this.ScopeFilter)
                    {
                        Write-Verbose "Processing ScopeConditionList $i"
                        $scopeConditions = foreach ($sc in $scg.ScopeConditionList)
                        {
                            Write-Verbose "Processing ScopeFilter: Attribute = '$($sc.Attribute)', ComparisonValue = '$($sc.ComparisonValue)', ComparisonOperator = '$($sc.ComparisonOperator)'"
                            [Microsoft.IdentityManagement.PowerShell.ObjectModel.ScopeCondition]::new($sc.Attribute, $sc.ComparisonValue, $sc.ComparisonOperator)
                        }
                        Write-Verbose "ScopeConditionList count is $($scopeConditions.Count)"
                        $rule | Add-ADSyncScopeConditionGroup -ScopeConditions $scopeConditions
                        $i++
                    }
                }

                if ($this.JoinFilter)
                {
                    $i = 0
                    foreach ($jcg in $this.JoinFilter)
                    {
                        Write-Verbose "Processing JoinConditionList $i"
                        $joinConditions = foreach ($jc in $jcg.JoinConditionList)
                        {
                            Write-Verbose "Processing JoinFilter: CSAttribute = '$($jc.CSAttribute)', MVAttribute = '$($jc.MVAttribute)', CaseSensitive = '$($jc.CaseSensitive)'"
                            [Microsoft.IdentityManagement.PowerShell.ObjectModel.JoinCondition]::new($jc.CSAttribute, $jc.MVAttribute, $jc.CaseSensitive)
                        }

                        Write-Verbose "JoinConditionList count is $($joinConditions.Count)"
                        $rule | Add-ADSyncJoinConditionGroup -JoinConditions $joinConditions
                    }

                }

                if ($this.AttributeFlowMappings)
                {
                    $i = 0
                    foreach ($af in $this.AttributeFlowMappings)
                    {
                        Write-Verbose "Processing AttributeFlowMapping $i, Source = '$($af.Source)', Destination = '$($af.Destination)', Expression = '$($af.Expression)'"
                        $afHashTable = Convert-ObjectToHashtable -Object $af
                        $param = Sync-Parameter -Command (Get-Command -Name Add-ADSyncAttributeFlowMapping) -Parameters $afHashTable
                        $param.SynchronizationRule = $rule

                        if ([string]::IsNullOrEmpty($param.Expression))
                        {
                            $param.Remove('Expression')
                        }

                        if ([string]::IsNullOrEmpty($param.Source))
                        {
                            $param.Remove('Source')
                        }

                        Add-ADSyncAttributeFlowMapping @param
                    }

                }

                Write-Verbose "Calling 'Add-ADSyncRule' to create or update the rule '$($this.Name)'."
                $rule | Add-ADSyncRule
            }
        }
        else
        {
            if ($existingRule)
            {
                Remove-ADSyncRule -Identifier $this.Identifier
            }
        }
    }
}
#EndRegion '.\Classes\AADSyncRule.ps1' 352
#Region '.\Classes\AttributeFlowMapping.ps1' -1

class AttributeFlowMapping
{
    AttributeFlowMapping()
    {
    }

    [DscProperty(Key)]
    [string]$Destination

    [DscProperty()]
    [bool]$ExecuteOnce

    [DscProperty(Key)]
    [string]$Expression

    [DscProperty(Key)]
    [AttributeMappingFlowType]$FlowType

    [DscProperty(NotConfigurable)]
    [string]$MappingSourceAsString

    [DscProperty(Key)]
    [string]$Source

    [DscProperty()]
    [AttributeValueMergeType]$ValueMergeType
}
#EndRegion '.\Classes\AttributeFlowMapping.ps1' 28
#Region '.\Classes\JoinCondition.ps1' -1

class JoinCondition
{
    [DscProperty()]
    [string]$CSAttribute

    [DscProperty()]
    [string]$MVAttribute

    [DscProperty()]
    [bool]$CaseSensitive

    JoinCondition()
    {
    }

    JoinCondition([string]$CSAttribute, [string]$MVAttribute, [bool]$CaseSensitive)
    {
        $this.CSAttribute = $CSAttribute
        $this.MVAttribute = $MVAttribute
        $this.CaseSensitive = $CaseSensitive
    }
}
#EndRegion '.\Classes\JoinCondition.ps1' 23
#Region '.\Classes\JoinConditionGroup.ps1' -1


class JoinConditionGroup
{
    [DscProperty()]
    [JoinCondition[]]$JoinConditionList

    ScopeConditionGroup()
    {
    }
}
#EndRegion '.\Classes\JoinConditionGroup.ps1' 11
#Region '.\Classes\ScopeCondition.ps1' -1

class ScopeCondition
{
    [DscProperty()]
    [string]$Attribute

    [DscProperty()]
    [string]$ComparisonValue

    [DscProperty()]
    [ComparisonOperator]$ComparisonOperator

    ScopeCondition()
    {
    }

    ScopeCondition([hashtable]$Definition)
    {
        $this.Attribute = $Definition['Attribute']
        $this.ComparisonValue = $Definition['ComparisonValue']
        $this.ComparisonOperator = $Definition['ComparisonOperator']
    }

    ScopeCondition([string]$Attribute, [string]$ComparisonValue, [string]$ComparisonOperator)
    {
        $this.Attribute = $Attribute
        $this.ComparisonValue = $ComparisonValue
        $this.ComparisonOperator = $ComparisonOperator
    }
}
#EndRegion '.\Classes\ScopeCondition.ps1' 30
#Region '.\Classes\ScopeConditionGroup.ps1' -1


class ScopeConditionGroup
{
    [DscProperty()]
    [ScopeCondition[]]$ScopeConditionList

    ScopeConditionGroup()
    {
    }
}
#EndRegion '.\Classes\ScopeConditionGroup.ps1' 11
#Region '.\Private\New-Guid2.ps1' -1

function New-Guid2 {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $InputString
    )

    $md5 = [System.Security.Cryptography.MD5]::Create()

    $hash = $md5.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($InputString))
    return [System.Guid]::new($hash).Guid
}
#EndRegion '.\Private\New-Guid2.ps1' 14
#Region '.\Public\Add-AADConnectDirectoryExtensionAttribute.ps1' -1

<#
.SYNOPSIS
    Adds a directory extension attribute to Azure AD Connect configuration.
 
.DESCRIPTION
    The Add-AADConnectDirectoryExtensionAttribute function adds a new directory extension attribute
    to the Azure AD Connect global settings. Directory extension attributes allow you to extend
    the schema of Azure AD objects with custom attributes that can be synchronized from on-premises
    Active Directory.
 
    This function supports two parameter sets: specifying individual properties or providing a
    complete attribute string. It includes validation and conflict resolution capabilities.
 
    This function requires Windows PowerShell 5.1 and does not work with PowerShell 7.
 
.PARAMETER Name
    Specifies the name of the directory extension attribute to add. The name should be unique
    within the object class and follow Azure AD naming conventions.
 
.PARAMETER Type
    Specifies the data type of the directory extension attribute. Common types include:
    - String: Text data
    - Integer: Numeric data
    - Boolean: True/False values
    - DateTime: Date and time values
 
.PARAMETER AssignedObjectClass
    Specifies the object class to which this attribute will be assigned. Common values include:
    - user: For user objects
    - group: For group objects
    - contact: For contact objects
    - device: For device objects
 
.PARAMETER IsEnabled
    Specifies whether the directory extension attribute is enabled for synchronization.
    Set to $true to enable or $false to disable.
 
.PARAMETER FullAttributeString
    Specifies a complete attribute definition string in the format:
    "attributeName.objectClass.dataType.enabledStatus"
    For example: "employeeNumber.user.String.True"
 
.PARAMETER Force
    Forces the addition of the attribute even if a conflicting attribute with the same name
    but different type exists. When specified, the existing conflicting attribute is removed.
 
.EXAMPLE
    Add-AADConnectDirectoryExtensionAttribute -Name "employeeNumber" -Type "String" -AssignedObjectClass "user" -IsEnabled $true
 
    Adds an employee number attribute for user objects as a string type.
 
.EXAMPLE
    Add-AADConnectDirectoryExtensionAttribute -FullAttributeString "departmentCode.user.String.True"
 
    Adds a department code attribute using the full attribute string format.
 
.EXAMPLE
    Add-AADConnectDirectoryExtensionAttribute -Name "badgeNumber" -Type "Integer" -AssignedObjectClass "user" -IsEnabled $true -Force
 
    Adds a badge number attribute, replacing any existing conflicting attribute with the same name.
 
.EXAMPLE
    Get-Content "attributes.txt" | ForEach-Object { Add-AADConnectDirectoryExtensionAttribute -FullAttributeString $_ }
 
    Adds multiple attributes from a text file, with each line containing a full attribute string.
 
.INPUTS
    String. You can pipe attribute strings to this function when using the FullAttributeString parameter.
 
.OUTPUTS
    None. This function does not return objects but modifies Azure AD Connect global settings.
 
.NOTES
    - This function requires Windows PowerShell 5.1 and does not work with PowerShell 7
    - Requires Azure AD Connect to be installed and the ADSync module to be available
    - Changes take effect immediately but may require synchronization cycle restart
    - Use Get-AADConnectDirectoryExtensionAttribute to verify the attribute was added successfully
    - Directory extension attributes are permanent once synchronized to Azure AD
 
.LINK
    https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-sync-feature-directory-extensions
 
.COMPONENT
    AADConnectDsc
 
.FUNCTIONALITY
    Azure AD Connect Directory Extension Attribute Management
#>

function Add-AADConnectDirectoryExtensionAttribute
{
    [CmdletBinding(DefaultParameterSetName = 'ByProperties')]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByProperties')]
        [string]$Name,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByProperties')]
        [string]$Type,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByProperties')]
        [string]$AssignedObjectClass,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByProperties')]
        [bool]$IsEnabled,

        [Parameter(Mandatory = $true, ParameterSetName = 'SingleObject')]
        [string]$FullAttributeString,

        [Parameter()]
        [switch]$Force
    )

    process
    {
        $currentAttributes = Get-AADConnectDirectoryExtensionAttribute

        if ($FullAttributeString)
        {
            $attributeValues = $FullAttributeString -split '\.'
            if ($attributeValues.Count -ne 4)
            {
                Write-Error "The attribute string did not have the correct format. Make sure it is like 'attributeName.group.String.True'"
                return
            }
            $Name = $attributeValues[0]
            $AssignedObjectClass = $attributeValues[1]
            $Type = $attributeValues[2]
            $IsEnabled = $attributeValues[3]
        }

        if ($currentAttributes | Where-Object {
                $_.Name -eq $Name -and
                $_.AssignedObjectClass -eq $AssignedObjectClass -and
                $_.Type -eq $Type -and
                $_.IsEnabled -eq $IsEnabled
            })
        {
            Write-Error "The attribute '$Name' with the type '$Type' assigned to the class '$AssignedObjectClass' is already defined."
            return
        }

        if (($existingAttribute = $currentAttributes | Where-Object {
                    $_.Name -eq $Name -and
                    $_.Type -ne $Type
                }) -and -not $Force)
        {
            Write-Error "The attribute '$Name' is already defined with the type '$($existingAttribute.Type)'."
            return
        }
        else
        {
            $existingAttribute | Remove-AADConnectDirectoryExtensionAttribute
        }

        $settings = Get-ADSyncGlobalSettings
        $attributeParameter = $settings.Parameters | Where-Object Name -EQ Microsoft.OptionalFeature.DirectoryExtensionAttributes
        $currentAttributeList = $attributeParameter.Value -split ','

        $newAttributeString = "$Name.$AssignedObjectClass.$Type.$IsEnabled"
        $currentAttributeList += $newAttributeString

        $attributeParameter.Value = $currentAttributeList -join ','
        $settings.Parameters.AddOrReplace($attributeParameter)

        Set-ADSyncGlobalSettings -GlobalSettings $settings | Out-Null
    }
}
#EndRegion '.\Public\Add-AADConnectDirectoryExtensionAttribute.ps1' 167
#Region '.\Public\Convert-ObjectToHashtable.ps1' -1

<#
.SYNOPSIS
    Converts a PowerShell object to a hashtable.
 
.DESCRIPTION
    The Convert-ObjectToHashtable function converts any PowerShell object to a hashtable by
    extracting all properties and their values. This utility function is commonly used in
    DSC configurations and Azure AD Connect management scenarios where hashtable representations
    of objects are needed for parameter passing or configuration storage.
 
    The function filters out properties with null values to create a clean hashtable with only
    meaningful data. This is particularly useful when working with Azure AD Connect objects
    that may have many optional properties.
 
    This function works with both Windows PowerShell 5.1 and PowerShell 7.
 
.PARAMETER Object
    Specifies the PowerShell object to convert to a hashtable. The object can be of any type
    that has properties accessible through the PSObject.Properties collection.
 
.EXAMPLE
    $syncRule = Get-ADSyncRule -Name "In from AD - User Common"
    $hashtable = Convert-ObjectToHashtable -Object $syncRule
 
    Converts an Azure AD Connect synchronization rule object to a hashtable.
 
.EXAMPLE
    Get-ADSyncRule | Select-Object -First 1 | Convert-ObjectToHashtable
 
    Retrieves a synchronization rule and converts it to a hashtable using pipeline input.
 
.EXAMPLE
    $user = [PSCustomObject]@{
        Name = "John Doe"
        Email = "john.doe@contoso.com"
        Department = $null
        Enabled = $true
    }
    $hashtable = Convert-ObjectToHashtable -Object $user
    # Results in: @{ Name = "John Doe"; Email = "john.doe@contoso.com"; Enabled = $true }
 
    Converts a custom object to a hashtable, excluding null properties.
 
.EXAMPLE
    $config = @{
        SyncRules = Get-ADSyncRule | ForEach-Object { Convert-ObjectToHashtable $_ }
    }
 
    Creates a configuration hashtable containing all synchronization rules as hashtables.
 
.INPUTS
    Object. You can pipe any PowerShell object to Convert-ObjectToHashtable.
 
.OUTPUTS
    Hashtable. Returns a hashtable containing all non-null properties and their values.
 
.NOTES
    - This function works with both Windows PowerShell 5.1 and PowerShell 7
    - Properties with null values are excluded from the resulting hashtable
    - Complex nested objects are included as-is (not recursively converted)
    - The function is optimized for performance and memory efficiency
    - Useful for DSC configurations and Azure AD Connect object manipulation
 
.LINK
    https://docs.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-hashtable
 
.COMPONENT
    AADConnectDsc
 
.FUNCTIONALITY
    PowerShell Object Utilities
#>

function Convert-ObjectToHashtable
{
    [OutputType([hashtable])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object]$Object
    )

    process
    {
        $hashtable = @{ }

        foreach ($property in $Object.PSObject.Properties.Where({ $null -ne $_.Value }))
        {
            $hashtable.Add($property.Name, $property.Value)
        }

        $hashtable
    }
}
#EndRegion '.\Public\Convert-ObjectToHashtable.ps1' 93
#Region '.\Public\Get-AADConnectDirectoryExtensionAttribute.ps1' -1

<#
.SYNOPSIS
    Retrieves directory extension attributes from Azure AD Connect configuration.
 
.DESCRIPTION
    The Get-AADConnectDirectoryExtensionAttribute function retrieves directory extension attributes
    that are currently configured in Azure AD Connect global settings. These attributes represent
    schema extensions that allow synchronization of custom attributes from on-premises Active Directory
    to Azure AD.
 
    The function can retrieve all directory extension attributes or filter by a specific attribute name.
    Each returned object contains the attribute name, data type, assigned object class, and enabled status.
 
    This function requires Windows PowerShell 5.1 and does not work with PowerShell 7.
 
.PARAMETER Name
    Specifies the name of a specific directory extension attribute to retrieve. If not specified,
    all directory extension attributes are returned. Supports wildcard patterns.
 
.EXAMPLE
    Get-AADConnectDirectoryExtensionAttribute
 
    Retrieves all directory extension attributes currently configured in Azure AD Connect.
 
.EXAMPLE
    Get-AADConnectDirectoryExtensionAttribute -Name "employeeNumber"
 
    Retrieves the directory extension attribute named "employeeNumber" if it exists.
 
.EXAMPLE
    Get-AADConnectDirectoryExtensionAttribute -Name "employee*"
 
    Retrieves all directory extension attributes with names starting with "employee".
 
.EXAMPLE
    $attributes = Get-AADConnectDirectoryExtensionAttribute
    $attributes | Where-Object Type -eq "String"
 
    Retrieves all directory extension attributes and filters for those with String data type.
 
.INPUTS
    None. You cannot pipe objects to Get-AADConnectDirectoryExtensionAttribute.
 
.OUTPUTS
    PSCustomObject. Returns objects with the following properties:
    - Name: The attribute name
    - Type: The data type (String, Integer, Boolean, DateTime, etc.)
    - AssignedObjectClass: The object class (user, group, contact, device, etc.)
    - IsEnabled: Whether the attribute is enabled for synchronization
 
.NOTES
    - This function requires Windows PowerShell 5.1 and does not work with PowerShell 7
    - Requires Azure AD Connect to be installed and the ADSync module to be available
    - Returns an empty result if no directory extension attributes are configured
    - The returned objects can be used as input for other directory extension attribute functions
 
.LINK
    https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-sync-feature-directory-extensions
 
.COMPONENT
    AADConnectDsc
 
.FUNCTIONALITY
    Azure AD Connect Directory Extension Attribute Management
#>

function Get-AADConnectDirectoryExtensionAttribute
{
    param (
        [Parameter()]
        [string]$Name
    )

    $settings = Get-ADSyncGlobalSettings
    $attributeParameter = $settings.Parameters | Where-Object Name -EQ Microsoft.OptionalFeature.DirectoryExtensionAttributes

    $attributes = $attributeParameter.Value -split ','

    if (-not $attributes)
    {
        return
    }

    if ($Name)
    {
        $attributes = $attributes | Where-Object { $_ -like "$Name.*" }
        if (-not $attributes)
        {
            Write-Error "The attribute '$Name' is not defined."
            return
        }
    }

    foreach ($attribute in $attributes)
    {
        $attribute = $attribute -split '\.'
        [pscustomobject]@{
            Name                = $attribute[0]
            Type                = $attribute[2]
            AssignedObjectClass = $attribute[1]
            IsEnabled           = $attribute[3]
        }
    }
}
#EndRegion '.\Public\Get-AADConnectDirectoryExtensionAttribute.ps1' 104
#Region '.\Public\Get-ADSyncRule.ps1' -1

<#
.SYNOPSIS
    Retrieves Azure AD Connect synchronization rules with enhanced filtering capabilities.
 
.DESCRIPTION
    The Get-ADSyncRule function provides a wrapper around the native ADSync\Get-ADSyncRule cmdlet,
    adding enhanced filtering capabilities by name and connector. This function supports multiple
    parameter sets for flexible rule retrieval and is designed to work with Windows PowerShell 5.1.
 
    This function is part of the AADConnectDsc module and requires an active Azure AD Connect
    installation with the ADSync PowerShell module available.
 
.PARAMETER Name
    Specifies the name of the synchronization rule to retrieve. When used alone, it searches
    across all connectors. When used with ConnectorName, it searches within the specified connector.
 
.PARAMETER Identifier
    Specifies the unique identifier (GUID) of the synchronization rule to retrieve.
    When specified, all other parameters are ignored.
 
.PARAMETER ConnectorName
    Specifies the name of the connector to filter synchronization rules.
    Can be used alone to get all rules for a connector, or with Name for specific rule lookup.
 
.EXAMPLE
    Get-ADSyncRule
 
    Retrieves all synchronization rules from Azure AD Connect.
 
.EXAMPLE
    Get-ADSyncRule -Name "In from AD - User Common"
 
    Retrieves the synchronization rule with the specified name from any connector.
 
.EXAMPLE
    Get-ADSyncRule -Identifier "12345678-1234-1234-1234-123456789012"
 
    Retrieves the synchronization rule with the specified GUID identifier.
 
.EXAMPLE
    Get-ADSyncRule -ConnectorName "contoso.com"
 
    Retrieves all synchronization rules associated with the specified connector.
 
.EXAMPLE
    Get-ADSyncRule -Name "In from AD - User Common" -ConnectorName "contoso.com"
 
    Retrieves the synchronization rule with the specified name from the specified connector.
 
.INPUTS
    None. You cannot pipe objects to Get-ADSyncRule.
 
.OUTPUTS
    Microsoft.IdentityManagement.PowerShell.ObjectModel.SynchronizationRule
    Returns synchronization rule objects that match the specified criteria.
 
.NOTES
    - This function requires Windows PowerShell 5.1 and does not work with PowerShell 7
    - Requires Azure AD Connect to be installed and the ADSync module to be available
    - The function provides enhanced error handling and parameter validation
    - Multiple parameter sets allow for flexible rule retrieval scenarios
 
.LINK
    https://docs.microsoft.com/en-us/azure/active-directory/hybrid/reference-connect-sync-functions-reference
 
.COMPONENT
    AADConnectDsc
 
.FUNCTIONALITY
    Azure AD Connect Synchronization Rule Management
#>

function Get-ADSyncRule
{
    [CmdletBinding(DefaultParameterSetName = 'ByName')]
    param (
        [Parameter(ParameterSetName = 'ByName')]
        [Parameter(Mandatory = $true, ParameterSetName = 'ByNameAndConnector')]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'ByIdentifier')]
        [guid]
        $Identifier,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByNameAndConnector')]
        [Parameter(Mandatory = $true, ParameterSetName = 'ByConnector')]
        [string]
        $ConnectorName
    )

    $connectors = Get-ADSyncConnector

    if ($PSCmdlet.ParameterSetName -eq 'ByIdentifier')
    {
        ADSync\Get-ADSyncRule -Identifier $Identifier
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'ByName')
    {
        if ($Name)
        {
            ADSync\Get-ADSyncRule | Where-Object Name -EQ $Name
        }
        else
        {
            ADSync\Get-ADSyncRule
        }
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'ByConnector')
    {
        $connector = $connectors | Where-Object Name-eq $ConnectorName
        ADSync\Get-ADSyncRule | Where-Object Connector -EQ $connector.Identifier
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'ByNameAndConnector')
    {
        $connector = $connectors | Where-Object Name -EQ $ConnectorName
        if ($null -eq $connector)
        {
            Write-Error "The connector '$ConnectorName' does not exist"
            return
        }
        ADSync\Get-ADSyncRule | Where-Object { $_.Name -eq $Name -and $_.Connector -eq $connector.Identifier }
    }
    else
    {
        ADSync\Get-ADSyncRule
    }
}
#EndRegion '.\Public\Get-ADSyncRule.ps1' 128
#Region '.\Public\Remove-AADConnectDirectoryExtensionAttribute.ps1' -1

<#
.SYNOPSIS
    Removes a directory extension attribute from Azure AD Connect configuration.
 
.DESCRIPTION
    The Remove-AADConnectDirectoryExtensionAttribute function removes a directory extension attribute
    from the Azure AD Connect global settings. This function allows you to clean up unused or
    incorrectly configured directory extension attributes from the synchronization configuration.
 
    The function supports two parameter sets: specifying individual properties or providing a
    complete attribute string. It includes validation to ensure the attribute exists before removal.
 
    WARNING: Removing a directory extension attribute that is actively used in synchronization
    rules may cause synchronization errors. Ensure the attribute is not referenced before removal.
 
    This function requires Windows PowerShell 5.1 and does not work with PowerShell 7.
 
.PARAMETER Name
    Specifies the name of the directory extension attribute to remove. Must match exactly with
    an existing attribute name.
 
.PARAMETER Type
    Specifies the data type of the directory extension attribute to remove. Must match exactly
    with the existing attribute's type (String, Integer, Boolean, DateTime, etc.).
 
.PARAMETER AssignedObjectClass
    Specifies the object class of the directory extension attribute to remove. Must match exactly
    with the existing attribute's object class (user, group, contact, device, etc.).
 
.PARAMETER FullAttributeString
    Specifies a complete attribute definition string in the format:
    "attributeName.objectClass.dataType.enabledStatus"
    For example: "employeeNumber.user.String.True"
 
.EXAMPLE
    Remove-AADConnectDirectoryExtensionAttribute -Name "employeeNumber" -Type "String" -AssignedObjectClass "user"
 
    Removes the employee number directory extension attribute for user objects.
 
.EXAMPLE
    Remove-AADConnectDirectoryExtensionAttribute -FullAttributeString "departmentCode.user.String.True"
 
    Removes the department code directory extension attribute using the full attribute string format.
 
.EXAMPLE
    Get-AADConnectDirectoryExtensionAttribute -Name "obsolete*" | Remove-AADConnectDirectoryExtensionAttribute
 
    Removes all directory extension attributes with names starting with "obsolete".
 
.EXAMPLE
    $attribute = Get-AADConnectDirectoryExtensionAttribute -Name "tempAttribute"
    if ($attribute) {
        Remove-AADConnectDirectoryExtensionAttribute -Name $attribute.Name -Type $attribute.Type -AssignedObjectClass $attribute.AssignedObjectClass
    }
 
    Safely removes a directory extension attribute after verifying it exists.
 
.INPUTS
    PSCustomObject. You can pipe directory extension attribute objects from Get-AADConnectDirectoryExtensionAttribute.
 
.OUTPUTS
    None. This function does not return objects but modifies Azure AD Connect global settings.
 
.NOTES
    - This function requires Windows PowerShell 5.1 and does not work with PowerShell 7
    - Requires Azure AD Connect to be installed and the ADSync module to be available
    - Changes take effect immediately but may require synchronization cycle restart
    - Verify that the attribute is not used in synchronization rules before removal
    - Use Get-AADConnectDirectoryExtensionAttribute to verify the attribute was removed successfully
    - Removed attributes cannot be recovered; back up configuration before making changes
 
.LINK
    https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-sync-feature-directory-extensions
 
.COMPONENT
    AADConnectDsc
 
.FUNCTIONALITY
    Azure AD Connect Directory Extension Attribute Management
#>

function Remove-AADConnectDirectoryExtensionAttribute
{
    [CmdletBinding(DefaultParameterSetName = 'ByProperties')]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByProperties')]
        [string]$Name,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByProperties')]
        [string]$Type,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByProperties')]
        [string]$AssignedObjectClass,

        [Parameter(Mandatory = $true, ParameterSetName = 'SingleObject')]
        $FullAttributeString
    )

    process
    {
        $currentAttributes = Get-AADConnectDirectoryExtensionAttribute

        if ($FullAttributeString)
        {
            $attributeValues = $FullAttributeString -split '\.'
            if ($attributeValues.Count -ne 4)
            {
                Write-Error "The attribute string did not have the correct format. Make sure it is like 'attributeName.group.String.True'".
                return
            }
            $Name = $attributeValues[0]
            $AssignedObjectClass = $attributeValues[1]
            $Type = $attributeValues[2]
            $IsEnabled = $attributeValues[3]
        }

        if (-not ($existingAttribute = $currentAttributes | Where-Object {
                    $_.Name -eq $Name -and
                    $_.AssignedObjectClass -eq $AssignedObjectClass -and
                    $_.Type -eq $Type
                }))
        {
            Write-Error "The attribute '$Name' with the type '$Type' assigned to the class '$AssignedObjectClass' is not defined."
            return
        }

        $settings = Get-ADSyncGlobalSettings
        $attributeParameter = $settings.Parameters | Where-Object Name -EQ Microsoft.OptionalFeature.DirectoryExtensionAttributes
        $currentAttributeList = $attributeParameter.Value -split ','

        $attributeStringToRemove = "$($existingAttribute.Name).$($existingAttribute.AssignedObjectClass).$($existingAttribute.Type).$($existingAttribute.IsEnabled)"
        $currentAttributeList = $currentAttributeList -ne $attributeStringToRemove

        $attributeParameter.Value = $currentAttributeList -join ','
        $settings.Parameters.AddOrReplace($attributeParameter)

        Set-ADSyncGlobalSettings -GlobalSettings $settings | Out-Null
    }
}
#EndRegion '.\Public\Remove-AADConnectDirectoryExtensionAttribute.ps1' 139