Public/Get-UserProvisioningSyncSessionOperation.ps1

function Get-UserProvisioningSyncSessionOperation {
    [CmdletBinding()]
    param ()

    Begin {
        Write-ConnectorVerbose "Getting all users from all included OUs..."
        $Script:UserMap = @{}
        $Script:IncludedOUs | ForEach-Object {
            Write-ConnectorVerbose "Getting users from OU: $_"
            Get-ADUser -SearchBase $_ -LDAPFilter "($($Script:LifeCycleStateAttribute)=*)" -Properties * | ForEach-Object { 
                $State = $_.($Script:LifeCycleStateAttribute) | ConvertFrom-Json
                
                if ($State.identifier) {
                    if ($Script:UserMap.ContainsKey($State.identifier)) {
                        Write-Warning "Multiple users with the same lifecycle state identifier '$($State.identifier)' found. This may lead to unexpected behavior. Please ensure that the attribute '$($Script:LifeCycleStateAttribute)' contains unique identifiers for each user."
                    }
                    
                    $Script:UserMap[$State.identifier] = $_
                }
                else {
                    Write-warning "User $($_.DistinguishedName) does not have a valid lifecycle state in attribute '$($Script:LifeCycleStateAttribute)'. Skipping user."
                }
            }
        }

        Write-ConnectorVerbose "Total users with valid lifecycle state found in included OUs: $($Script:UserMap.Count)"
    }

    Process {
        $Script:SyncSessionObjects.GetEnumerator() | ForEach-Object {
            $Identifier = $_.Key
            $InputObject = $_.Value

            if ($Script:UserMap.ContainsKey($Identifier)) {
                $ExistingUser = $Script:UserMap[$Identifier]

                # Compare the existing user with the input object to determine if an update is needed
                    
                $Parameters = @{}

                $InputObject.Keys | Foreach-Object {
                    if ($_ -eq "manager") {
                        if ($null -eq $InputObject.manager) {
                            if ($ExistingUser.manager) {
                                Write-Verbose "Manager differs for user with identifier '$Identifier'. Manager will be cleared."
                                $Parameters["Clear"] ??= @()
                                $Parameters["Clear"] += "manager"
                            }
                        }
                        elseif ($Script:UserMap.ContainsKey($InputObject.manager)) {
                            if ($Script:UserMap[$InputObject.manager].DistinguishedName -ne $ExistingUser.manager) {
                                $Parameters["Replace"] ??= @{}
                                $Parameters["Replace"][$_] = $Script:UserMap[$InputObject.manager].DistinguishedName
                            }
                        }
                        else {
                            Write-Warning "Manager with identifier '$($InputObject.manager)' not found in existing users. Manager will not be updated for user with identifier '$Identifier'."
                        }
                    }
                    elseif ($_ -eq "enabled") {
                        if ($InputObject.enabled -ne $ExistingUser.Enabled) {
                            Write-Verbose "Enabled state differs for user with identifier '$Identifier'. Enabled will be updated to '$($InputObject.enabled)'."
                            $Parameters["Enabled"] = $InputObject.enabled
                        }
                        
                    }
                    elseif ($ExistingUser.$_ -cne $InputObject.$_) {
                        if ($null -eq $InputObject.$_) {
                            $Parameters["Clear"] ??= @()
                            $Parameters["Clear"] += $_
                        }
                        else {
                            $Parameters["Replace"] ??= @{}
                            $Parameters["Replace"][$_] = $InputObject.$_
                        }
                    }
                }

                $State = $ExistingUser."$($Script:LifeCycleStateAttribute)" | ConvertFrom-Json
                if ($State.disabled) {
                    $State.disabled = $null
                    $Parameters["Replace"] ??= @{}
                    $Parameters["Replace"]["$($Script:LifeCycleStateAttribute)"] = $State | ConvertTo-Json -Compress
                }

                if ($Parameters.Count -gt 0) {
                    Write-Verbose "User with identifier '$Identifier' exists but has differences. An update operation will be planned."
                    New-UserProvisioningSyncSessionOperation -Action "Set-ADUser" -Identity $ExistingUser.ObjectGUID.ToString() -Parameters $Parameters
                }
                else {
                    # User exists and is the same, no operation needed
                    Write-Debug "User with identifier '$Identifier' already exists and is up to date. No operation will be planned."
                }
            }
            else {
                # User does not exist, create needed
                Write-Verbose "User with identifier '$Identifier' does not exist. A create operation will be planned."
                $Parameters = @{
                    OtherAttributes = @{}
                }

                $InputObject.Keys | ForEach-Object {
                    if ($_ -eq "manager") {
                        if ($InputObject.manager) {
                            if ($Script:UserMap.ContainsKey($InputObject.manager)) {
                                $Parameters["OtherAttributes"][$_] = $Script:UserMap[$InputObject.manager].DistinguishedName
                            }
                            else {
                                Write-Warning "Manager with identifier '$($InputObject.manager)' not found in existing users. Manager will not be set for user with identifier '$Identifier'."
                            }
                        }
                    }
                    elseif ($_ -eq "enabled") {
                        $Parameters["Enabled"] = $InputObject.enabled ?? $false
                    }
                    elseif ($_ -eq "ou") {
                        $Parameters["Path"] = $InputObject.ou
                    }
                    elseif ($InputObject.$_) {
                        $Parameters["OtherAttributes"][$_] = $InputObject.$_
                    }
                }

                $Parameters["OtherAttributes"][$Script:LifeCycleStateAttribute] = @{
                    identifier = $Identifier
                    disabled   = $null
                } | ConvertTo-Json -Compress

                if (!$Parameters.ContainsKey("SamAccountName")) {
                    if (!$InputObject.givenName -or !$InputObject.sn) {
                        Write-Warning "Cannot generate SamAccountName for user with identifier '$Identifier' because givenName or sn is missing."
                    }
                    else {
                        Get-UserProvisioningSamAccountName -givenName $InputObject.givenName -sn $InputObject.sn | Where-Object {
                            $s = $_
                            !(Get-ADUser -LDAPFilter "(sAMAccountName=$s)" -ErrorAction SilentlyContinue)
                        } | Select-Object -First 1 | ForEach-Object {
                            $Parameters["SamAccountName"] = $_
                        }

                        if (!$Parameters["SamAccountName"]) {
                            $Parameters["SamAccountName"] = (New-Guid).ToString().Replace("-", "").Substring(0, 20)
                        }
                    }
                }

                if (!$Parameters.ContainsKey("Name")) {
                    if ($Parameters["SamAccountName"]) {
                        $Parameters["Name"] = $Parameters["SamAccountName"]
                    }
                    else {
                        $Parameters["Name"] = $Identifier
                    }
                }

                if (!$Parameters.ContainsKey("Path")) {
                    $Parameters["Path"] = $Script:DefaultDestinationOU
                }
                
                New-UserProvisioningSyncSessionOperation -Action "New-ADUser" -Identity $Identifier -Parameters $Parameters
            }
        }

        # Any remaining users in the map are not present in the input objects and should be scheduld by deletion, by:
        # Checking the life cycle attribute 'disabled' property
        # If the date is more than $Script:DeleteUsersAfterDays (set during connect) days ago - create a Remove-ADObject operation
        # Else if the 'disabled' time stamp is not there, create a Set-ADUser operation that updates the life cycle attribute with a 'disabled'timestamp to now()
        
        $Script:UserMap.GetEnumerator() | 
        Where-Object { -not $Script:SyncSessionObjects.ContainsKey($_.Key) } |
        ForEach-Object {
            $Identifier = $_.Key
            $ExistingUser = $_.Value
            $State = $ExistingUser.($Script:LifeCycleStateAttribute) | ConvertFrom-Json

            if ($null -ne $State.disabled) {
                $DisabledDate = Get-Date $State.disabled

                if ($ExistingUser.Enabled) {
                    Write-Warning "User with identifier '$Identifier' is enabled but has a disabled timestamp in the lifecycle state attribute. This is an inconsistent state. A disable operation will be planned to correct the inconsistency before any deletion scheduling is done."
                    New-UserProvisioningSyncSessionOperation -Action "Set-ADUser" -Identity $ExistingUser.DistinguishedName -Parameters @{
                        Replace  = @{
                            "$($Script:LifeCycleStateAttribute)" = @{
                                identifier = $Identifier
                                disabled   = (Get-Date).ToString("o")
                            } | ConvertTo-Json -Compress
                        }
                        Enabled = $false
                    }
                } 
                elseif ($DisabledDate -lt (Get-Date).AddDays(-$Script:DeleteUsersAfterDays)) {
                    # User has been disabled for longer than the threshold, schedule for deletion
                    Write-Verbose "User with identifier '$Identifier' has been disabled since '$DisabledDate', which is longer than the threshold of '$($Script:DeleteUsersAfterDays)' days. A delete operation will be planned."
                    New-UserProvisioningSyncSessionOperation -Action "Remove-ADObject" -Identity $ExistingUser.DistinguishedName
                }
                else {
                    # User has been disabled but not long enough, no operation needed
                    Write-Debug "User with identifier '$Identifier' has been disabled since '$DisabledDate', which is not longer than the threshold of '$($Script:DeleteUsersAfterDays)' days. No operation will be planned at this time."
                }
            }
            else {
                # User is not disabled, schedule for disablement by updating the life cycle state attribute with a 'disabled' timestamp to now()
                Write-Verbose "User with identifier '$Identifier' is not disabled but is missing from the input objects. A disable operation will be planned."
                New-UserProvisioningSyncSessionOperation -Action "Set-ADUser" -Identity $ExistingUser.DistinguishedName -Parameters @{
                    Replace  = @{
                        "$($Script:LifeCycleStateAttribute)" = @{
                            identifier = $Identifier
                            disabled   = (Get-Date).ToString("o")
                        } | ConvertTo-Json -Compress
                    }
                    Enabled = $false
                }
            }
        }
    }

    End {

    }
}