Public/Get-UserWritebackOperations.ps1

function Get-UserWritebackOperations {
    [CmdletBinding()]

    Param(
        [Parameter(Mandatory = $false)]
        $AttributeOverrides = @{}
    )

    Process {
        #region Get all users in the specified group from Entra ID
        Write-Verbose "Getting members of group with object ID '$Script:GroupObjectId' from Entra ID."
        $EntraIDUsers = @()
        $Uri = "https://graph.microsoft.com/v1.0/groups/$Script:GroupObjectId/members?`$select=id,displayName,accountEnabled,givenName,surname,userPrincipalName,onPremisesDistinguishedName,onPremisesUserPrincipalName,onPremisesSamAccountName,onPremisesSecurityIdentifier,onPremisesDomainName&`$top=999"

        do {
            $Response = Invoke-RestMethod -Uri $Uri -Method Get -Headers (Get-EntraIDAccessTokenHeader -Profile $Script:AccessTokenProfile)
            if ($Response.value) {
                $EntraIDUsers += $Response.value
            }
            $Uri = $Response.'@odata.nextLink'
        } while ($Uri)

        if (!$EntraIDUsers) {
            Write-Error "No users found in group with object ID '$Script:GroupObjectId' in Entra ID."
            return @()
        }

        Write-Verbose "Found $($EntraIDUsers.Count) users in group with object ID '$Script:GroupObjectId'."
        #endregion

        #region Get all users from Active Directory
        Write-Verbose "Getting all users from Active Directory."
        $ADUsers = Get-ADUser -Filter * -Properties enabled,DisplayName, adminDescription, UserPrincipalName, SamAccountName, DistinguishedName, ObjectSID
        $ADUsersMap = @{}
        foreach ($ADUser in $ADUsers) {
            $ADUsersMap[$ADUser.ObjectSID.ToString()] = $ADUser
            if ($ADUser.UserPrincipalName) {
                $ADUsersMap[$ADUser.UserPrincipalName] = $ADUser
            }
            if ($ADUser.adminDescription -and $ADUser.adminDescription -like "userwriteback_*") {
                $ADUsersMap[$ADUser.adminDescription] = $ADUser
            }
        }
        Write-Verbose "Found $($ADUsers.Count) users in Active Directory."
        #endregion

        #region Join users from Entra ID and Active Directory and calculate required operations
        $EntraIDUsers | ForEach-Object {
            $EntraIDUser = $_
            $ADUser = $null
            $adminDescription = "userwriteback_$($EntraIDUser.id)"
            
            if (!$ADUser) {
                $ADUser = $ADUsersMap[$adminDescription]
                if ($ADUser) {
                    Write-Debug "Joined Entra ID user $($EntraIDUser.userPrincipalName) ($($EntraIDUser.id)) with AD user $($ADUser.SamAccountName) ($($ADUser.ObjectSID)) using adminDescription."
                }
            }

            if (!$ADUser -and $EntraIDUser.onPremisesSecurityIdentifier) {
                $ADUser = $ADUsersMap[$EntraIDUser.onPremisesSecurityIdentifier]
                if ($ADUser) {
                    Write-Debug "Joined Entra ID user $($EntraIDUser.userPrincipalName) ($($EntraIDUser.id)) with AD user $($ADUser.SamAccountName) ($($ADUser.ObjectSID)) using onPremisesSecurityIdentifier."
                }
            }
            
            if (!$ADUser -and $EntraIDUser.onPremisesUserPrincipalName) {
                $ADUser = $ADUsersMap[$EntraIDUser.onPremisesUserPrincipalName]
                if ($ADUser) {
                    Write-Debug "Joined Entra ID user $($EntraIDUser.userPrincipalName) ($($EntraIDUser.id)) with AD user $($ADUser.SamAccountName) ($($ADUser.ObjectSID)) using onPremisesUserPrincipalName."
                }
            }

            if (!$ADUser -and $EntraIDUser.userPrincipalName) {
                $ADUser = $ADUsersMap[$EntraIDUser.userPrincipalName]
                if ($ADUser) {
                    Write-Debug "Joined Entra ID user $($EntraIDUser.userPrincipalName) ($($EntraIDUser.id)) with AD user $($ADUser.SamAccountName) ($($ADUser.ObjectSID)) using userPrincipalName."
                }
            }

            if (!$ADUser) {
                Write-Verbose "No matching AD user found for Entra ID user $($EntraIDUser.userPrincipalName) ($($EntraIDUser.id)). This user will be created in Active Directory."

                New-UserWritebackOperation -Action New-ADUser -EntraIDUser $EntraIDUser -Parameters @{
                    Path              = $AttributeOverrides.ContainsKey("path") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["path"] -ArgumentList $EntraIDUser, $null) : $Script:DefaultDestinationOU
                    Name              = $AttributeOverrides.ContainsKey("name") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["name"] -ArgumentList $EntraIDUser, $null) : (New-Guid).ToString().Substring(0, 18)
                    SamAccountName    = $AttributeOverrides.ContainsKey("sAMAccountName") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["sAMAccountName"] -ArgumentList $EntraIDUser, $null) : (New-Guid).ToString().Substring(0, 18)
                    UserPrincipalName = $AttributeOverrides.ContainsKey("userPrincipalName") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["userPrincipalName"] -ArgumentList $EntraIDUser, $null) : $EntraIDUser.UserPrincipalName
                    GivenName         = $AttributeOverrides.ContainsKey("givenName") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["givenName"] -ArgumentList $EntraIDUser, $null) : $EntraIDUser.GivenName
                    Surname           = $AttributeOverrides.ContainsKey("surname") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["surname"] -ArgumentList $EntraIDUser, $null) : $EntraIDUser.Surname
                    DisplayName       = $AttributeOverrides.ContainsKey("displayName") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["displayName"] -ArgumentList $EntraIDUser, $null) : $EntraIDUser.DisplayName
                    Enabled           = $EntraIDUser.accountEnabled ?? $false
                    OtherAttributes   = @{
                        adminDescription = $adminDescription # Store the Entra ID user ID in adminDescription for tracking purposes
                    }
                }
            }
            else {
                Write-Verbose "Matching AD user found for Entra ID user $($EntraIDUser.userPrincipalName) ($($EntraIDUser.id)): $($ADUser.SamAccountName) ($($ADUser.ObjectSID))."

                $CalculatedActiveDirectoryAttributes = @{
                    UserPrincipalName = $AttributeOverrides.ContainsKey("userPrincipalName") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["userPrincipalName"] -ArgumentList $EntraIDUser, $ADUser) : $EntraIDUser.UserPrincipalName
                    GivenName         = $AttributeOverrides.ContainsKey("givenName") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["givenName"] -ArgumentList $EntraIDUser, $ADUser) : $EntraIDUser.GivenName
                    Surname           = $AttributeOverrides.ContainsKey("surname") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["surname"] -ArgumentList $EntraIDUser, $ADUser) : $EntraIDUser.Surname
                    DisplayName       = $AttributeOverrides.ContainsKey("displayName") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["displayName"] -ArgumentList $EntraIDUser, $ADUser) : $EntraIDUser.DisplayName
                    Enabled           = $EntraIDUser.accountEnabled ?? $false
                }

                $ActiveDirectoryAttributeUpdates = @{}
                $CalculatedActiveDirectoryAttributes.GetEnumerator() | ForEach-Object {
                    $Key = $_.Key
                    $Value = $_.Value
                    if ($ADUser.$Key -ne $Value) {
                        Write-Verbose "Attribute '$Key' differs between Entra ID user and AD user. Entra ID value: '$Value', AD value: '$($ADUser.$Key)'. This attribute will be updated in Active Directory."
                        $ActiveDirectoryAttributeUpdates[$Key] = $Value
                    } else {
                        Write-Debug "Attribute '$Key' is the same between Entra ID user and AD user. Value: '$Value'."
                    }
                }

                if($ActiveDirectoryAttributeUpdates.Count -gt 0) {
                    New-UserWritebackOperation -Action Set-ADUser -EntraIDUser $EntraIDUser -ADUser $ADUser -Identity $ADUser.ObjectSID.ToString() -Parameters $ActiveDirectoryAttributeUpdates
                } else {
                    Write-Verbose "No attribute updates required for AD user '$($ADUser.SamAccountName)'."
                }

                $CalculatedEntraIDAttributes = @{
                    onPremisesDistinguishedName = $ADUser.DistinguishedName
                    onPremisesSamAccountName    = $ADUser.SamAccountName
                    onPremisesUserPrincipalName = $ADUser.UserPrincipalName
                    onPremisesSecurityIdentifier = $ADUser.ObjectSID.ToString()
                    onPremisesDomainName = ($ADUser.DistinguishedName.Split(",") | Where-Object { $_ -like "DC=*" } | ForEach-Object { $_.Substring(3) }) -join "."
                }

                $EntraIDAttributeUpdates = @{}
                $CalculatedEntraIDAttributes.GetEnumerator() | ForEach-Object {
                    $Key = $_.Key
                    $Value = $_.Value
                    if ($EntraIDUser.$Key -ne $Value) {
                        Write-Warning "Attribute '$Key' differs between AD user and Entra ID user. AD value: '$Value', Entra ID value: '$($EntraIDUser.$Key)'. Please update this attribute in Entra ID."
                        $EntraIDAttributeUpdates[$Key] = $Value
                    } else {
                        Write-Debug "Attribute '$Key' is the same between AD user and Entra ID user. Value: '$Value'."
                    }
                }

                if($EntraIDAttributeUpdates.Count -gt 0) {
                    Write-Verbose "Entra ID user '$($EntraIDUser.userPrincipalName)' ($($EntraIDUser.id)) needs updates"
                    New-UserWritebackOperation -Action "Patch Entra ID User" -EntraIDUser $EntraIDUser -ADUser $ADUser -Identity $EntraIDUser.id -Parameters $EntraIDAttributeUpdates
                } else {
                    Write-Debug "No attribute updates required for Entra ID user '$($EntraIDUser.userPrincipalName)'."
                }
            }
        }
        #endregion

        #region
        # Find AD users that are not in the Entra ID group and need to be disabled
        $EntraIDUserMap = $EntraIDUsers | Where-Object { $_.onPremisesSecurityIdentifier } | Group-Object -AsHashTable -Property onPremisesSecurityIdentifier
        $EntraIDUserMap ??= @{}

        $ADUsers | 
        Where-Object adminDescription -like "userwriteback_*" | 
        ForEach-Object {
            $ADUser = $_
            if (-not $EntraIDUserMap.ContainsKey($ADUser.ObjectSID.ToString())) {
                Write-Verbose "AD user '$($ADUser.SamAccountName)' ($($ADUser.ObjectSID)) is not in the Entra ID group and will be disabled in Active Directory."

                if($ADUser.Enabled -eq $false) {
                    Write-Debug "AD user '$($ADUser.SamAccountName)' ($($ADUser.ObjectSID)) is already disabled in Active Directory. No action required."
                    return
                }

                New-UserWritebackOperation -Action Set-ADUser -ADUser $ADUser -Identity $ADUser.ObjectSID.ToString() -Parameters @{
                    Enabled = $false
                }
            }
        }
        #endregion
    }
}