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/microsoft.graph.user?`$select=id,customSecurityAttributes,employeeid,employeetype,displayName,accountEnabled,givenName,surname,officeLocation,userPrincipalName,onPremisesDistinguishedName,onPremisesUserPrincipalName,onPremisesSamAccountName,onPremisesSecurityIdentifier,onPremisesDomainName,onPremisesExtensionAttributes,companyName,department,mobilePhone,jobtitle,city,mail&`$top=999&`$expand=manager(`$select=id,onPremisesDistinguishedName,onPremisesDomainName)" 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." $Properties = @( "enabled", "DisplayName", "manager", "employeeid", "employeetype", "adminDescription", "UserPrincipalName", "SamAccountName", "DistinguishedName", "ObjectSID", "givenName", "sn", "company", "department", "office", "title", "mobile", "city", "mail" ) if (!$Script:DisableExtensionAttributeMapping) { 1..15 | ForEach-Object { $Properties += "extensionAttribute$_" } } $ADUsers = Get-ADUser -Filter * -Properties $Properties -ErrorAction Stop $ADUsersMap = @{} foreach ($ADUser in $ADUsers) { $ADUsersMap[$ADUser.ObjectSID.ToString()] = $ADUser $ADUsersMap[$ADUser.DistinguishedName] = $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." } } $AllCalculatedAttributes = @{ path = $AttributeOverrides.ContainsKey("path") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["path"] -ArgumentList $EntraIDUser, $ADUser) : $Script:DefaultDestinationOU name = $AttributeOverrides.ContainsKey("name") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["name"] -ArgumentList $EntraIDUser, $ADUser) : $EntraIDUser.UserPrincipalName sAMAccountName = $AttributeOverrides.ContainsKey("sAMAccountName") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["sAMAccountName"] -ArgumentList $EntraIDUser, $ADUser) : (New-Guid).ToString().Substring(0, 18) 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 sn = $AttributeOverrides.ContainsKey("sn") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["sn"] -ArgumentList $EntraIDUser, $ADUser) : $EntraIDUser.Surname displayName = $AttributeOverrides.ContainsKey("displayName") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["displayName"] -ArgumentList $EntraIDUser, $ADUser) : $EntraIDUser.DisplayName mobile = $AttributeOverrides.ContainsKey("mobile") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["mobile"] -ArgumentList $EntraIDUser, $ADUser) : $EntraIDUser.mobilePhone company = $AttributeOverrides.ContainsKey("company") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["company"] -ArgumentList $EntraIDUser, $ADUser) : $EntraIDUser.companyName department = $AttributeOverrides.ContainsKey("department") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["department"] -ArgumentList $EntraIDUser, $ADUser) : $EntraIDUser.department title = $AttributeOverrides.ContainsKey("title") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["title"] -ArgumentList $EntraIDUser, $ADUser) : $EntraIDUser.jobTitle mail = $AttributeOverrides.ContainsKey("mail") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["mail"] -ArgumentList $EntraIDUser, $ADUser) : $EntraIDUser.mail city = $AttributeOverrides.ContainsKey("city") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["city"] -ArgumentList $EntraIDUser, $ADUser) : $EntraIDUser.city manager = if ($EntraIDUser.manager.onPremisesDistinguishedName -and $ADUsersMap.ContainsKey($EntraIDUser.manager.onPremisesDistinguishedName)) { $EntraIDUser.manager.onPremisesDistinguishedName } else { $null } office = $AttributeOverrides.ContainsKey("office") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["office"] -ArgumentList $EntraIDUser, $ADUser) : $EntraIDUser.officeLocation enabled = $EntraIDUser.accountEnabled ?? $false employeeType = $AttributeOverrides.ContainsKey("employeeType") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["employeeType"] -ArgumentList $EntraIDUser, $null) : $EntraIDUser.employeeType employeeId = $AttributeOverrides.ContainsKey("employeeId") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["employeeId"] -ArgumentList $EntraIDUser, $null) : $EntraIDUser.employeeId } if (!$Script:DisableExtensionAttributeMapping) { 1..15 | ForEach-Object { $Attr = "extensionAttribute$_" $AllCalculatedAttributes[$Attr] = $AttributeOverrides.ContainsKey($Attr) ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides[$Attr] -ArgumentList $EntraIDUser, $null) : $EntraIDUser.onPremisesExtensionAttributes.$Attr } } 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." $Parameters = @{OtherAttributes = @{adminDescription = $adminDescription } } $AllCalculatedAttributes.GetEnumerator() | ForEach-Object { if ($_.Value -ne "NO-FLOW") { if ($_.Key -in "path", "name", "sAMAccountName", "userPrincipalName", "displayName", "mobilePhone", "company", "department", "title", "city", "manager", "office", "enabled") { $Parameters[$_.Key] = $_.Value } $Parameters.OtherAttributes[$_.Key] = $_.Value } } New-UserWritebackOperation -Action New-ADUser -EntraIDUser $EntraIDUser -Parameters $Parameters } else { $Name = $AttributeOverrides.ContainsKey("name") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["name"] -ArgumentList $EntraIDUser, $ADUser) : $EntraIDUser.UserPrincipalName if ($Name -cne $ADUser.Name) { Write-Verbose "Attribute 'Name' differs between Entra ID user and AD user. Entra ID value: '$Name', AD value: '$($ADUser.Name)'. This attribute will be updated in Active Directory." New-UserWritebackOperation -Action Rename-ADObject -EntraIDUser $EntraIDUser -ADUser $ADUser -Identity $ADUser.DistinguishedName -Parameters @{ NewName = $Name } } else { Write-Debug "Attribute 'Name' is the same between Entra ID user and AD user. Value: '$Name'." } $Path = $AttributeOverrides.ContainsKey("path") ? (Invoke-Command -NoNewScope -ScriptBlock $AttributeOverrides["path"] -ArgumentList $EntraIDUser, $ADUser) : $EntraIDUser.UserPrincipalName $CurrentPath = "OU={0}" -f ($ADUser.DistinguishedName -split "OU=", 2)[1] if ($Path -ne $CurrentPath) { Write-Verbose "The path differs AD and the calculated value. The object will be moved." New-UserWritebackOperation -Action Move-ADObject -EntraIDUser $EntraIDUser -ADUser $ADUser -Identity $ADUser.DistinguishedName -Parameters @{ TargetPath = $Path } } else { Write-Debug "The object $($ADUser.DistinguishedName) is already in the correct place." } Write-Verbose "Matching AD user found for Entra ID user $($EntraIDUser.userPrincipalName) ($($EntraIDUser.id)): $($ADUser.SamAccountName) ($($ADUser.ObjectSID))." $Parameters = @{} $AllCalculatedAttributes.GetEnumerator() | ForEach-Object { $Key = $_.Key if ($Key -in "path", "name") { return } if ($_.Value -ne "NO-FLOW") { if ($_.Value -ne $ADUser.$Key) { Write-Verbose "Attribute '$Key' differs between the calculated value and the AD user. Calculated value: '$($_.Value)', AD value: '$($ADUser.$Key)'. This attribute will be updated in Active Directory." if ($Key -in "sAMAccountName", "userPrincipalName", "displayName", "mobile", "company", "department", "title", "city", "manager", "office", "enabled") { $Parameters[$Key] = $_.Value } else { $Parameters.Replace ??= @{} $Parameters.Replace[$Key] = $_.Value } } else { Write-Debug "Attribute '$Key' already has the correct calculated value of '$($_.Value)'." } } } if ($Parameters.Count -gt 0) { New-UserWritebackOperation -Action Set-ADUser -EntraIDUser $EntraIDUser -ADUser $ADUser -Identity $ADUser.ObjectSID.ToString() -Parameters $Parameters } 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 } } |