Public/Get-UserProvisioningSyncSessionOperation.ps1

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

    Begin {
        Write-ConnectorVerbose "Getting all users from all included OUs..."
        $Script:UserMap = @{}
        $Script:UserMapAdditional = @{}
        $Script:UserMapAdditionalDuplicates = @{}

        $Script:IncludedOUs | ForEach-Object {
            Write-ConnectorVerbose "Getting users from OU: $_"
            Get-ADUser -SearchBase $_ -LDAPFilter "($($Script:LifeCycleStateAttribute)=*)" -Properties $Script:UserProperties | 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."
                }
            }

            if ($Script:AdditionalJoinAttribute) {
                Get-ADUser -SearchBase $_ -LDAPFilter "($($Script:AdditionalJoinAttribute)=*)" -Properties $Script:UserProperties | ForEach-Object { 
                    if ($_.($Script:LifeCycleStateAttribute)) { return; }

                    $_joinValue = $_.$($Script:AdditionalJoinAttribute) | Select-Object -First 1

                    if ($Script:UserMapAdditionalDuplicates.ContainsKey($_joinValue) -or $Script:UserMapAdditional.ContainsKey($_joinValue)) {
                        Write-Warning "Multiple users with the same additional join attribute '$($_joinValue)'"
                        $Script:UserMapAdditionalDuplicates[$_joinValue] ??= @()
                        $Script:UserMapAdditionalDuplicates[$_joinValue] += $_

                        if ($Script:UserMapAdditional.ContainsKey($_joinValue)) {
                            $Script:UserMapAdditionalDuplicates[$_joinValue] += $Script:UserMapAdditional[$_joinValue]
                            $Script:UserMapAdditional.Remove($_joinValue)
                        }
                    }
                    else {
                        $Script:UserMapAdditional[$_joinValue] = $_
                    }

                    Write-Debug "User $($_.DistinguishedName) added to additional join attribute map with key '$($_joinValue)'"
                }
            }
        }

        Write-ConnectorVerbose "Total users with valid lifecycle state found in included OUs: $($Script:UserMap.Count)"
        
        if ($Script:AdditionalJoinAttribute) {
            Write-ConnectorVerbose "Total users in additional join attribute map found in included OUs: $($Script:UserMapAdditional.Count)"
            Write-Warning "Total number of duplicated additional join attribute values: $($Script:UserMapAdditionalDuplicates.Count)"

            if ($Script:UserMapAdditionalDuplicates.Count -gt 0) {
                Write-Warning "List of duplicated additional join attribute values:"
                $Script:UserMapAdditionalDuplicates.GetEnumerator() | ForEach-Object {
                    Write-Warning " - Value '$($_.Key)' has $($Script:UserMapAdditionalDuplicates[$_.Key].Count) users:"
                    $Script:UserMapAdditionalDuplicates[$_.Key] | ForEach-Object {
                        Write-Warning " - $($_.DistinguishedName)"
                    }
                }
            }
        }
    }

    Process {
        Write-Verbose "Getting all used sAMAccountNames"
        $UsedSamAccountNames = @{}
        Get-ADUser -Properties samaccountname -filter * | ForEach-Object {
            $UsedSamAccountNames[$_.samaccountname] = $true
        }

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

            $ExistingUser = $Script:UserMap[$Identifier]


            # Try additional join attribute matching if enabled and no user found by lifecycle state identifier
            if (!$ExistingUser -and $Script:AdditionalJoinAttribute -and $InputObject.$($Script:AdditionalJoinAttribute)) {
                $joinValue = $InputObject.$($Script:AdditionalJoinAttribute)
                
                if ($Script:UserMapAdditional.ContainsKey($joinValue)) {
                    $ExistingUser = $Script:UserMapAdditional[$joinValue]
                    Write-Debug "User with identifier '$Identifier' matched to existing user '$($ExistingUser.DistinguishedName)' by additional join attribute '$($Script:AdditionalJoinAttribute)' with value '$joinValue'."
                }
            }


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

                $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"
                                $OldValues["manager"] = $ExistingUser.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
                                $OldValues["manager"] = $ExistingUser.manager
                            }
                        }
                        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
                            $OldValues["Enabled"] = $ExistingUser.Enabled
                        }
                        
                    }
                    elseif ($ExistingUser.$_ -cne $InputObject.$_) {
                        if ($null -eq $InputObject.$_) {
                            $Parameters["Clear"] ??= @()
                            $Parameters["Clear"] += $_
                            $OldValues[$_] = $ExistingUser.$_
                        }
                        else {
                            $Parameters["Replace"] ??= @{}
                            $Parameters["Replace"][$_] = $InputObject.$_
                            $OldValues[$_] = $ExistingUser.$_
                        }
                    }
                }

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

                }

                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() -ADDN $ExistingUser.DistinguishedName -Parameters $Parameters -OldValues $OldValues -IAMCoreIdentityId $Identifier
                }
                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 = $_
                            !$UsedSamAccountNames.ContainsKey($s)
                        } | Select-Object -First 1 | ForEach-Object {
                            $Parameters["SamAccountName"] = $_
                        }

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

                        $UsedSamAccountNames[$Parameters["SamAccountName"]] = $true
                    }
                }

                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 -IAMCoreIdentityId $Identifier
            }
        }

        # 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  -IAMCoreIdentityId $Identifier -Action "Set-ADUser" -Identity $ExistingUser.ObjectGUID.ToString() -ADDN $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 -IAMCoreIdentityId $Identifier -Action "Remove-ADObject" -Identity $ExistingUser.ObjectGUID.ToString() -ADDN $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 -IAMCoreIdentityId $Identifier -Action "Set-ADUser" -Identity $ExistingUser.ObjectGUID.ToString() -ADDN $ExistingUser.DistinguishedName -Parameters @{
                    Replace = @{
                        "$($Script:LifeCycleStateAttribute)" = @{
                            identifier = $Identifier
                            disabled   = (Get-Date).ToString("o")
                        } | ConvertTo-Json -Compress
                    }
                    Enabled = $false
                }
            }
        }
    }

    End {

    }
}