#Author: Jos Lieben (OGD)
#Date: 15-05-2017
#Script home: www.lieben.nu
#Copyright: Leave this header intact, credit the author, otherwise free to use
#Purpose: sync distribution groups between Office 365 and an onprem Active Directory (Exchange not mandatory)
#Requires –Version 3
#example call: .\O365GroupSync.ps1 -o365login "admin@groupsync.onmicrosoft.com" -o365password "" [-differential] [-syncDirection 1|2|3]
<#Notes / features
*Dynamic Distribution Lists in Active Directory are completely ignored
*DisplayName is a REQUIRED attribute for groups on either side
*LegacyExchangeDN is added as X500 address to O365 group to support cached outlook addressbook entries
*existing X400/X500 or non-SMTP: type addresses will be ignored as they won't match existing domains in O365
*O365 Group and AD Anchor = DisplayName
*Member object Anchor = proxyAddresses vs emailAddresses comparison, 1 match = total match
*Selection for O365 can be based on a name prefix (non case-sensitive)
*Autodetection of onprem and exchange online accepted domains
*Auto elevation
*membership change support (add/delete)
*group membership change support (add/delete)
*group rename support
*group deletion support
*new group detection and creation
*hiding from address list supported
*require authenticated sender supported
*sync proxyaddress changes (only during differential sync)
*automatically add display name to groups that don't have one
*send email notification of results
REQUIREMENTS:
-Active Directory PS Module available
-Properly configured parameters (see below)
-Sufficient rights in AD and O365 for the accounts used
-Every target user or group HAS to have an email address, and every group HAS to have displayName set correctly
TODO:
-memberjoin/memberdepart restrictions
-cache only required properties when caching O365 objects / storing custom versions
-check group managers during full sync
-check permitted senders during full sync
CHANGELOG:
V0.13: creation of groups both ways, autodetection of available domains on both sides
V0.14 02/09/2016 JosL: membership change support
V0.15 02/09/2016 JosL: group rename support
V0.16 02/09/2016 JosL: group deletion support
V0.17 04/09/2016 JosL: improved group caching to include 1 layer of members, added full/diff option
V0.18 05/09/2016 JosL: implemented ReadOnly and differential switches
V0.19 06/09/2016 JosL: full sync of group membership implemented without differential / cache
V0.20 09/09/2016 JosL: support for Contact and MailUser objects
V0.21 09/09/2016 JosL: proxyAddresses change support
V0.22 13/09/2016 JosL: delete cache after using it to prevent it being used twice
V0.23 20/09/2016 JosL: write allowed senders (groups or members) to groups upon creation
V0.24 23/09/2016 JosL: switched to Filter without object cache for performance
V0.25 23/09/2016 JosL: automatically add a displayName to an AD group based on CN if displayName is empty
V0.25 23/09/2016 JosL: visual progress indicator when caching AD or O365 groups in % and timeleft
V0.26 23/09/2016 JosL: detect and process changes to allowed senders
V0.27 23/09/2016 JosL: first create all groups, then process membership and sender auth
V0.28 25/10/2016 JosL: no auto elevation, just terminate with an error when not running elevated
V0.29 25/10/2016 JosL: added email notification function
V0.30 01/11/2016 JosL: added prefix filter for AD group selection
V0.31 07/11/2016 JosL: automated version check
V0.32 29/11/2016 JosL: reduce severity from error to warning when member of group cannot be found
V0.32 30/11/2016 JosL: 2 new functions to replace Compare-Object, as this wasn't performing well
V0.33 30/11/2016 JosL: new O365 caching method, direct lookups were too slow
V0.34 02/12/2016 JosL: fixed cache compare with new function names, improved lookup of AD users with smtp: prefix to avoid false positives
V0.35 05/12/2016 JosL: bugfixes, improved logging (filter), new parameter: skipDisabledAccounts, better progress indicators
V0.36 05/12/2016 JosL: revamped O365 cache with special fast lookup hashtable to increase performance, implemented use of this new cache hash table
V0.37 06/12/2016 JosL: added debug mode, don't count version mismatch as double error
V0.38 08/12/2016 JosL: re-cache O365 Objects after creating groups, so nested groups get nested right away instead of at next run
V0.39 13/12/2016 JosL: returnChangedMembersInADGroups function optimized
V0.40 13/12/2016 JosL: implemented .NET streamwriter vs add-content to avoid file locking issues of the logfile
V0.41 13/12/2016 JosL: implemented fastSearch cache in both group retrieval functions to reduce time needed to compare group members
V0.42 13/12/2016 JosL: changed full/differential switches, full = run a full, differential = run a differential, no longer exclude one another
V0.43 13/12/2016 JosL: allow X500 addresses to sync at initial creation
V0.44 20/12/2016 JosL: remove SMTP prefix when searching the fastsearch cache, as these are saved without a prefix
V0.44 20/12/2016 JosL: removed log spam about skipping inactive accounts
V0.45 29/12/2016 JosL: when creating O365 groups, set managedBy after creating all groups (in case a group is an owner this prevents the group not being found yet)
V0.46 03/01/2017 JosL: adjusted mail parameters to optionally only email when there are issues, or changes, or always
V0.47 17/01/2017 JosL: send error mail if log file is locked and causes a crash. Process renames before anything else in a differential sync
V0.48 17/01/2017 JosL: reconnect to Exchange Online each time we're caching objects, fixed a bug in detecting Deleted Groups on both sides
V0.49 17/01/2017 JosL: moved all non-creation processing steps for AD groups to post-processing function, overwrite primary smtp instead of updating when going from AD->O365
V0.50 17/01/2017 JosL: set mailNickName and LegacyExchangeDN properties when creating group in AD so exchange picks it up in the list
V0.51 31/01/2017 JosL: actually send email notification if logfile is locked, prevent running twice (autokill other same script PS processes)
V0.52 07/02/2017 JosL: use Set-ADGroup vs Add-ADGroupMember and Remove-AdGroupMember as the latter don't support contacts in certain situations (undocumented issue: http://stackoverflow.com/questions/28542825/powershell-add-adgroupmember-objectclasscontact-to-a-distribution-group-error)
V0.53 13/03/2017 JosL: multi-delete protection, prevents deletion of all groups and forces manual action by admin
V0.54 03/04/2017 JosL: retry if membership retrieval of an Office 365 group fails and remove previous sessions when reconnecting to O365 ExO
V0.55 11/04/2017 JosL: clear hint about how to use multiple recipients for mail notification, custom ExO session health / reconnect function
V0.56 15/05/2017 JosL: group selector expanded to optionally select based on the extensionattribute in AD/O365 as an alternative to the display name prefix. Update all your groups before using this if you're switching to this mode
#>
Param(
[Parameter(Mandatory=$true)][String]$o365login,
[Parameter(Mandatory=$true)][String]$o365password,
[Parameter(Mandatory=$true)][Int]$syncDirection = 1, #specifies sync direction,
#syncDirection 1: if a change originates in either location, sync it to the other
#syncDirection 2: AD is leading, membership changes or new / deleted groups in O365 will not be synced to AD
#syncDirection 3: O365 is leading, membership changes or new / deleted groups in AD will not be synced to O365
[Switch]$differential, #If this switch is used, a differential scan will run (will fail if cache files are missing)
[Switch]$full, #If this switch is used, a full scan will run
[Switch]$readOnly, #supply this switch to ensure no actual data is changed.
[Switch]$debugMode #if you add this switch, a transcript log will be written to the same folder as the log folder
)
#region SCRIPT CONFIGURATION:
$sourceOU = "OU=Groups,OU=2 XXX Sweden,DC=XXX,DC=LOCAL" #sourceOU: LDAP path to one or more source OU's to sync to O365, seperate multiple OU's with a semicolon (;)
$targetOU = "OU=Groups,OU=2 XXX Sweden,DC=XXX,DC=LOCAL" #targetOU: LDAP path to ONE OU to sync groups back to from O365, if omitted new groups will not be synced from Office 365 to AD
$domainsPresentLocally = "" #domains that are configured in O365, seperate multiple entries with a comma (,), if left empty will be autodetected
$domainsPresentInCloud = "" #domains that are configured in onprem Exchange, seperate multiple entries with a comma (,), if left empty will be autodetected
$cloudGroupNamePrefix = "" #prefix if you wish to only sync back a subselection of groups in Office 365, e.g. NLD-The Hague, not compatible with $cloudCustomAttributeValue
$localGroupNamePrefix = "" #prefix if you wish to only sync a subselection of groups to Office 365, e.g. NLD-The Hague, not compatible with $localCustomAttributeValue
$cloudCustomAttributeValue = "" #value of this field if you wish to only sync back a subselection of groups in Office 365, e.g. NLD. Will customAttribute2 in selection, not compatible with $cloudGroupNamePrefix
$localCustomAttributeValue = "" #value of this field if you wish to only sync a subselection of groups to Office 365, e.g. NLD. Will extensionAttribute2 in selection, not compatible with $localGroupNamePrefix
$logPath = "c:\ogd\O365GroupSync.log" #the log file can contain important hints if there are issues, and can be used to roll back changes, clean up as often as you like
$autoRemediateGroupDisplayNames = $True #if set, will automatically correct displayNames of groups where the group CN does not match the display name, or the group does not have a display name
$cacheFolderPath = "c:\ogd" #note, if the cache is deleted, the script will not process updates/deletes until it has rebuilt the cache, this could cause deleted groups to reappear during the first run
$defaultDLAdminInO365 = "admin@groupsync.onmicrosoft.com" #this user becomes owner of all Office 365 Distribution Groups the script creates that currently do not have a valid 'ManagedBy' set in Active Directory
$verbose = $True #more verbose logging and on screen display
$sendMailSummary = $False #enable to have the script send a summary of errors/warnings/objects changed, currently logged in user's credentials will be used
$sendMailSummary_When = "Errors,Warnings,Changes" #options: Always (always send a summary), Warnings (if at least 1 warning), Errors (if at least 1 error), Changes (if at least 1 change). Seperate with Comma if choosing multiple
$sendMailSummary_From = "" #email address to send email FROM
$sendMailSummary_To = "" #email address to send summary email to, if multiple, use the following format: "address1@domain.com", "address2@domain.com"
$sendMailSummary_Server = "" #server name/fqdn or IP address to submit mail to
$sendMailSummary_Port = 25 #server port to submit mail on
$sendMailSummary_SSL = $False #set to True if your server requires SSL
$versionCheck = $True #does a version check and logs an error if the script is not on the most recent version
$skipDisabledAccounts = $True #if set to True, the script won't add inactive accounts from the source AD to groups in Office 365, note that shared mailboxes are usually inactive accounts
$maxProjectedDeletesProtection = 0 #if set to anything above 0, will crash the script with admin notification when more than this number of deletes are queued.
####END OF CONFIGURATION, DON'T TOUCH ANYTHING FROM HERE ON
$totalWarnings = 0
$totalErrors = 0
$totalChanges = 0
$totalChanged = 0
$ADObjectCache = @()
$version = "0.56"
$scriptName = "O365GroupSync"
$WarningActionPreference = "Stop"
$ErrorActionPreference = "Stop"
#endregion
#Check if elevated
If (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
$arguments = "& '" + $myinvocation.mycommand.definition + "'"
Throw "This script requires elevation to run properly, please run this script as an Administrator"
Read-Host "Press any key to exit"
Exit
}
if($debugMode){
$transcriptPath = "$($logPath)-debug"
Start-Transcript -Path $transcriptPath -append
}
function validateExOConnection{
Param(
[Parameter(Mandatory=$true)]$o365login,
[Parameter(Mandatory=$true)]$o365password,
[switch]$retry
)
if($script:Session -eq $Null -or $script:Session.State -ne "Opened"){
#There is no session, or it has gone stale
log -text "Exchange Online connection not available or has gone stale, attempting to connect" -color "Green"
$failed = $False
try{
$secpasswd = ConvertTo-SecureString $o365password -AsPlainText -Force
$Credentials = New-Object System.Management.Automation.PSCredential ($o365login, $secpasswd)
$a = New-PSSessionOption
$a.IdleTimeout = 432000000000
$script:Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $Credentials -Authentication Basic -AllowRedirection -SessionOption $a
$res = Import-PSSession $Session -AllowClobber -DisableNameChecking -WarningAction SilentlyContinue
log -text "Connected to Exchange Online" -color "Green"
return $Null
}catch{
$failed = $True
log -text "Failed to connect to Exchange Online!" -color "Red" -append -error
}
if($failed -and !$retry){
validateExoConnection -o365login $o365login -o365password $o365password -retry
return $Null
}
if($failed -and $retry){
log -text "Failed to connect to Exchange Online twice! Aborting" -color "Red" -append -error
abort_GS -failed
}
}
}
function preventDoubleSchedule{
try{
$scriptFileName = split-path $MyInvocation.PSCommandPath -Leaf
}catch{$scriptFileName = $Null}
try{
[Array]$psProcesses = @(Get-WmiObject Win32_Process -Filter "name like '%Powershell.exe%' and handle != '$pid'" | where {$_})
}catch{
Throw
}
if($psProcesses.Count -gt 0){
foreach($psProcess in $psProcesses){
if($psProcess.CommandLine -like "*$scriptFileName*" -and $scriptFileName){
##we've found a Powershell process that is running this script, but does not have the same process ID, lets try to kill it
try{
Stop-Process -Id $psProcess.Handle -Force -Confirm:$False
}catch{
Throw
}
}
}
}
}
function JosL-WebRequest{
Param(
$url
)
$maxAttempts = 3
$attempts=0
while($true){
$attempts++
try{
$retVal = @{}
$request = [System.Net.WebRequest]::Create($url)
$request.TimeOut = 5000
$request.UserAgent = "Lieben Consultancy"
$response = $request.GetResponse()
$retVal.StatusCode = $response.StatusCode
$retVal.StatusDescription = $response.StatusDescription
$retVal.Headers = $response.Headers
$stream = $response.GetResponseStream()
$streamReader = [System.IO.StreamReader]($stream)
$retVal.Content = $streamReader.ReadToEnd()
$streamReader.Close()
$response.Close()
return $retVal
}catch{
if($attempts -ge $maxAttempts){Throw}else{sleep -s 2}
}
}
}
function versionCheck{
Param(
$currentVersion
)
$apiURL = "http://www.lieben.nu/lieben_api.php?script=$scriptName&version=$currentVersion"
$apiKeyword = "latest$($scriptName)Version"
try{
$result = JosL-WebRequest -Url $apiURL
}catch{
Throw "Failed to connect to API url for version check: $apiURL $($Error[0])"
}
try{
$keywordIndex = $result.Content.IndexOf($apiKeyword)
if($keywordIndex -lt 1){
Throw ""
}
}catch{
Throw "Connected to API url for version check, but invalid API response"
}
$latestVersion = $result.Content.SubString($keywordIndex+$apiKeyword.Length+1,4)
if($latestVersion -ne $currentVersion){
Throw "$scriptName version mismatch, current version: v$currentVersion, latest version: v$latestVersion"
}
}
function abort_GS{
Param(
[Switch]$failed
)
try{
$htmlBody = "
Execution details for O365GroupSync
Total changes detected: $($totalChanges)
Total changes processed: $($totalChanged)
Total number of warnings: $($totalWarnings)
Total number of errors: $($totalErrors)
Script windows user: $($env:USERNAME)
Script O365 user: $o365login
Source machine: $($env:COMPUTERNAME)
Logfile path on source machine: $logPath
O365 GroupSync v$version by www.lieben.nu"
if($sendMailSummary){
$sendMailSummary = $False
if($failed){$sendMailSummary = $True}
$reasons = $sendMailSummary_When.Split(",")
if($reasons -contains "Warnings" -and $totalWarnings -gt 0){
$sendMailSummary = $True
}
if($reasons -contains "Always"){
$sendMailSummary = $True
}
if($reasons -contains "Changes" -and $totalChanges -gt 0){
$sendMailSummary = $True
}
if($reasons -contains "Errors" -and $totalErrors -gt 0){
$sendMailSummary = $True
}
}
}catch{
write-output "Error parsing mail content before sending it $($Error[0])"
}
if($failed){
try{log -text "Script execution terminated prematurely because of an error, see earlier log entries for details" -color "Red"}catch{$Null}
if($sendMailSummary){
sendMailSummary -subject "O365GroupSync CRITICAL ERROR" -body "O365GroupSync failed to run properly, please check the log: $($env:COMPUTERNAME) $logPath.
The last error was: $($script:Error[0])"
}
}else{
try{log -text "Script execution terminated normally, there were $totalErrors errors and $totalWarnings warnings" -color "Green"}catch{$Null}
if($sendMailSummary){
sendMailSummary -subject "O365GroupSync ran ($totalErrors errors)" -body $htmlBody
}
}
try{$script:logFileStreamWriter.Dispose();$script:logFileStream.Dispose();}catch{$Null}
if($debugMode){Stop-Transcript}
try{
Get-PSSession | Remove-PSSession -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -Confirm:$False
Get-Variable -Scope Local | Remove-Variable -Scope Local -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -Confirm:$False -Force
Get-Variable -Scope Script | Remove-Variable -Scope Script -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -Confirm:$False -Force
}catch{$Null}
Exit
}
function sendMailSummary{
Param(
[String]$subject,
[String]$body
)
try{
if($sendMailSummary_SSL){
$res = Send-MailMessage -To $sendMailSummary_To -From $sendMailSummary_From -Subject $subject -BodyAsHtml $body -UseSsl -SmtpServer $sendMailSummary_Server -Port $sendMailSummary_Port
}else{
$res = Send-MailMessage -To $sendMailSummary_To -From $sendMailSummary_From -Subject $subject -BodyAsHtml $body -SmtpServer $sendMailSummary_Server -Port $sendMailSummary_Port
}
log -text "mail summary sent to $sendMailSummary_To" -color "Green"
}catch{
log -text "failed to send mail summary to $sendMailSummary_To $res" -color "Red" -append
}
}
function retrieveRemoteAcceptedDomains{
try{
$domains = Get-AcceptedDomain | select DomainName -ExpandProperty DomainName -ErrorAction Stop
return $domains
}catch{
Throw "Failed to retrieve AcceptedDomains in Exchange Online $($Error[0])"
}
}
function retrieveOnpremAcceptedDomains{
Param(
[Parameter(Mandatory=$true)]$ADObjectCache
)
try{
$domains = @()
foreach($object in $ADObjectCache){
foreach($email in $object.ProxyAddresses){
if($email -like "*@*"){
$email = $email.Split("@",[System.StringSplitOptions]::RemoveEmptyEntries)[1]
if($email -like "*.*" -and $domains -notcontains $email){
$subdomain = $email.Split(".",[System.StringSplitOptions]::RemoveEmptyEntries)[1]
if($subdomain.Length -gt 1){
$domains += $email
}
}
}
}
}
return $domains
}catch{
Throw "Failed to retrieve unique proxy addresses from Active Directory $($Error[0])"
}
}
function checkIfAddressIsAllowed{
Param(
[Parameter(Mandatory=$true)]$address,
[Parameter(Mandatory=$true)]$mode #1 = check if it is allowed in the cloud, #2 = check if it is allowed on premises
)
if($address.IndexOf("X500",[System.StringComparison]::CurrentCultureIgnoreCase) -eq 0){return $True}
try{
$domainPortion = $address.Split("@")[1]
}catch{
return $False
}
if($domainPortion){
if($mode -eq 1){
if($domainsPresentInCloud -contains $domainPortion){
return $True
}else{
return $False
}
}
if($mode -eq 2){
if($domainsPresentLocally -contains $domainPortion){
return $True
}else{
return $False
}
}
}else{
return $True
}
}
function log{
param (
[Parameter(Mandatory=$true)][String]$text,
[Parameter(Mandatory=$true)][String]$color,
[Switch]$append,
[Switch]$error,
[Switch]$warning
)
if($error){
$script:totalErrors++
$text = "ERROR | $text"
}
elseif($warning){
$script:totalWarnings++
$text = "WARNING | $text"
}
else{
$text = "INFO | $text"
}
try{
if($append){$text = "$text, $($global:Error[0].Exception) at $($global:Error[0].InvocationInfo.ScriptLineNumber) $($global:Error[0].ErrorDetails)"}
}catch{$text = "$text,failed to append error message"}
try{
$script:logFileStreamWriter.WriteLine("$(Get-Date) | $text")
}catch{$Null}
Write-Host $text -ForegroundColor $color
}
function cacheADObjects{
try{
$ADObjects = @(Get-ADObject -Filter {(ObjectClass -eq "user" -or ObjectClass -eq "Group" -or ObjectClass -eq "contact")} -Properties proxyAddresses,name,DistinguishedName,DisplayName,ObjectGuid,mail,cn,managedBy,authOrig,dLMemSubmitPerms -ErrorAction Stop)
}catch{
Throw "Failed to cache AD objects. $($Error[0]) $ADObjects"
}
return $ADObjects
}
function cacheO365Objects{
validateExOConnection -o365login $o365login -o365password $o365password
$O365Cache = @{}
$O365Cache.FastSearch = @{}
$script:WarningActionPreference = "SilentlyContinue"
$script:ErrorActionPreference = "SilentlyContinue"
Write-Progress -Activity "Retrieving O365 Objects" -PercentComplete 0 -Status "Processing mailboxes"
#first look for mailboxes
try{
[Array]$O365Cache.Mailboxes = @()
get-mailbox -ResultSize Unlimited -erroraction SilentlyContinue -warningAction SilentlyContinue | % {
if($_){
$O365Cache.Mailboxes += $_
$O365Cache.FastSearch."$($_.primarySmtpAddress)" = "mbx_$($O365Cache.Mailboxes.Count-1)"
}
}
}catch{$Null}
Write-Progress -Activity "Retrieving O365 Objects" -PercentComplete 25 -Status "Processing contacts"
#now look for contacts
try{
[Array]$O365Cache.MailContacts = @()
get-mailcontact -ResultSize Unlimited -erroraction SilentlyContinue -warningAction SilentlyContinue | % {
if($_){
$O365Cache.MailContacts += $_
$O365Cache.FastSearch."$($_.primarySmtpAddress)" = "con_$($O365Cache.MailContacts.Count-1)"
}
}
}catch{$Null}
Write-Progress -Activity "Retrieving O365 Objects" -PercentComplete 50 -Status "Processing mail users"
#now look for mailusers
try{
[Array]$O365Cache.MailUsers = @()
get-mailuser -ResultSize Unlimited -erroraction SilentlyContinue -warningAction SilentlyContinue | % {
if($_){
$O365Cache.MailUsers += $_
$O365Cache.FastSearch."$($_.primarySmtpAddress)" = "usr_$($O365Cache.MailUsers.Count-1)"
}
}
}catch{$Null}
Write-Progress -Activity "Retrieving O365 Objects" -PercentComplete 75 -Status "Processing groups"
#now look for groups
try{
[Array]$O365Cache.Groups = @()
Get-distributiongroup -ResultSize Unlimited -erroraction SilentlyContinue -warningAction SilentlyContinue | % {
if($_){
$O365Cache.Groups += $_
$O365Cache.FastSearch."$($_.primarySmtpAddress)" = "grp_$($O365Cache.Groups.Count-1)"
}
}
}catch{$Null}
Write-Progress -Activity "Retrieving O365 Objects" -PercentComplete 100 -Status "O365 Objects cached!"
$script:WarningActionPreference = "Stop"
$script:ErrorActionPreference = "Stop"
return $O365Cache
}
function retrieveADGroupsAndMembers{
Param(
[Parameter(Mandatory=$true)]$sourceOU
)
#retrieve current groups in AD
$sourceOUs = $sourceOU.Split(";",[System.StringSplitOptions]::RemoveEmptyEntries)
$adDistributionGroups = @()
$adDistributionGroupsAndMembers = @()
$starttime = Get-Date
foreach($ou in $sourceOUs){
try{
$adDistributionGroups += Get-ADGroup -Filter {mail -like "*"} -Properties * -SearchBase $ou -SearchScope Subtree -ErrorAction Stop
}catch{
Throw "Failed to load groups from $ou $($Error[0])"
}
}
if($adDistributionGroups[0].DisplayName.Length -gt 0){
if($adDistributionGroups.Count -gt 1){
$adDistributionGroups = $adDistributionGroups.GetEnumerator() | sort -Property name
}
if($localGroupNamePrefix){
$localGroupNamePrefix = "$($localGroupNamePrefix)*"
if($verbose){log -text "using $localGroupNamePrefix as filter in AD distribution group selection" -color "Green"}
$adDistributionGroups = $adDistributionGroups | where {$_.displayName -like $localGroupNamePrefix}
}
if($localCustomAttributeValue){
if($verbose){log -text "using $localCustomAttributeValue as filter in AD distribution groups, only groups with this value in extensionAttribute2 will be synced" -color "Green"}
$adDistributionGroups = $adDistributionGroups | where {$_.extensionAttribute2 -eq $localCustomAttributeValue}
}
$done=0
foreach($adDistributionGroup in $adDistributionGroups){
try{$percentComplete = ($done / ($adDistributionGroups.Count)) * 100}catch{$percentComplete = 1}
$done++
if($percentComplete -gt 0){
$runtime = ((Get-Date) - $starttime).TotalSeconds
$timeleft = ((100/$percentComplete)*$runtime)-$runtime
$ts = [timespan]::fromseconds($timeleft)
$remainingTime = "$("{0:hh\:mm\:ss}" -f $ts)"
}
if($adDistributionGroup.objectGuid.Guid -eq $Null){
continue
}
Write-Progress -Activity "Retrieving distribution groups" -PercentComplete $percentComplete -Status "Processing: $($adDistributionGroup.displayName), time left: $remainingTime"
$memberObjects = @()
$fastSearchCache = @{}
foreach($member in $adDistributionGroup.member){
try{
$res = searchADForUserOrGroup -searchQuery $member
$fastSearchCache."$($res.ObjectGuid.Guid)" = 1
$memberObjects += $res
}catch{$Null}
}
$adDistributionGroup = $adDistributionGroup | Add-Member -MemberType NoteProperty -Name MemberObjects -Value $memberObjects -PassThru -ErrorAction Stop -Force
$adDistributionGroup = $adDistributionGroup | Add-Member -MemberType NoteProperty -Name FastSearch -Value $fastSearchCache -PassThru -ErrorAction Stop -Force
$adDistributionGroupsAndMembers += $adDistributionGroup
}
Write-Progress -Activity "Retrieving distribution groups" -PercentComplete 100 -Status "DONE" -Completed
return $adDistributionGroupsAndMembers
}
return @()
}
function retrieveO365GroupsAndMembers{
#retrieve current groups and their members in Office 365
$o365DistributionGroups = @()
$o365DistributionGroupsAndMembers = @()
try{
$o365DistributionGroups = @(Get-DistributionGroup -ResultSize Unlimited -WarningAction Stop -ErrorAction Stop | where{$_.DisplayName.Length -gt 0})
if($cloudGroupNamePrefix){
$cloudGroupNamePrefix = "$cloudGroupNamePrefix*"
if($verbose){log -text "using $cloudGroupNamePrefix as filter in O365 distribution group selection" -color "Green"}
$o365DistributionGroups = @($o365DistributionGroups | Where {$_.DisplayName -like "$cloudGroupNamePrefix"})
}
if($cloudCustomAttributeValue){
if($verbose){log -text "using $cloudCustomAttributeValue as filter, only o365groups with this value in customerAttribute2 will be used" -color "Green"}
$o365DistributionGroups = @($o365DistributionGroups | Where {$_.CustomAttribute2 -eq "$cloudCustomAttributeValue"})
}
}catch{
Throw "Get-DistributionGroup failed with $($Error[0])"
}
Write-Progress -Activity "Retrieving distribution groups" -PercentComplete 0 -Status "Loading $($o365DistributionGroups) from Office 365, this can take a while"
if($O365DistributionGroups[0].DisplayName.Length -gt 0){
if($o365DistributionGroups.Count -gt 1){
$o365DistributionGroups = $o365DistributionGroups.GetEnumerator() | sort -Property name
}
$done=0
foreach($distributionGroup in $o365DistributionGroups){
validateExOConnection -o365login $o365login -o365password $o365password
try{$percentComplete = ($done / ($o365DistributionGroups.Count)) * 100}catch{$percentComplete = 1}
Write-Progress -Activity "Retrieving distribution groups" -PercentComplete $percentComplete -Status "Processing: $($distributionGroup.displayName)"
$memberObjects = @()
$fail = $False
try{
$members = Get-DistributionGroupMember -resultsize Unlimited -identity $distributionGroup.Identity -ErrorAction Stop
}catch{
$fail=$True
}
if($fail){
try{
Sleep -s 5
$members = Get-DistributionGroupMember -resultsize Unlimited -identity $distributionGroup.Identity -ErrorAction Stop
}catch{
Throw "Failed to cache membership for $($distributionGroup.DisplayName) with ID $($distributionGroup.Identity) because of $members and $($Error[0])"
}
}
$subDone = 0
$starttime = Get-Date
$fastSearchCache = @{}
foreach($member in $members){
try{$percentCompleteSub = ($subDone / ($members.Count)) * 100}catch{$percentCompleteSub = 1}
if($percentCompleteSub -gt 0){
$runtime = ((Get-Date) - $starttime).TotalSeconds
$timeleft = ((100/$percentCompleteSub)*$runtime)-$runtime
$ts = [timespan]::fromseconds($timeleft)
$remainingTime = "$("{0:hh\:mm\:ss}" -f $ts)"
}
Write-Progress -Activity "Retrieving distribution groups" -PercentComplete $percentComplete -Status "Processing: $($distributionGroup.displayName), $($member.displayName) ($subDone / $($members.Count) remaining time for this group: $remainingTime"
try{
$res = searchExOForUserOrGroupV2 -O365ObjectCache $O365ObjectCache -searchQuery $member.primarySmtpAddress
$fastSearchCache."$($res.guid.guid)" = 1
$memberObjects += $res
}catch{$Null}
$subDone++
}
$distributionGroup = $distributionGroup | Add-Member -MemberType NoteProperty -Name Members -Value $memberObjects -PassThru -ErrorAction Stop -Force
$distributionGroup = $distributionGroup | Add-Member -MemberType NoteProperty -Name FastSearch -Value $fastSearchCache -PassThru -ErrorAction Stop -Force
$o365DistributionGroupsAndMembers += $distributionGroup
$done++
}
Write-Progress -Activity "Retrieving distribution groups" -PercentComplete 100 -Status "DONE" -Completed
return $o365DistributionGroupsAndMembers
}
return @()
}
function returnDeletedGroups{
Param(
[Parameter(Mandatory=$true)]$referenceGroups, #cache
[Parameter(Mandatory=$true)]$currentGroups, #current groups
[Parameter(Mandatory=$true)]$mode #1 = office 365 groups, 2 = AD groups
)
if($mode -eq 1){
$compareParam = "Guid"
}else{
$compareParam = "ObjectGuid"
}
$deletedGroups = @()
foreach($group in $referenceGroups){
[Array]$found = @($currentGroups | where {$_.$compareParam -eq $group.$compareParam -and $_})
if($found.Count -le 0){
$deletedGroups += $group
}
}
return $deletedGroups
}
function returnNewGroups{
Param(
[Parameter(Mandatory=$true)]$referenceGroups, #cache
[Parameter(Mandatory=$true)]$currentGroups, #current groups
[Parameter(Mandatory=$true)]$mode #1 = office 365 groups, 2 = AD groups
)
if($mode -eq 1){
$compareParam = "Guid"
}else{
$compareParam = "ObjectGuid"
}
$newGroups = @()
foreach($group in $currentGroups){
[Array]$found = @($referenceGroups | where {$_.$compareParam -eq $group.$compareParam -and $_})
if($found.Count -le 0){
$newGroups += $group
}
}
return $newGroups
}
function searchExOForUserOrGroupV2{#returns Exchange Online object, when searching based on an email address, also finds if alias, but much slower
Param(
[Parameter(Mandatory=$true)]$O365ObjectCache,
[Parameter(Mandatory=$true)]$searchQuery, #defaults to email address, but switches can modify this
[Switch]$byAlias,
[Switch]$byDisplayName
)
if($searchQuery.Length -lt 2){Throw "Invalid query: $searchQuery"}
if(!$byAlias -and !$byDisplayName) {
if($searchQuery.IndexOf("smtp:",[System.StringComparison]::CurrentCultureIgnoreCase) -eq 0){
$searchQuery = $searchQuery.SubString(5)
}
try{
$res = $O365ObjectCache.FastSearch.$searchQuery
switch($res.Split("_")[0]){
"mbx"{
$retVal = $O365ObjectCache.Mailboxes[$res.Split("_")[1]]
if($retVal){return $retVal}
}
"con"{
$retVal = $O365ObjectCache.MailContacts[$res.Split("_")[1]]
if($retVal){return $retVal}
}
"usr"{
$retVal = $O365ObjectCache.MailUsers[$res.Split("_")[1]]
if($retVal){return $retVal}
}
"grp"{
$retVal = $O365ObjectCache.Groups[$res.Split("_")[1]]
if($retVal){return $retVal}
}
default{
Throw
}
}
}catch{$Null}
$searchQuery = "smtp:$searchQuery" #default to searching by email, prepend smtp: as this is how O365 knows them
}
#first look for a mailbox
try{
if($byAlias){[Array]$res = @($O365ObjectCache.Mailboxes | where {$_.alias -eq $searchQuery -and $_})}
elseif($byDisplayName){[Array]$res = @($O365ObjectCache.Mailboxes | where {$_.DisplayName -eq $searchQuery -and $_})}
else{[Array]$res = @($O365ObjectCache.Mailboxes | where {$_.EmailAddresses -Contains $searchQuery -and $_})}
if($res.Count -eq 0){Throw "No Mailbox Found"}
}catch{$res = $Null}
#if none is found, look for a contact
if($res -eq $Null){
try{
if($byAlias){[Array]$res = @($O365ObjectCache.MailContacts | where {$_.alias -eq $searchQuery -and $_})}
elseif($byDisplayName){[Array]$res = @($O365ObjectCache.MailContacts | where {$_.DisplayName -eq $searchQuery -and $_})}
else{[Array]$res = @($O365ObjectCache.MailContacts | where {$_.EmailAddresses -Contains $searchQuery -and $_})}
if($res.Count -eq 0){Throw "No Contact Found"}
}catch{$res = $Null}
}
#if none is found, look for a mailuser
if($res -eq $Null){
try{
if($byAlias){[Array]$res = @($O365ObjectCache.MailUsers | where {$_.alias -eq $searchQuery -and $_})}
elseif($byDisplayName){[Array]$res = @($O365ObjectCache.MailUsers | where {$_.DisplayName -eq $searchQuery -and $_})}
else{[Array]$res = @($O365ObjectCache.MailUsers | where {$_.EmailAddresses -Contains $searchQuery -and $_})}
if($res.Count -eq 0){Throw "No MailUser Found"}
}catch{$res = $Null}
}
#if none is found, look for a Group
if($res -eq $Null){
try{
if($byAlias){[Array]$res = @($O365ObjectCache.Groups | where {$_.alias -eq $searchQuery -and $_})}
elseif($byDisplayName){[Array]$res = @($O365ObjectCache.Groups | where {$_.DisplayName -eq $searchQuery -and $_})}
else{[Array]$res = @($O365ObjectCache.Groups | where {$_.EmailAddresses -Contains $searchQuery -and $_})}
if($res.Count -eq 0){Throw "No DistributionGroup Found"}
}catch{$res = $Null}
}
#return false if nothing found, otherwise return object
$script:WarningActionPreference = "Stop"
$script:ErrorActionPreference = "Stop"
if($res){
return $res[0]
}else{
Throw "Could not find $searchQuery in Exchange Online"
}
}
function searchADForUserOrGroup{
param(
[Parameter(Mandatory=$true)]$searchQuery,
[Switch]$byMail, #search for any object that has $searchQuery defined as a proxyAddress
[Switch]$anyAlias #searchQuery is not a string, but aliasses, if one of the aliasses is found on an object in AD, return that object immediately
)
if($searchQuery.Length -lt 2){Throw "Invalid search Query $searchQuery"}
$found = $False
if($byMail){
if($anyAlias){
foreach($query in $searchQuery){
if($query.IndexOf("smtp",[System.StringComparison]::CurrentCultureIgnoreCase) -eq -1){
$query = "smtp:$($query)"
}
[Array]$adresult = @(Get-ADObject -Filter {(ObjectClass -eq "user" -or ObjectClass -eq "Group" -or ObjectClass -eq "contact") -and (proxyAddresses -like $query)} -Properties userAccountControl,proxyAddresses,name,DistinguishedName,DisplayName,ObjectGuid,mail,cn,managedBy,authOrig,dLMemSubmitPerms -ErrorAction Stop | where{$_})
if($adresult.Count -gt 0){
break
}
}
}else{
if($searchQuery.IndexOf("smtp",[System.StringComparison]::CurrentCultureIgnoreCase) -eq -1){
$searchQuery = "smtp:$($searchQuery)"
}
[Array]$adresult = @(Get-ADObject -Filter {(ObjectClass -eq "user" -or ObjectClass -eq "Group" -or ObjectClass -eq "contact") -and (proxyAddresses -like $searchQuery)} -Properties userAccountControl,proxyAddresses,name,DistinguishedName,DisplayName,ObjectGuid,mail,cn,managedBy,authOrig,dLMemSubmitPerms -ErrorAction Stop | where{$_})
}
}else{
[Array]$adresult = @(Get-ADObject -Filter {(ObjectClass -eq "user" -or ObjectClass -eq "Group" -or ObjectClass -eq "contact") -and (name -eq $searchQuery -or distinguishedName -eq $searchQuery -or displayName -eq $searchQuery)} -Properties userAccountControl,proxyAddresses,name,DistinguishedName,DisplayName,ObjectGuid,mail,cn,managedBy,authOrig,dLMemSubmitPerms -ErrorAction Stop | where{$_})
}
if($adresult.Count -gt 1){
log -text "Warning, multiple objects found for $searchQuery in AD, only returning first object" -color "Red" -error
$adresult = $adresult[0]
}elseif($adresult.Count -eq 0){
Throw "Could not find $searchQuery in AD"
}
return $adresult
}
function returnMissingGroups{#this function searches what groups in the second parameter are not present in the first and returns those as a collection
param(
[Parameter(Mandatory=$false)]$referenceGroups,
[Parameter(Mandatory=$false)]$differenceGroups
)
$retVal = @()
if(!$referenceGroups -and !$differenceGroups){
Throw "No groups were supplied to compare, this script cannot run if there are no groups in both AD and O365"
}
if(!$referenceGroups){return $differenceGroups}
if(!$differenceGroups){return $Null}
foreach($group in $differenceGroups){
if($group.DisplayName){
[Array]$res = @($referenceGroups | where{$_.DisplayName -eq $group.DisplayName})
}
if($res.Count -eq 0){
$retVal += $group
}
}
return $retVal
}
function returnRenamedGroups{#this function searches what groups in the second parameter are differently named in the first and returns those as a collection
param(
[Parameter(Mandatory=$false)]$referenceGroups,
[Parameter(Mandatory=$false)]$differenceGroups,
[Parameter(Mandatory=$false)]$mode #1 = AD Groups, #2 = O365 Groups
)
$retVal = @()
if(!$referenceGroups -or !$differenceGroups){
Throw "No groups were supplied to compare names"
}
foreach($group in $differenceGroups){
$res = @()
if($mode -eq 1){
if($group.ObjectGuid.Guid){
[Array]$res = @($referenceGroups | where{$_.ObjectGuid.Guid -eq $group.ObjectGuid.Guid})
}
}else{
if($group.Guid.Guid){
[Array]$res = @($referenceGroups | where{$_.Guid.Guid -eq $group.Guid.Guid})
}
}
if($res.Count -eq 0){
continue #as this group was not found, it isn't part of the renamed scope
}elseif($res.Count -gt 1){
continue #multiple results found, so invalid result
}else{
if($res[0].displayName -ne $group.displayName){
$obj = New-Object PSObject
$obj | Add-Member NoteProperty newIdentifier($res[0].DisplayName)
$obj | Add-Member NoteProperty newDisplayName($group.DisplayName)
$retVal+=$obj
}
}
}
return $retVal
}
function returnChangedMembersInO365Groups{
param(
[Parameter(Mandatory=$true)]$referenceGroups, #original cache of groups
[Parameter(Mandatory=$true)]$differenceGroups #new situation
)
$retVal = @()
#Objects that represent actions to take
#Mode 1: remove member, 2: add member
#DisplayName: display name of the target group to modify
#memberMail: email address of the member to add/remove
Write-Progress -Activity "Comparing O365 cache and current group membership" -PercentComplete 0 -Status "Init"
$done = 0
foreach($group in $differenceGroups){
validateExOConnection -o365login $o365login -o365password $o365password
$originalMembers = @()
$currentMembers = @()
#find this new group in cached older groups
$matchingGroup = $referenceGroups | where{$_.Guid.Guid -eq $group.Guid.Guid}
if($matchingGroup.Guid.Guid.Length -lt 1){
continue
}
try{$percentComplete = ($done / ($differenceGroups.Count)) * 100}catch{$percentComplete = 1}
Write-Progress -Activity "Comparing O365 cache and current group membership" -PercentComplete $percentComplete -Status "Processing: $($group.displayName)"
$matchingGroup.Members | % {
try{
$res = searchExOForUserOrGroupV2 -O365ObjectCache $script:O365ObjectCache -searchQuery $_.PrimarySmtpAddress
$originalMembers += $res
}catch{$Null}
}
get-distributiongroupmember -ResultSize Unlimited -Identity $group.DistinguishedName -erroraction silentlycontinue | % {
try{
$res = searchExOForUserOrGroupV2 -O365ObjectCache $script:O365ObjectCache -searchQuery $_.PrimarySmtpAddress
$currentMembers += $res
}catch{$Null}
}
try{
[Array]$changes = @(Compare-Object -ReferenceObject $originalMembers -DifferenceObject $currentMembers -Property Guid -PassThru -ErrorAction Stop | where {$_})
if($changes[0].Guid.Guid.Length -lt 1){
Throw "No changes found for $($group.DisplayName)"
}
}catch{
continue
}
foreach($change in $changes){
#skip if the member doesn't have an email address
if($change.primarysmtpaddress.Length -lt 1){
continue
}
$obj = New-Object PSObject
#removed user
if($change.SideIndicator -eq "<="){
$obj | Add-Member NoteProperty mode(1)
}
#added user
if($change.SideIndicator -eq "=>"){
$obj | Add-Member NoteProperty mode(2)
}
$obj | Add-Member NoteProperty DisplayName($group.DisplayName)
$obj | Add-Member NoteProperty memberMail($change.primarysmtpaddress)
$retVal += $obj
}
$done++
}
return $retVal
}
function returnChangedProxyAddresses{
param(
[Parameter(Mandatory=$true)]$referenceGroups, #original cache of groups
[Parameter(Mandatory=$true)]$differenceGroups, #new situation
[Parameter(Mandatory=$true)]$mode #1 = O365 groups, 2 = AD groups
)
$retVal = @()
#Objects that represent actions to take
#Mode 1: remove address, 2: add address
#DisplayName: display name of the target group to modify
#proxyAddress: address to add or remove
foreach($group in $differenceGroups){
#find this group's corresponding group
if($mode -eq 1){
$matchingGroup = $referenceGroups | where{$_.Guid.Guid -eq $group.Guid.Guid}
if($matchingGroup.Guid.Guid.Length -lt 1){
continue
}
}else{
$matchingGroup = $referenceGroups | where{$_.ObjectGuid.Guid -eq $group.ObjectGuid.Guid}
if($matchingGroup.ObjectGuid.Guid.Length -lt 1){
continue
}
}
try{
if($mode -eq 1){
$originalProxies = $matchingGroup.EmailAddresses
$currentProxies = $group.EmailAddresses
}else{
$originalProxies = $matchingGroup.proxyAddresses
$currentProxies = $group.proxyAddresses
}
foreach($address in $originalProxies){
if($address -and $currentProxies -notcontains $address){
if($mode -eq 1){#check if this address is allowed on premises, otherwise skip
if(!(checkIfAddressIsAllowed -mode 2 -address $address)){
continue
}
}
if($mode -eq 2){#check if this address is allowed in the cloud, otherwise skip
if(!(checkIfAddressIsAllowed -mode 1 -address $address)){
continue
}
}
#deleted address, add to list
$obj = New-Object PSObject
$obj | Add-Member NoteProperty mode(1)
$obj | Add-Member NoteProperty DisplayName($group.DisplayName)
$obj | Add-Member NoteProperty proxyAddress($address)
$retVal += $obj
}
}
foreach($address in $currentProxies){
if($address -and $originalProxies -notcontains $address){
if($mode -eq 1){#check if this address is allowed on premises, otherwise skip
if(!(checkIfAddressIsAllowed -mode 2 -address $address)){
continue
}
}
if($mode -eq 2){#check if this address is allowed in the cloud, otherwise skip
if(!(checkIfAddressIsAllowed -mode 1 -address $address)){
continue
}
}
#added address, add to list
$obj = New-Object PSObject
$obj | Add-Member NoteProperty mode(2)
$obj | Add-Member NoteProperty DisplayName($group.DisplayName)
$obj | Add-Member NoteProperty proxyAddress($address)
$retVal += $obj
}
}
}catch{
continue
}
}
return $retVal
}
function returnChangedAuthorizedSenders{
param(
[Parameter(Mandatory=$true)]$referenceGroups, #original cache of groups
[Parameter(Mandatory=$true)]$differenceGroups, #new situation
[Parameter(Mandatory=$true)]$mode #1 = O365 groups, 2 = AD groups
)
$retVal = @()
#Objects that represent actions to take
#Mode 1: remove single sender, 2: add single sender, 3: remove group sender, 4: add group sender
#DisplayName: display name of the target group to modify
#authorizedSender: sender identifier to add or remove
foreach($group in $differenceGroups){
#find this group's corresponding group
if($mode -eq 1){
$matchingGroup = $referenceGroups | where{$_.Guid.Guid -eq $group.Guid.Guid}
if($matchingGroup.Guid.Guid.Length -lt 1){
continue
}
}else{
$matchingGroup = $referenceGroups | where{$_.ObjectGuid.Guid -eq $group.ObjectGuid.Guid}
if($matchingGroup.ObjectGuid.Guid.Length -lt 1){
continue
}
}
try{
if($mode -eq 1){
$originalAllowedSingleSenders = $matchingGroup.AcceptMessagesOnlyFrom
$currentAllowedSingleSenders = $group.AcceptMessagesOnlyFrom
$originalAllowedGroupSenders = $matchingGroup.AcceptMessagesOnlyFromDLMembers
$currentAllowedGroupSenders = $group.AcceptMessagesOnlyFromDLMembers
}else{
$originalAllowedSingleSenders = $matchingGroup.authOrig
$currentAllowedSingleSenders = $group.authOrig
$originalAllowedGroupSenders = $matchingGroup.dLMemSubmitPerms
$currentAllowedGroupSenders = $group.dLMemSubmitPerms
}
foreach($originalAllowedSingleSender in $originalAllowedSingleSenders){
if($originalAllowedSingleSender -and $currentAllowedSingleSenders -notcontains $originalAllowedSingleSender){
#deleted address, add to list
$obj = New-Object PSObject
$obj | Add-Member NoteProperty mode(1)
$obj | Add-Member NoteProperty DisplayName($group.DisplayName)
$obj | Add-Member NoteProperty authorizedSender($originalAllowedSingleSender)
$retVal += $obj
}
}
foreach($currentAllowedSingleSender in $currentAllowedSingleSenders){
if($currentAllowedSingleSender -and $originalAllowedSingleSenders -notcontains $currentAllowedSingleSender){
#deleted address, add to list
$obj = New-Object PSObject
$obj | Add-Member NoteProperty mode(2)
$obj | Add-Member NoteProperty DisplayName($group.DisplayName)
$obj | Add-Member NoteProperty authorizedSender($currentAllowedSingleSender)
$retVal += $obj
}
}
foreach($originalAllowedGroupSender in $originalAllowedGroupSenders){
if($originalAllowedGroupSender -and $currentAllowedGroupSenders -notcontains $originalAllowedGroupSender){
#deleted address, add to list
$obj = New-Object PSObject
$obj | Add-Member NoteProperty mode(3)
$obj | Add-Member NoteProperty DisplayName($group.DisplayName)
$obj | Add-Member NoteProperty authorizedSender($originalAllowedGroupSender)
$retVal += $obj
}
}
foreach($currentAllowedGroupSender in $currentAllowedGroupSenders){
if($currentAllowedGroupSender -and $originalAllowedGroupSenders -notcontains $currentAllowedGroupSender){
#added address, add to list
$obj = New-Object PSObject
$obj | Add-Member NoteProperty mode(4)
$obj | Add-Member NoteProperty DisplayName($group.DisplayName)
$obj | Add-Member NoteProperty authorizedSender($currentAllowedGroupSender)
$retVal += $obj
}
}
}catch{
continue
}
}
return $retVal
}
function returnChangedMembersInADGroups{
param(
[Parameter(Mandatory=$true)]$referenceGroups, #original cache of groups
[Parameter(Mandatory=$true)]$differenceGroups #new situation
)
$retVal = @()
#Objects that represent actions to take
#Mode 1: remove member, 2: add member
#DisplayName: display name of the target group to modify
#memberMail: email address of the member to add/remove
foreach($group in $differenceGroups){
$originalMembers = @()
$currentMembers = @()
#find this new group in cached older groups
$matchingGroup = $referenceGroups | where{$_.ObjectGuid -eq $group.ObjectGuid}
if($matchingGroup.ObjectGuid.Guid.Length -lt 1){
continue
}
try{
[Array]$changes = @(Compare-Object -ReferenceObject $matchingGroup.MemberObjects -DifferenceObject $group.MemberObjects -Property ObjectGUID -PassThru -ErrorAction Stop | where {$_})
if($changes[0].ObjectGUID.Guid.Length -lt 1){
Throw "No changes found for $($group.DisplayName)"
}
}catch{
continue
}
foreach($change in $changes){
#skip if the member doesn't have an email address
if($change.mail.Length -lt 1){
continue
}
$obj = New-Object PSObject
#removed user
if($change.SideIndicator -eq "<="){
$obj | Add-Member NoteProperty mode(1)
}
#added user
if($change.SideIndicator -eq "=>"){
$obj | Add-Member NoteProperty mode(2)
}
$obj | Add-Member NoteProperty DisplayName($group.DisplayName)
$obj | Add-Member NoteProperty memberMail($change.mail)
$retVal += $obj
}
}
return $retVal
}
function returnDifferentMembersBetweenGroups{
param(
[Parameter(Mandatory=$true)]$o365Groups, #o365 groups object
[Parameter(Mandatory=$true)]$adGroups #ad groups object
)
$retVal = @()
#Objects that represent actions to take
#mode: 1=add to AD group, 2=add to O365 group
#DisplayName: display name of the target group to modify
#targetIdentifier: unique id of target group to modify
#memberIdentifier: email address of the member to add/remove for O365, or DistinguishedName for AD
$percentComplete = 0
$done = 0
Write-Progress -Activity "Comparing groups..." -PercentComplete $percentComplete -Status "Loading $($adGroups.Count + $o365Groups.Count) groups..."
#build member lists per group and process them
foreach($adGroup in $adGroups){
try{$percentComplete = (($done / $adGroups.Count) * 100)}catch{$percentComplete = 0}
#find this AD group's corresponding O365 group
$o365Group = $o365Groups | where{$_.DisplayName -eq $adGroup.DisplayName}
if($o365Group.Guid.Guid.Length -lt 1){
continue
}
Write-Progress -Activity "Comparing groups..." -PercentComplete $percentComplete -Status "Working on $($adGroup.DisplayName)"
#process member lists and add any changes to changes array
#first TO Office 365
foreach($adGroupMember in $adGroup.MemberObjects){
Write-Progress -Activity "Comparing groups..." -PercentComplete $percentComplete -Status "Working on $($adGroup.DisplayName) - $($adGroupMember.mail)"
if($adGroupMember.userAccountControl -band 2 -and $skipDisabledAccounts){
continue
}
#if can't find in the O365 group, add to list
try{
#user has to exist in ExO, retrieve user from ExO or move to next adGroupMember
$res = searchExOForUserOrGroupV2 -O365ObjectCache $script:O365ObjectCache -searchQuery $adGroupMember.mail
#check if already present in O365Group
if($res.Guid.Guid.Length -gt 1 -and ($o365Group.FastSearch."$($res.Guid.Guid)" -ne 1)){
$obj = New-Object PSObject
$obj | Add-Member NoteProperty mode(2)
$obj | Add-Member NoteProperty DisplayName($o365Group.DisplayName)
$obj | Add-Member NoteProperty targetIdentifier($o365Group.DistinguishedName)
$obj | Add-Member NoteProperty memberIdentifier($res.primarySmtpAddress)
$retVal += $obj
}
}catch{
continue
}
}
#and then TO AD
foreach($o365GroupMember in $o365Group.Members){
Write-Progress -Activity "Comparing groups..." -PercentComplete $percentComplete -Status "Working on $($adGroup.DisplayName) - $($o365GroupMember.primarySmtpAddress)"
#if can't find in the O365 group, add to list
try{
#user has to exist in AD, retrieve user from AD or move to next o365GroupMember
$res = searchADForUserOrGroup -searchQuery $o365GroupMember.primarySmtpAddress -byMail -anyAlias
#check if already present in AD Group
if($res.ObjectGuid.Guid.Length -gt 1 -and ($adGroup.FastSearch."$($res.ObjectGuid.Guid)" -ne 1)){
$obj = New-Object PSObject
$obj | Add-Member NoteProperty mode(1)
$obj | Add-Member NoteProperty DisplayName($adGroup.DisplayName)
$obj | Add-Member NoteProperty targetIdentifier($adGroup.DistinguishedName)
$obj | Add-Member NoteProperty memberIdentifier($res.distinguishedName)
$retVal += $obj
}
}catch{
continue
}
}
$done++
}
Write-Progress -Activity "Comparing groups..." -PercentComplete 100 -Status "DONE!"
return $retVal
}
function provisionADGroup{
Param(
[Parameter(Mandatory=$true)]$o365GroupObject,
[Parameter(Mandatory=$true)]$targetOU
)
#now create the group
$returnData = @{}
try{
if(!$readOnly){
$res = New-ADGroup -Name $o365GroupObject.DisplayName -SamAccountName $o365GroupObject.DisplayName -DisplayName $o365GroupObject.DisplayName -Description "This group was automatically created by O365GroupSync on $(Get-Date)" -GroupScope Universal -GroupCategory Security -PassThru -Path $targetOU -ErrorAction Stop
}
}catch{
Throw "Failed to create $($o365GroupObject.DisplayName), error reported by command: $($Error[0]) $res"
}
$returnData.groupInfo = $res
$returnData.oldGroupInfo = $o365GroupObject
$script:totalChanged++
return $returnData
}
function provisionADGroups{
Param(
[Parameter(Mandatory=$true)]$groups
)
$groupPostProcessingDataSet = @()
foreach($o365NewGroup in $groups){
[Array]$res = @($adDistributionGroups | where {$_.DisplayName -eq $o365NewGroup.DisplayName} | where {$_})
if($res.Count -gt 0){
log -text "Error: $($o365NewGroup.DisplayName) already exists in AD, it was probably already provisioned manually and will be updated next run" -color "Yellow" -warning
continue
}
if($o365NewGroup.DisplayName){
try{
log -text "Sending command to create new group in AD (O365 group: $($o365NewGroup.DisplayName))" -color "Green"
$groupPostProcessingDataSet += (provisionADGroup -o365GroupObject $o365NewGroup -targetOU $targetOU)
log -text "Command completed succesfully, $($o365NewGroup.DisplayName) created" -Color "Green"
}catch{
log -text "Failure reported in Group Provisioning of $($o365NewGroup.DisplayName) in AD" -Color "Red" -append -error
}
}
}
##Process extra commands after groups creation:
foreach($groupPostProcessingData in $groupPostProcessingDataSet){
#owners vertalen
$managers = ""
$managedBy = $groupPostProcessingData.oldGroupInfo.ManagedBy
if($managedBy){
foreach($manager in $managedBy){
try{
$newManager = searchExOForUserOrGroupV2 -O365ObjectCache $script:O365ObjectCache -searchQuery $manager -byAlias
if($newManager.primarySmtpAddress){
$ADRes = searchADForUserOrGroup -searchQuery $newManager.emailAddresses -byMail -anyAlias
if($managers){
Throw "Active Directory only supports 1 manager per group"
}
$managers=$ADRes.distinguishedName
if($verbose) {log -text "will add $($ADRes.distinguishedName) as manager to $($groupPostProcessingData.oldGroupInfo.DisplayName)" -color "Green"}
}else{
Throw "$($newManager.UserPrincipalName) does not have an email address defined and will not be propagated to AD in group $($groupPostProcessingData.oldGroupInfo.DisplayName)"
}
}catch{
log -text "Invalid manager ($manager), cannot be found in Office 365 or AD or does not have an email address or maximum managers reached, will not add to group $($groupPostProcessingData.oldGroupInfo.DisplayName)" -color "Yellow" -append -warning
}
}
}
if($managers){
try{
$result = Set-ADGroup -Identity $groupPostProcessingData.groupInfo.ObjectGuid -ManagedBy $managers -ErrorAction Stop
}catch{
log -text "Failed to set manager ($managers) for $($groupPostProcessingData.oldGroupInfo.DisplayName), error reported by command: $($Error[0]) $result" -color "Yellow" -warning
}
}
#if any cleanup is required, do so here
[Array]$proxyAddressesTemp = @($groupPostProcessingData.oldGroupInfo.EmailAddresses | where {$_})
#strip domains not present onprem
if($domainsPresentLocally){
[Array]$proxyAddressesTemp = @($proxyAddressesTemp | where {(checkIfAddressIsAllowed -mode 2 -address $_)})
}
$proxyAddresses = @()
$proxyAddressesTemp | % {$proxyAddresses += $_.ToString()}
#cache properties or rewrite them according to target needs
if($groupPostProcessingData.oldGroupInfo.HiddenFromAddressListsEnabled -eq $True){
if($verbose) {log -text "Will hide $($groupPostProcessingData.oldGroupInfo.DisplayName)" -color "Green"}
$HiddenFromAddressListsEnabled = "TRUE"
}else{
$HiddenFromAddressListsEnabled = "FALSE"
}
if($groupPostProcessingData.oldGroupInfo.RequireSenderAuthenticationEnabled -eq $True){
if($verbose) {log -text "Will set sender authentication for $($groupPostProcessingData.oldGroupInfo.DisplayName)" -color "Green"}
$RequireSenderAuthenticationEnabled = "TRUE"
}else{
$RequireSenderAuthenticationEnabled = "FALSE"
}
#do some properties
if(!$readOnly){
try{
if($proxyAddresses.Count -lt 1){
Throw "No email aliases could be used as no configured aliases of the source group are known in the target domain"
}
$result = Set-ADGroup -Identity $groupPostProcessingData.groupInfo.ObjectGuid -Replace @{'proxyAddresses'=$proxyAddresses} -ErrorAction Stop
}catch{
log -text "Failed to set proxyAddresses for $($groupPostProcessingData.oldGroupInfo.DisplayName), error reported by command: $($Error[0]) $result" -color "Red" -error
}
if($localCustomAttributeValue){
try{
$result = Set-ADGroup -Identity $groupPostProcessingData.groupInfo.ObjectGuid -Replace @{'extensionAttribute2'=$localCustomAttributeValue} -ErrorAction Stop
log -text "set extensionAttribute2 to $localCustomAttributeValue" -color "Green"
}catch{
log -text "Failed to set extensionAttribute2 to $localCustomAttributeValue : $($Error[0]) $result" -color "Red" -error
}
}
try{
$result = Set-ADGroup -Identity $groupPostProcessingData.groupInfo.ObjectGuid -Replace @{'mailNickname'=$groupPostProcessingData.oldGroupInfo.alias} -ErrorAction Stop
$result = Set-ADGroup -Identity $groupPostProcessingData.groupInfo.ObjectGuid -Replace @{'legacyExchangeDN'=$groupPostProcessingData.oldGroupInfo.legacyExchangeDN} -ErrorAction Stop
}catch{
log -text "Failed to set Exchange properties, this distributionlist will not show up in the Exchange console: $($Error[0]) $result" -color "Yellow" -warning
}
try{
$result = Set-ADGroup -Identity $groupPostProcessingData.groupInfo.ObjectGuid -Replace @{'msExchHideFromAddressLists'="$HiddenFromAddressListsEnabled"} -ErrorAction Stop
}catch{
log -text "Failed to set hide status for $($groupPostProcessingData.oldGroupInfo.DisplayName), error reported by command: $($Error[0]) $result" -color "Red" -error
}
try{
$result = Set-ADGroup -Identity $groupPostProcessingData.groupInfo.ObjectGuid -Replace @{'mail'=$groupPostProcessingData.oldGroupInfo.PrimarySmtpAddress} -ErrorAction Stop
}catch{
log -text "Failed to set email address $($groupPostProcessingData.oldGroupInfo.PrimarySmtpAddress) for $($groupPostProcessingData.oldGroupInfo.DisplayName), error reported by command: $($Error[0]) $result" -color "Red" -error
}
try{
$result = Set-ADGroup -Identity $groupPostProcessingData.groupInfo.ObjectGuid -Replace @{'msExchRequireAuthToSendTo'="$RequireSenderAuthenticationEnabled"} -ErrorAction Stop
}catch{
log -text "Failed to set required sender auth status for $($groupPostProcessingData.oldGroupInfo.DisplayName), error reported by command: $($Error[0]) $result" -color "Red" -error
}
}
#members
$members = @()
try{
[Array]$o365GroupObjectMembers = @($groupPostProcessingData.oldGroupInfo.Members | where {$_})
}catch{
log -text "Failed to retrieve members for $($groupPostProcessingData.oldGroupInfo.DisplayName)" -color "Red" -append -error
}
if($o365GroupObjectMembers.Count -gt 0){
foreach($member in $o365GroupObjectMembers){
try{
$newMember = searchADForUserOrGroup -searchQuery $member.EmailAddresses -byMail -anyAlias
if($newMember.mail){
$members+=$newMember.distinguishedName
if($verbose) {log -text "will add $($newMember.mail) as member to $($groupPostProcessingData.oldGroupInfo.DisplayName)" -color "Green"}
}else{
Throw "$($newMember.UserPrincipalName) does not have an email address defined and will not be added as member of group $($groupPostProcessingData.oldGroupInfo.DisplayName)"
}
}catch{
log -text "Object $member cannot be found in Office 365 or AD or does not have an email address and will not be added to AD group $($groupPostProcessingData.oldGroupInfo.DisplayName)" -color "Red" -append -warning
}
}
}
foreach($member in $members){
Try{
if(!$readOnly){
$result = Add-ADGroupMember -Identity $groupPostProcessingData.groupInfo.ObjectGuid -Members $member -ErrorAction Stop
}
}catch{
log -text "Failed to add $member to $($groupPostProcessingData.oldGroupInfo.DisplayName)" -color "Yellow" -append -warning
}
}
log -text "members added to $($groupPostProcessingData.oldGroupInfo.DisplayName)" -color "Green"
#authorized senders (users, AcceptMessagesOnlyFrom property)
$permittedSendingUsers = @()
if($groupPostProcessingData.oldGroupInfo.AcceptMessagesOnlyFrom){
foreach($member in $groupPostProcessingData.oldGroupInfo.AcceptMessagesOnlyFrom){
try{
$newMember = searchExOForUserOrGroupV2 -O365ObjectCache $script:O365ObjectCache -searchQuery $member -byAlias
if($newMember.primarySmtpAddress){
$ADRes = searchADForUserOrGroup -searchQuery $newMember.emailAddresses -byMail -anyAlias
$permittedSendingUsers+=$ADRes.DistinguishedName
if($verbose) {log -text "will add $($ADRes.displayName) as authorized sender to $($groupPostProcessingData.oldGroupInfo.DisplayName)" -color "Green"}
}else{
Throw "$($newMember.UserPrincipalName) does not have an email address defined and will not be added as authorized sender to group $($groupPostProcessingData.oldGroupInfo.DisplayName)"
}
}catch{
log -text "O365 object $member cannot be found in Office 365 or AD and will not be added as authorized sender to AD group $($groupPostProcessingData.oldGroupInfo.DisplayName)" -color "Yellow" -append -warning
}
}
}
try{
if($permittedSendingUsers.Count -gt 0){
$result = $groupPostProcessingData.groupInfo | Set-ADGroup -Replace @{'authOrig'=$permittedSendingUsers} -ErrorAction Stop
}
}catch{
log -text "Failed to set authOrig (permitted sending users) for $($groupPostProcessingData.oldGroupInfo.DisplayName), error reported by command: $($Error[0]) $result" -color "Yellow" -warning
}
#authorized senders (groups, AcceptMessagesOnlyFromDLMembers property)
$permittedSendingGroups = @()
if($groupPostProcessingData.oldGroupInfo.AcceptMessagesOnlyFromDLMembers){
foreach($member in $groupPostProcessingData.oldGroupInfo.AcceptMessagesOnlyFromDLMembers){
try{
$newMember = searchExOForUserOrGroupV2 -O365ObjectCache $script:O365ObjectCache -searchQuery $member -byAlias
if($newMember.primarySmtpAddress){
$ADRes = searchADForUserOrGroup -searchQuery $newMember.emailAddresses -byMail -anyAlias
$permittedSendingGroups+=$ADRes.DistinguishedName
if($verbose) {log -text "will add $($ADRes.displayName) as authorized sender to $($groupPostProcessingData.oldGroupInfo.DisplayName)" -color "Green"}
}else{
Throw "$($newMember.UserPrincipalName) does not have an email address defined and will not be added as authorized sender to group $($groupPostProcessingData.oldGroupInfo.DisplayName)"
}
}catch{
log -text "O365 object $member cannot be found in Office 365 or AD and will not be added as authorized sender to AD group $($groupPostProcessingData.oldGroupInfo.DisplayName)" -color "Yellow" -append -warning
}
}
}
try{
if($permittedSendingGroups.Count -gt 0){
$result = Set-ADGroup -Identity $groupPostProcessingData.groupInfo.ObjectGuid -Replace @{'dLMemSubmitPerms'=$permittedSendingGroups} -ErrorAction Stop
}
}catch{
log -text "Failed to set authOrig (permitted sending users) for $($groupPostProcessingData.oldGroupInfo.DisplayName), error reported by command: $($Error[0]) $result" -color "Yellow" -warning
}
}
}
function provisionO365Group{
Param(
[Parameter(Mandatory=$true)]$adGroupObject
)
$managers = @()
$managers+=$defaultDLAdminInO365
#cache properties or rewrite them according to target needs
if($adGroupObject.msExchHideFromAddressLists -eq "TRUE"){
if($verbose) {log -text "Will hide $($adGroupObject.DisplayName)" -color "Green"}
$HiddenFromAddressListsEnabled = $True
}else{
$HiddenFromAddressListsEnabled = $False
}
if($adGroupObject.msExchRequireAuthToSendTo -eq "TRUE"){
if($verbose) {log -text "Will set sender authentication for $($adGroupObject.DisplayName)" -color "Green"}
$RequireSenderAuthenticationEnabled = $True
}else{
$RequireSenderAuthenticationEnabled = $False
}
$returnData = @{}
#now create the group
try{
if(!$readOnly){
$res = New-DistributionGroup -Name $adGroupObject.DisplayName -ManagedBy $managers -Type Security -ErrorAction Stop
}
}catch{
Throw "Failed to create $($adGroupObject.DisplayName), error reported by command: $($Error[0]) $res"
}
$returnData.groupInfo = $res
$returnData.oldGroupInfo = $adGroupObject
#if any cleanup is required, do so here
[Array]$proxyAddresses = @($adGroupObject.ProxyAddresses | where {$_})
#strip domains not present in the Cloud
[Array]$proxyAddresses = @($proxyAddresses | where {(checkIfAddressIsAllowed -mode 1 -address $_)})
#add onprem legacyExchangeDN for outlook address cache
if($adGroupObject.LegacyExchangeDN){
if($verbose) {log -text "will add X500:$($adGroupObject.LegacyExchangeDN) as proxy address to $($adGroupObject.DisplayName)" -color "Green"}
$proxyAddresses+="X500:$($adGroupObject.LegacyExchangeDN)"
}
#set proxyAddresses
if(!$readOnly){
try{
if($proxyAddresses.Count -lt 1){
Throw "No email aliases could be used as no configured aliases of the source group are known in Exchange Online"
}
$res = Set-DistributionGroup -Identity $adGroupObject.DisplayName -EmailAddresses $proxyAddresses -ErrorAction Stop
}catch{
log -text "Failed to set aliases for $($adGroupObject.DisplayName), error reported by command: $($Error[0]) $res" -color "Red" -error
}
}
if(!$readOnly){
try{
$res = Set-DistributionGroup -Identity $adGroupObject.DisplayName -HiddenFromAddressListsEnabled $HiddenFromAddressListsEnabled -RequireSenderAuthenticationEnabled $RequireSenderAuthenticationEnabled -ErrorAction Stop
}catch{
log -text "Failed to set hidefromaddresslist and requiresenderauthentication properties on $($adGroupObject.DisplayName), error reported by command: $($Error[0]) $res" -color "Red" -error
}
}
$script:totalChanged++
return $returnData
}
function provisionO365Groups{
Param(
[Parameter(Mandatory=$true)]$groups
)
$groupPostProcessingDataSet = @()
foreach($adNewGroup in $groups){
[Array]$res = @($o365DistributionGroups | where {$_.DisplayName -eq $adNewGroup.DisplayName} | where {$_})
if($res.Count -gt 0){
log -text "Error: $($adNewGroup.DisplayName) already exists in Office 365, it was probably already provisioned manually and will be updated next run" -color "Yellow" -warning
continue
}
if($adNewGroup.DisplayName){
try{
log -text "Sending command to create new group in Office 365 (local AD group: $($adNewGroup.DisplayName))" -color "Green"
$groupPostProcessingDataSet += (provisionO365Group -adGroupObject $adNewGroup)
log -text "Command completed succesfully, $($adNewGroup.DisplayName) created" -Color "Green"
}catch{
log -text "Failure reported in Group Provisioning of $($adNewGroup.DisplayName) in Office 365" -Color "Red" -append -error
}
}else{
log -text "Error, group does not have a displayname" -color "Red" -append -error
}
}
#region cache O365 objects
try{
log -text "Caching current O365 objects..." -color "Green"
$script:O365ObjectCache = cacheO365Objects
log -text "Cached $($O365ObjectCache.Mailboxes.Count) mailboxes and $($O365ObjectCache.Groups.Count) groups in O365" -color "Green"
}catch{
log -text "Failed to cache O365 objects" -color "Red" -append -error
abort_GS -failed
}#endregion
foreach($groupPostProcessingData in $groupPostProcessingDataSet){
#owners vertalen
$managers = @()
$managedBy = $groupPostProcessingData.oldGroupInfo.ManagedBy
if($managedBy){
foreach($manager in $managedBy){
try{
$newManager = searchADForUserOrGroup -searchQuery $manager
if($newManager.mail){
$o365Res = searchExOForUserOrGroupV2 -O365ObjectCache $script:O365ObjectCache -searchQuery $newManager.mail
$managers+=$o365Res.primarySmtpAddress
if($verbose) {log -text "will add $($o365Res.primarySmtpAddress) as manager to $($groupPostProcessingData.oldGroupInfo.DisplayName)" -color "Green"}
}else{
Throw "$($newManager.UserPrincipalName) does not have an email address defined and will not be propagated to Office in group $($groupPostProcessingData.oldGroupInfo.DisplayName)"
}
}catch{
log -text "Invalid manager for group $($groupPostProcessingData.oldGroupInfo.DisplayName), cannot be found in Office 365 or AD or does not have an email address" -color "Yellow" -append -warning
}
}
if($managers.Count -lt 1){
$managers+=$defaultDLAdminInO365
log -text "No managers set in AD for group $($groupPostProcessingData.oldGroupInfo.DisplayName), defaulting to $defaultDLAdminInO365" -color "Yellow" -append -warning
}
}else{
$managers+=$defaultDLAdminInO365
}
#members
$members = @()
if($groupPostProcessingData.oldGroupInfo.MemberObjects){
foreach($member in $groupPostProcessingData.oldGroupInfo.MemberObjects){
try{
$newMember = searchADForUserOrGroup -searchQuery $member.mail -byMail -anyAlias
if($newMember.mail){
if($newMember.userAccountControl -band 2 -and $skipDisabledAccounts){
continue
}
$o365Res = searchExOForUserOrGroupV2 -O365ObjectCache $script:O365ObjectCache -searchQuery $newMember.mail
$members+=$o365Res.primarySmtpAddress
if($verbose) {log -text "will add $($o365Res.primarySmtpAddress) as member to $($groupPostProcessingData.groupInfo.DisplayName)" -color "Green"}
}else{
Throw "$($newMember.UserPrincipalName) does not have an email address defined and will not be added as member of group $($groupPostProcessingData.groupInfo.DisplayName)"
}
}catch{
log -text "AD object $($member.cn) cannot be found in Office 365 or AD or does not have an email address and will not be added to O365 group $($groupPostProcessingData.groupInfo.DisplayName)" -color "Red" -append -warning
}
}
if($members){
try{
Update-DistributionGroupMember -Identity $groupPostProcessingData.groupInfo.DisplayName -ByPassSecurityGroupManagerCheck -Confirm:$False -Members $members
log -text "Members added to $($groupPostProcessingData.groupInfo.DisplayName)" -Color "Green"
}catch{
log -text "Failed to add members to $($groupPostProcessingData.groupInfo.DisplayName)" -color "Red" -append -error
}
}
}
#authorized senders (users, authOrig property)
$permittedSendingUsersOrGroups = @()
if($groupPostProcessingData.oldGroupInfo.authOrig){
foreach($member in $groupPostProcessingData.oldGroupInfo.authOrig){
try{
$newMember = searchADForUserOrGroup -searchQuery $member
if($newMember.mail){
if($newMember.userAccountControl -band 2 -and $skipDisabledAccounts){
continue
}
$o365Res = searchExOForUserOrGroupV2 -O365ObjectCache $script:O365ObjectCache -searchQuery $newMember.mail
$permittedSendingUsersOrGroups+=$o365Res.primarySmtpAddress
if($verbose) {log -text "will add $($o365Res.primarySmtpAddress) as authorized sender to $($groupPostProcessingData.groupInfo.DisplayName)" -color "Green"}
}else{
Throw "$($newMember.UserPrincipalName) does not have an email address defined and will not be added as authorized sender to group $($groupPostProcessingData.groupInfo.DisplayName)"
}
}catch{
log -text "AD object $($member.cn) cannot be found in Office 365 or AD or does not have an email address and will not be added as authorized sender to O365 group $($groupPostProcessingData.groupInfo.DisplayName)" -color "yellow" -append -warning
}
}
}
#authorized senders (groups, dlMemSubmitPerms)
if($groupPostProcessingData.oldGroupInfo.dLMemSubmitPerms){
foreach($member in $groupPostProcessingData.oldGroupInfo.dLMemSubmitPerms){
try{
$newMember = searchADForUserOrGroup -searchQuery $member
if($newMember.mail){
if($newMember.userAccountControl -band 2 -and $skipDisabledAccounts){
continue
}
$o365Res = searchExOForUserOrGroupV2 -O365ObjectCache $script:O365ObjectCache -searchQuery $newMember.mail
$permittedSendingUsersOrGroups+=$o365Res.primarySmtpAddress
if($verbose) {log -text "will add $($o365Res.primarySmtpAddress) as authorized sender to $($groupPostProcessingData.groupInfo.DisplayName)" -color "Green"}
}else{
Throw "$($newMember.UserPrincipalName) does not have an email address defined and will not be added as authorized sender to group $($groupPostProcessingData.groupInfo.DisplayName)"
}
}catch{
log -text "AD object $($member.cn) cannot be found in Office 365 or AD or does not have an email address and will not be added as authorized sender to O365 group $($groupPostProcessingData.groupInfo.DisplayName)" -color "Yellow" -append -warning
}
}
}
#set CustomAttribute2:
if($cloudCustomAttributeValue){
try{
$res = Set-DistributionGroup -Identity $groupPostProcessingData.groupInfo.DisplayName -ByPassSecurityGroupManagerCheck -CustomAttribute2 $cloudCustomAttributeValue -Confirm:$False -ErrorAction Stop
log -text "Set customAttribute2 to $cloudCustomAttributeValue for $($groupPostProcessingData.groupInfo.DisplayName)" -color "Green"
}catch{
log -text "Failed to set customAttribute2 to $cloudCustomAttributeValue $($Error[0]) $res" -color "Red" -error
}
}
#add permitted senders
if($permittedSendingUsersOrGroups.Count -gt 0){
try{
$res = Set-DistributionGroup -Identity $groupPostProcessingData.groupInfo.DisplayName -ManagedBy $managers -ByPassSecurityGroupManagerCheck -Confirm:$False -AcceptMessagesOnlyFromSendersOrMembers $permittedSendingUsersOrGroups -ErrorAction Stop
log -text "Set authorized senders for $($groupPostProcessingData.groupInfo.DisplayName)" -color "Green"
}catch{
log -text "Failed to set authorized senders $($Error[0]) $res" -color "Yellow" -warning
}
}
}
}
try{
preventDoubleSchedule
}catch{
write-host "FAILED TO PREVENT DOUBLE INSTANCES OF THIS SCRIPT" -ForegroundColor Red
$totalErrors++
}
try{
$script:logFileStream = New-Object IO.FileStream([System.IO.Path]::Combine($logPath), [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [IO.FileShare]::Read)
$script:logFileStreamWriter = New-Object System.IO.StreamWriter($script:logFileStream)
log -text "O365 Group Sync v$version - $($env:USERNAME) on $($env:COMPUTERNAME) starting-----" -color "Green"
log -text "Log path: $logPath" -color "Green"
log -text "Cache database files should be located in: $cacheFolderPath" -color "Green"
if($readOnly){log -text "Running in READ-ONLY mode, no actual changes will occur" -color "Yellow" -warning}
log -text "syncDirection is set to $syncDirection" -color "Green"
log -text "---------------------------------------------" -color "Green"
}catch{
$totalErrors++
abort_GS -failed
}
#do a version check if allowed
if($versionCheck){
try{
versionCheck -currentVersion $version
log -text "you are running the latest (v$version) version of O365GroupSync" -color "Green"
}catch{
log -text "ERROR: $($Error[0])" -color "Red" -error
}
}
$ADCachePath = Join-Path -Path $cacheFolderPath -ChildPath "O365GroupSync_AD.cache"
$O365CachePath = Join-Path -Path $cacheFolderPath -ChildPath "O365GroupSync_O365.cache"
#region check parameters
if(!$sourceOU -or !$targetOU){
log -text "sourceOU or targetOU not specified in script configuration, exiting" -color "Red" -error
abort_GS -failed
}#endregion
#region load AD cache from file if it exists
if([System.IO.File]::Exists($ADCachePath)){
try{
log -text "Loading data from $ADCachePath" -color "Green"
[Array]$ADCache = @(Import-Clixml -Path $ADCachePath -ErrorAction Stop | where {$_})
log -text "Distribution Groups loaded from $ADCachePath, $($ADCache.Count) objects found" -color "Green"
}catch{
$ADCache = $False
log -text "Failed to load groups from $ADCachePath, file may have been corrupted, a new cache will be created" -color "Red" -append -error
if($differential){
log -text "-differential was supplied as script argument, this is an invalid option when the Cache cannot be found" -color "Red" -error
}
}
}else{
$ADCache = $False
log -text "No cache file for Active Directory found, if this is a first run this is to be expected" -color "Green"
}#endregion
#region load O365 cache from file if it exists
if([System.IO.File]::Exists($O365CachePath)){
try{
log -text "Loading data from $O365CachePath" -color "Green"
[Array]$O365Cache = @(Import-Clixml -Path $O365CachePath -ErrorAction Stop | where {$_})
log -text "Distribution Groups loaded from $O365CachePath, $($O365Cache.Count) objects found" -color "Green"
}catch{
$O365Cache = $False
log -text "Failed to load groups from $O365CachePath, file may have been corrupted, a new cache will be created" -color "Red" -append -error
if($differential){
log -text "-differential was supplied as script argument, this is an invalid option when the Cache cannot be found" -color "Red" -error
}
}
}else{
$O365Cache = $False
log -text "No cache file for Office 365 found, if this is a first run this is to be expected" -color "Green"
}#endregion
#region connect to local AD
try{
ipmo ActiveDirectory -Force -ErrorAction Stop
log -text "Active Directory connection: OK" -color "Green"
}catch{
log -text "failed to load Active Directory Module" -color "Red" -append -error
abort_GS -failed
}#endregion
#region cache objects in local AD
try{
log -text "Caching current AD objects..." -color "Green"
[Array]$ADObjectCache = @(cacheADObjects | where {$_})
if($ADObjectCache.Count -lt 1){Throw "No objects found in AD!"}
log -text "Cached $($ADObjectCache.Count) objects in AD" -color "Green"
}catch{
log -text "Failed to cache current objects in local AD" -color "Red" -append -error
abort_GS -failed
}#endregion
#region evaluate local email domains
if($domainsPresentLocally -eq ""){
$domainsPresentLocally = @(retrieveOnpremAcceptedDomains -ADObjectCache $ADObjectCache | where {$_})
}else{
log -text "Accepted Domains in Active Directory have been manually specified" -color "Green"
$domainsPresentLocally = @($domainsPresentLocally.Split(",",[System.StringSplitOptions]::RemoveEmptyEntries) | where {$_})
}
log -text "the following domains will be used as accepted domains in Active Directory: $($domainsPresentLocally -Join ",")" -color "Green"
#endregion
#region cache O365 objects
try{
log -text "Caching current O365 objects..." -color "Green"
$O365ObjectCache = cacheO365Objects
log -text "Cached $($O365ObjectCache.Mailboxes.Count) mailboxes and $($O365ObjectCache.Groups.Count) groups in O365" -color "Green"
}catch{
log -text "Failed to cache O365 objects" -color "Red" -append -error
abort_GS -failed
}#endregion
#region check that our default admin actually exists
try{
$res = searchExOForUserOrGroupV2 -O365ObjectCache $O365ObjectCache -searchQuery $defaultDLAdminInO365
log -text "Validated existence of $defaultDLAdminInO365 in Office 365" -color "Green"
}catch{
log -text "$defaultDLAdminInO365 could not be found in Office 365, please correctly configure the defaultDLAdminInO365 variable in this script's configuration section" -Color "Red" -error
abort_GS -failed
}#endregion
#region retrieve Exchange Online domains
if($domainsPresentInCloud -eq ""){
$domainsPresentInCloud = @(retrieveRemoteAcceptedDomains | where {$_})
}else{
log -text "Accepted Domains in Exchange Online have been manually specified" -color "Green"
$domainsPresentInCloud = @($domainsPresentInCloud.Split(",",[System.StringSplitOptions]::RemoveEmptyEntries) | where {$_})
}
log -text "the following domains will be used as accepted domains in Exchange Online: $($domainsPresentInCloud -Join ",")" -color "Green"
#endregion
#region load local distribution groups
try{
log -text "Loading distribution groups and their members from AD" -color "Green"
[Array]$adDistributionGroups = @(retrieveADGroupsAndMembers -sourceOU $sourceOU | where {$_})
log -text "Loaded $($adDistributionGroups.Count) groups from AD" -color "Green"
}catch{
log -text "There was a problem loading Distribution Groups" -color "Red" -append -error
abort_GS -failed
}#endregion
#region autoRemediateGroupDisplayNames if necessary and set
if($autoRemediateGroupDisplayNames){
log -text "autoRemediateGroupDisplayNames was set to True, checking for groups without a displayName or where the display name does not match the CN" -color "Green"
$changed = $False
foreach($adGroup in $adDistributionGroups){
if($adGroup.displayName.Length -lt 1 -or $adGroup.displayName -eq $Null -or $adGroup.displayName -ne $adGroup.cn){
try{
if(!$readOnly){
$res = Set-ADGroup -Identity $adGroup.objectGuid -DisplayName $adGroup.cn -ErrorAction Stop
$changed = $true
}
log -text "set displayName for $($adGroup.distinguishedName) to $($adGroup.cn) because the display name was not properly configured" -color "Green"
}catch{
log -text "Failed to set displayName for $($adGroup.distinguishedName) to $($adGroup.cn)" -color "Red" -append -error
}
}
}
if($changed){
#region load local distribution groups
try{
log -text "Loading distribution groups and their members from AD" -color "Green"
[Array]$adDistributionGroups = @(retrieveADGroupsAndMembers -sourceOU $sourceOU | where {$_})
log -text "Loaded $($adDistributionGroups.Count) groups from AD" -color "Green"
}catch{
log -text "There was a problem loading Distribution Groups" -color "Red" -append -error
abort_GS -failed
}#endregion
}
}#endregion
#region load O365 distribution groups
try{
log -text "Loading distribution groups and their members from O365" -color "Green"
[Array]$o365DistributionGroups = @(retrieveO365GroupsAndMembers | where {$_})
log -text "Loaded $($o365DistributionGroups.Count) groups from O365" -color "Green"
}catch{
log -text "There was a problem loading Distribution Groups from O365" -color "Red" -append -error
abort_GS -failed
}#endregion
#region validate at least 1 side has groups
if($o365DistributionGroups.Count -lt 1 -and $adDistributionGroups.Count -lt 1){
log -text "No groups detected on both sides, please ensure you have selected the right source AD OU('s) and target O365 tenant" -color "Red" -error
abort_GS -failed
}#endregion
#region if there is an AD cache file, let's see what has changed since the moment the cache was generated -SyncWindow ($adDistributionGroups.Count/2)
if($ADCache -and $differential){
$adChanges = 0
try{
log -text "Comparing AD cache with current AD..." -color "Green"
#detect new groups
[Array]$adNewGroups = @(returnNewGroups -referenceGroups $ADCache -currentGroups $adDistributionGroups -mode 2 | where {$_})
if($adNewGroups[0].ObjectGuid.Guid.Length -gt 0){
log -text "found $($adNewGroups.Count) new groups" -color "Green"
$adChanges+=$adNewGroups.Count
}else{
log -text "Did not find any new groups" -color "Green"
$adNewGroups = $False
}
#detect deleted groups
[Array]$adDeletedGroups = @(returnDeletedGroups -referenceGroups $ADCache -currentGroups $adDistributionGroups -mode 2 | where {$_})
if($adDeletedGroups[0].ObjectGuid.Guid.Length -gt 0){
log -text "found $($adDeletedGroups.Count) deleted groups" -color "Green"
$adChanges+=$adDeletedGroups.Count
}else{
log -text "Did not find any deleted groups" -color "Green"
$adDeletedGroups = $False
}
#stop if max deletes is triggered
if($maxProjectedDeletesProtection -gt 0 -and $maxProjectedDeletesProtection -le $adDeletedGroups.Count){
log -text "You've set maxProjectedDeletesProtection to $maxProjectedDeletesProtection, while $($adDeletedGroups.Count) deletes are pending. Manually delete these groups, clear the cache and run O365GroupSync again. script terminated" -error
abort_GS -failed
}
#detect group name changes
try{
[Array]$adRenamedGroups = @(returnRenamedGroups -referenceGroups $ADCache -differenceGroups $adDistributionGroups -mode 1 | where {$_})
if($adRenamedGroups[0].newIdentifier.Length -gt 0){
log -text "found $($adRenamedGroups.Count) renamed groups" -color "Green"
$adChanges+=$adRenamedGroups.Count
}else{
log -text "Did not find any renamed groups" -color "Green"
$adRenamedGroups = $False
}
}catch{
log -text "There was a problem comparing groups in the AD cache with the current AD" -color "Red" -append -error
$adRenamedGroups = $False
}
#detect changed proxyAddresses (aliases)
[Array]$adGroupProxyAddressChanges = @(returnChangedProxyAddresses -referenceGroups $ADCache -differenceGroups $adDistributionGroups -mode 2| where {$_})
if($adGroupProxyAddressChanges[0].mode -gt 0){
log -text "found $($adGroupProxyAddressChanges.Count) proxyAddress changes to process" -color "Green"
$adChanges+=$adGroupProxyAddressChanges.Count
}else{
log -text "Did not find any proxyAddress changes to process" -color "Green"
$adGroupProxyAddressChanges = $False
}
#detect changed authorized senders
[Array]$adGroupAuthorizedSenderChanges = @(returnChangedAuthorizedSenders -referenceGroups $ADCache -differenceGroups $adDistributionGroups -mode 2| where {$_})
if($adGroupAuthorizedSenderChanges[0].mode -gt 0){
log -text "found $($adGroupAuthorizedSenderChanges.Count) authorized sender changes to process" -color "Green"
$adChanges+=$adGroupAuthorizedSenderChanges.Count
}else{
log -text "Did not find an authorized sender changes to process" -color "Green"
$adGroupAuthorizedSenderChanges = $False
}
#detect groups that have changed memberships
[Array]$adGroupMemberChanges = @(returnChangedMembersInADGroups -referenceGroups $ADCache -differenceGroups $adDistributionGroups | where {$_})
if($adGroupMemberChanges[0].mode -gt 0){
log -text "found $($adGroupMemberChanges.Count) membership changes to process" -color "Green"
$adChanges+=$adGroupMemberChanges.Count
}else{
log -text "Did not find any membership changes to process" -color "Green"
$adGroupMemberChanges = $False
}
$script:totalChanges+=$adChanges
}catch{
log -text "Failed to compare AD cache with current AD, will ignore the cache" -color "Red" -append -error
$adChanges = 0
}
}else{
$adChanges = 0
log -text "No changes detected between AD cache and current AD" -color "Green"
}#endregion
#region if there is an O365 Cache file, let's see what has changed since the moment the cache was generated
if($O365Cache -and $differential){
$O365Changes = 0
try{
log -text "Comparing O365 cache with current O365..." -color "Green"
#detect new groups
[Array]$O365NewGroups = @(returnNewGroups -referenceGroups $O365Cache -currentGroups $o365DistributionGroups -mode 1 | where {$_})
if($O365NewGroups[0].Guid.Guid.Length -gt 0){
log -text "found $($O365NewGroups.Count) new groups" -color "Green"
$O365Changes+=$O365NewGroups.Count
}else{
log -text "Did not find any new groups" -color "Green"
$O365NewGroups = $False
}
#detect deleted groups
[Array]$O365DeletedGroups = @(returnDeletedGroups -referenceGroups $O365Cache -currentGroups $o365DistributionGroups -mode 1 | where {$_})
if($O365DeletedGroups[0].Guid.Guid.Length -gt 0){
log -text "found $($O365DeletedGroups.Count) deleted groups" -color "Green"
$O365Changes+=$O365DeletedGroups.Count
}else{
log -text "Did not find any deleted groups" -color "Green"
$O365DeletedGroups = $False
}
#stop if max deletes is triggered
if($maxProjectedDeletesProtection -gt 0 -and $maxProjectedDeletesProtection -le $O365DeletedGroups.Count){
log -text "You've set maxProjectedDeletesProtection to $maxProjectedDeletesProtection, while $($O365DeletedGroups.Count) deletes are pending. Manually delete these groups, clear the cache and run O365GroupSync again. script terminated" -error
abort_GS -failed
}
#detect renamed groups
try{
[Array]$O365RenamedGroups = @(returnRenamedGroups -referenceGroups $O365Cache -differenceGroups $o365DistributionGroups -mode 2 | where {$_})
if($O365RenamedGroups[0].newIdentifier.Length -gt 0){
log -text "found $($O365RenamedGroups.Count) renamed groups" -color "Green"
$O365Changes+=$O365RenamedGroups.Count
}else{
log -text "Did not find any renamed groups" -color "Green"
$O365RenamedGroups = $False
}
}catch{
log -text "There was a problem comparing groups in the O365 cache with the current O365 data" -color "Red" -append -error
$O365RenamedGroups = $False
}
#detect changed emailAddresses (aliases)
[Array]$o365GroupEmailAddressChanges = @(returnChangedProxyAddresses -referenceGroups $O365Cache -differenceGroups $o365DistributionGroups -mode 1| where {$_})
if($o365GroupEmailAddressChanges[0].mode -gt 0){
log -text "found $($o365GroupEmailAddressChanges.Count) emailAddress changes to process" -color "Green"
$O365Changes+=$o365GroupEmailAddressChanges.Count
}else{
$o365GroupEmailAddressChanges = $False
log -text "Did not find any emailAddress changes to process" -color "Green"
}
#detect changed authorized senders
[Array]$o365GroupAuthorizedSenderChanges = @(returnChangedAuthorizedSenders -referenceGroups $O365Cache -differenceGroups $o365DistributionGroups -mode 1| where {$_})
if($o365GroupAuthorizedSenderChanges[0].mode -gt 0){
log -text "found $($o365GroupAuthorizedSenderChanges.Count) authorized sender changes to process" -color "Green"
$O365Changes+=$o365GroupAuthorizedSenderChanges.Count
}else{
$o365AuthorizedSenderChanges = $False
log -text "Did not find any authorized sender changes to process" -color "Green"
}
#detect groups that have changed memberships
[Array]$O365GroupMemberChanges = @(returnChangedMembersInO365Groups -referenceGroups $O365Cache -differenceGroups $o365DistributionGroups | where {$_})
if($O365GroupMemberChanges[0].mode -gt 0){
log -text "found $($O365GroupMemberChanges.Count) membership changes to process" -color "Green"
$O365Changes+=$O365GroupMemberChanges.Count
}else{
$O365GroupMemberChanges = $False
log -text "Did not find any membership changes to process" -color "Green"
}
$script:totalChanges+=$O365Changes
}catch{
log -text "Failed to compare O365 cache with current O365, will ignore the cache" -color "Red" -append -error
$O365Changes = 0
}
}else{
$O365Changes = 0
log -text "No changes detected between O365 cache and current O365" -color "Green"
}#endregion
#region process changes from AD to Office 365
if($adChanges -gt 0){
log -text "Processing $adChanges differential changes to Office 365 from AD" -color "Green"
if($syncDirection -ne 3){
##RENAME GROUPS
if($adRenamedGroups[0].newIdentifier){
foreach($renamedGroup in $adRenamedGroups){
try{
if(!$readOnly){
$res = Set-DistributionGroup -Identity $renamedGroup.newIdentifier -DisplayName $renamedGroup.newDisplayName -BypassSecurityGroupManagerCheck -Confirm:$False -ErrorAction Stop
}
$script:totalChanged++
##fix cache manually so we don't need to re-cache objects
$count = 0
foreach($group in $O365ObjectCache.Groups){
if($group.DisplayName -eq $renamedGroup.newIdentifier){
$O365ObjectCache.Groups[$count].DisplayName = $renamedGroup.newDisplayName
break
}
$count++
}
log -text "Renamed O365 group to $($renamedGroup.newDisplayName) (group id: $($renamedGroup.newIdentifier))" -color "Green"
}catch{
log -text "There was a problem renaming $($renamedGroup.newIdentifier) to $($renamedGroup.newDisplayName) $res" -color "Red" -append -error
}
}
}
##ADD NEW GROUPS
if($adNewGroups[0].DisplayName){
log -text "Provisioning $($adNewGroups.Count) groups" -color "Green"
provisionO365Groups -groups $adNewGroups
}
##DELETE GROUPS
if($adDeletedGroups[0].ObjectGuid.Guid){
if($adDeletedGroups.Count -ge $o365DistributionGroups.Count){
log -text "Multi-delete protection triggered, you are trying to delete ALL groups in scope, please do so manually, to protect your data this script will not delete all groups" -color "Red" -error
abort_GS -failed
}
foreach($deletedGroup in $adDeletedGroups){
try{
if(!$readOnly){
$res = Remove-DistributionGroup -Identity $deletedGroup.DisplayName -BypassSecurityGroupManagerCheck -Confirm:$False
}
$script:totalChanged++
log -text "Deleted O365 group: $($deletedGroup.DisplayName)" -color "Green"
}catch{
log -text "There was a problem deleting $($deletedGroup.DisplayName) $res" -color "Red" -append -error
}
}
}
##CHANGED PROXYADDRESSES
if($adGroupProxyAddressChanges[0].mode){
foreach($adGroupProxyAddressChange in $adGroupProxyAddressChanges){
try{
$targetGroup = searchExOForUserOrGroupV2 -O365ObjectCache $O365ObjectCache -searchQuery $adGroupProxyAddressChange.DisplayName -byDisplayName
}catch{
log -text "Failed to find $($adGroupProxyAddressChange.DisplayName) in O365" -color "Red" -error
continue
}
if($adGroupProxyAddressChange.mode -eq 1){ #delete
try{
if(!$readOnly){
$res = Set-DistributionGroup -Identity $targetGroup.Guid.Guid -EmailAddresses @{Remove=$adGroupProxyAddressChange.proxyAddress} -Confirm:$False -ErrorAction Stop
}
$script:totalChanged++
log -text "Removed $($adGroupProxyAddressChange.proxyAddress) from $($adGroupProxyAddressChange.DisplayName)." -color "Green"
}catch{
log -text "Failed to remove $($adGroupProxyAddressChange.proxyAddress) from $($adGroupProxyAddressChange.DisplayName). $res" -color "Red" -append -error
}
}
if($adGroupProxyAddressChange.mode -eq 2){ #add
try{
if(!$readOnly){
if($adGroupProxyAddressChange.proxyAddress -clike "SMTP:*"){
$res = Set-DistributionGroup -Identity $targetGroup.Guid.Guid -PrimarySmtpAddress $adGroupProxyAddressChange.proxyAddress.substring(5) -Confirm:$False -ErrorAction Stop
}else{
$res = Set-DistributionGroup -Identity $targetGroup.Guid.Guid -EmailAddresses @{Add=$adGroupProxyAddressChange.proxyAddress} -Confirm:$False -ErrorAction Stop
}
}
$script:totalChanged++
log -text "Added $($adGroupProxyAddressChange.proxyAddress) to $($adGroupProxyAddressChange.DisplayName)." -color "Green"
}catch{
log -text "Failed to add $($adGroupProxyAddressChange.proxyAddress) to $($adGroupProxyAddressChange.DisplayName). $res" -color "Red" -append -error
}
}
}
}
#CHANGED DELEGATES
if($adGroupAuthorizedSenderChanges[0].mode){
foreach($adGroupAuthorizedSenderChange in $adGroupAuthorizedSenderChanges){
try{
$targetGroup = searchExOForUserOrGroupV2 -O365ObjectCache $O365ObjectCache -searchQuery $adGroupAuthorizedSenderChange.DisplayName -byDisplayName
}catch{
log -text "Failed to find $($adGroupAuthorizedSenderChange.DisplayName) in O365" -color "Red" -error
continue
}
try{
$sourceSender = searchAdForUserOrGroup -searchQuery $adGroupAuthorizedSenderChange.authorizedSender
$targetSender = searchExOForUserOrGroupV2 -O365ObjectCache $O365ObjectCache -searchQuery $sourceSender.mail
}catch{
log -text "Failed to find $($adGroupAuthorizedSenderChange.authorizedSender) in O365 or AD" -color "Red" -error
continue
}
if($adGroupAuthorizedSenderChange.mode -eq 1){ #delete single user
try{
if(!$readOnly){
$newValues = $targetGroup.AcceptMessagesOnlyFrom -notmatch $targetSender.Alias
$res = Set-DistributionGroup -Identity $targetGroup.Guid.Guid -AcceptMessagesOnlyFrom $newValues -Confirm:$False -ErrorAction Stop
}
log -text "Removed $($targetSender.Alias) from $($targetGroup.DisplayName)." -color "Green"
}catch{
log -text "Failed to remove $($targetSender.Alias) from $($targetGroup.DisplayName). $res" -color "Red" -append -error
}
}
if($adGroupAuthorizedSenderChange.mode -eq 2){ #add single user
try{
if(!$readOnly){
$newValues = $targetGroup.AcceptMessagesOnlyFrom + $targetSender.Alias
$res = Set-DistributionGroup -Identity $targetGroup.Guid.Guid -AcceptMessagesOnlyFrom $newValues -Confirm:$False -ErrorAction Stop
$script:totalChanged++
}
log -text "Added $($targetSender.Alias) to $($targetGroup.DisplayName)." -color "Green"
}catch{
log -text "Failed to add $($targetSender.Alias) to $($targetGroup.DisplayName). $res" -color "Red" -append -error
}
}
if($adGroupAuthorizedSenderChange.mode -eq 3){ #delete single group
try{
if(!$readOnly){
$newValues = $targetGroup.AcceptMessagesOnlyFromDLMembers -notmatch $targetSender.Alias
$res = Set-DistributionGroup -Identity $targetGroup.Guid.Guid -AcceptMessagesOnlyFromDLMembers $newValues -Confirm:$False -ErrorAction Stop
$script:totalChanged++
}
log -text "Removed $($targetSender.Alias) from $($targetGroup.DisplayName)." -color "Green"
}catch{
log -text "Failed to remove $($targetSender.Alias) from $($targetGroup.DisplayName). $res" -color "Red" -append -error
}
}
if($adGroupAuthorizedSenderChange.mode -eq 4){ #add single group
try{
if(!$readOnly){
$newValues = $targetGroup.AcceptMessagesOnlyFromDLMembers + $targetSender.Alias
$res = Set-DistributionGroup -Identity $targetGroup.Guid.Guid -AcceptMessagesOnlyFromDLMembers $newValues -Confirm:$False -ErrorAction Stop
$script:totalChanged++
}
log -text "Added $($targetSender.Alias) to $($targetGroup.DisplayName)." -color "Green"
}catch{
log -text "Failed to add $($targetSender.Alias) to $($targetGroup.DisplayName). $res" -color "Red" -append -error
}
}
}
}
##PROCESS MEMBERSHIP CHANGES
if($adGroupMemberChanges[0].mode){
foreach($adGroupMemberChange in $adGroupMemberChanges){
if($adGroupMemberChange.mode -eq 1){
try{
$user = searchExOForUserOrGroupV2 -O365ObjectCache $O365ObjectCache -searchQuery $adGroupMemberChange.memberMail
if(!$readOnly){
$res = Remove-DistributionGroupMember -Identity $adGroupMemberChange.DisplayName -Member $adGroupMemberChange.memberMail -BypassSecurityGroupManagerCheck -Confirm:$False -ErrorAction Stop
$script:totalChanged++
}
log -text "Removed $($adGroupMemberChange.memberMail) from $($adGroupMemberChange.DisplayName)." -color "Green"
}catch{
log -text "Failed to remove $($adGroupMemberChange.memberMail) from $($adGroupMemberChange.DisplayName). $res" -color "Yellow" -append -warning
}
}
if($adGroupMemberChange.mode -eq 2){
if($adGroupMemberChange.userAccountControl -band 2 -and $skipDisabledAccounts){
continue
}
try{
$user = searchExOForUserOrGroupV2 -O365ObjectCache $O365ObjectCache -searchQuery $adGroupMemberChange.memberMail
if(!$readOnly){
$res = Add-DistributionGroupMember -Identity $adGroupMemberChange.DisplayName -Member $adGroupMemberChange.memberMail -BypassSecurityGroupManagerCheck -Confirm:$False -ErrorAction Stop
$script:totalChanged++
}
log -text "Added $($adGroupMemberChange.memberMail) to $($adGroupMemberChange.DisplayName)." -color "Green"
}catch{
log -text "Failed to add $($adGroupMemberChange.memberMail) to $($adGroupMemberChange.DisplayName). $res" -color "Yellow" -append -warning
}
}
}
}
#RESET CACHE
#region cache O365 objects
try{
log -text "Caching current O365 objects..." -color "Green"
$O365ObjectCache = cacheO365Objects
log -text "Cached $($O365ObjectCache.Mailboxes.Count) mailboxes and $($O365ObjectCache.Groups.Count) groups in O365" -color "Green"
}catch{
log -text "Failed to cache O365 objects" -color "Red" -append -error
abort_GS -failed
}#endregion
try{
log -text "Propagated all recent changes from AD to O365, re-caching data from O365" -color "Green"
[Array]$o365DistributionGroups = @(retrieveO365GroupsAndMembers | where {$_})
log -text "Loaded $($o365DistributionGroups.Count) groups from O365" -color "Green"
$res = Remove-Item $ADCachePath -Force -Recurse -Confirm:$False
log -text "Removed AD Cache File" -color "Green"
}catch{
log -text "There was a problem resetting the cache while processing changes to O365 $res" -color "Red" -append -error
abort_GS -failed
}
}else{
log -text "Changes detected in AD since last run, but sync direction is set to O365 > AD, ignoring changes in AD" -color "Green" -warning
}
}#endregion
#region process changes from O365 to AD
if($O365Changes -gt 0){
log -text "Processing $O365Changes differential changes to AD from Office 365" -color "Green"
if($syncDirection -ne 2){
##RENAME GROUPS
if($O365RenamedGroups[0].newIdentifier){
foreach($renamedGroup in $O365RenamedGroups){
try{
$targetGroup = searchADForUserOrGroup -searchQuery $renamedGroup.newIdentifier
}catch{
log -text "Failed to find $($renamedGroup.newIdentifier) in AD" -color "Red" -error
continue
}
try{
if(!$readOnly){
$res = Set-ADGroup -Identity $targetGroup.DistinguishedName -DisplayName $renamedGroup.newDisplayName -ErrorAction Stop
$script:totalChanged++
}
log -text "Renamed AD group to $($renamedGroup.newDisplayName) (group id: $($targetGroup.DistinguishedName))" -color "Green"
}catch{
log -text "There was a problem renaming $($targetGroup.DistinguishedName) to $($renamedGroup.newDisplayName) $res" -color "Red" -append -error
}
}
}
##ADD NEW GROUPS
if($O365NewGroups[0].DisplayName){
provisionADGroups -groups $O365NewGroups
}
##DELETE GROUPS
if($O365DeletedGroups[0].Guid.Guid){
if($O365DeletedGroups.Count -ge $adDistributionGroups.Count){
log -text "Multi-delete protection triggered, you are trying to delete ALL groups in scope, please do so manually, to protect your data this script will not delete all groups" -color "Red" -error
abort_GS -failed
}
foreach($deletedGroup in $O365DeletedGroups){
try{
$targetGroup = searchADForUserOrGroup -searchQuery $deletedGroup.displayName
}catch{
log -text "Failed to find $($deletedGroup.displayName) in AD" -color "Red" -error
continue
}
try{
if(!$readOnly){
$res = Remove-ADGroup -Identity $targetGroup.ObjectGuid -Confirm:$False -ErrorAction Stop
}
$script:totalChanged++
log -text "Deleted AD group: $($deletedGroup.displayName)" -color "Green"
}catch{
log -text "There was a problem deleting $($deletedGroup.displayName) $res" -color "Red" -append -error
}
}
}
##CHANGED PROXYADDRESSES
if($o365GroupEmailAddressChanges[0].mode){
foreach($o365GroupEmailAddressChange in $o365GroupEmailAddressChanges){
try{
$targetGroup = searchADForUserOrGroup -searchQuery $o365GroupEmailAddressChange.DisplayName
}catch{
log -text "Failed to find $($o365GroupEmailAddressChange.DisplayName) in AD" -color "Red" -error
continue
}
if($o365GroupEmailAddressChange.mode -eq 1){ #delete
try{
if(!$readOnly){
$res = Set-ADGroup -Identity $targetGroup.ObjectGuid.Guid -Remove @{ProxyAddresses=$o365GroupEmailAddressChange.proxyAddress} -Confirm:$False -ErrorAction Stop
$script:totalChanged++
}
log -text "Removed $($o365GroupEmailAddressChange.proxyAddress) from $($o365GroupEmailAddressChange.DisplayName)." -color "Green"
}catch{
log -text "Failed to remove $($o365GroupEmailAddressChange.proxyAddress) from $($o365GroupEmailAddressChange.DisplayName). $res" -color "Red" -append -error
}
}
if($o365GroupEmailAddressChange.mode -eq 2){ #add
try{
if(!$readOnly){
$res = Set-ADGroup -Identity $targetGroup.ObjectGuid.Guid -Add @{ProxyAddresses=$o365GroupEmailAddressChange.proxyAddress} -Confirm:$False -ErrorAction Stop
$script:totalChanged++
}
log -text "Added $($o365GroupEmailAddressChange.proxyAddress) to $($o365GroupEmailAddressChange.DisplayName)." -color "Green"
}catch{
log -text "Failed to add $($o365GroupEmailAddressChange.proxyAddress) to $($o365GroupEmailAddressChange.DisplayName). $res" -color "Red" -append -error
}
}
}
}
##CHANGED DELEGATES
if($o365GroupAuthorizedSenderChanges[0].mode){
foreach($o365GroupAuthorizedSenderChange in $o365GroupAuthorizedSenderChanges){
try{
$targetGroup = searchADForUserOrGroup -searchQuery $o365GroupAuthorizedSenderChange.DisplayName
}catch{
log -text "Failed to find $($o365GroupAuthorizedSenderChange.DisplayName) in AD" -color "Red" -error
continue
}
try{
$sourceSender = searchExOForUserOrGroupV2 -O365ObjectCache $O365ObjectCache -searchQuery $o365GroupAuthorizedSenderChange.authorizedSender -byAlias
$targetSender = searchADForUserOrGroup -searchQuery $sourceSender.primarySmtpAddress -byMail -anyAlias
}catch{
log -text "Failed to find $($o365GroupAuthorizedSenderChange.authorizedSender) in O365 or AD" -color "Red" -error
continue
}
if($o365GroupAuthorizedSenderChange.mode -eq 1){ #delete single user
try{
if(!$readOnly){
$res = Set-ADGroup -Identity $targetGroup.ObjectGuid.Guid -Remove @{AuthOrig=$targetSender.distinguishedName} -Confirm:$False -ErrorAction Stop
$script:totalChanged++
}
log -text "Removed $($targetSender.distinguishedName) from $($targetGroup.DisplayName)." -color "Green"
}catch{
log -text "Failed to remove $($targetSender.distinguishedName) from $($targetGroup.DisplayName). $res" -color "Red" -append -error
}
}
if($o365GroupAuthorizedSenderChange.mode -eq 2){ #add single user
try{
if(!$readOnly){
$res = Set-ADGroup -Identity $targetGroup.ObjectGuid.Guid -Add @{AuthOrig=$targetSender.distinguishedName} -Confirm:$False -ErrorAction Stop
$script:totalChanged++
}
log -text "Added $($targetSender.distinguishedName) to $($targetGroup.DisplayName)." -color "Green"
}catch{
log -text "Failed to add $($targetSender.distinguishedName) to $($targetGroup.DisplayName). $res" -color "Red" -append -error
}
}
if($o365GroupAuthorizedSenderChange.mode -eq 3){ #delete single group
try{
if(!$readOnly){
$res = Set-ADGroup -Identity $targetGroup.ObjectGuid.Guid -Remove @{dLMemSubmitPerms=$targetSender.distinguishedName} -Confirm:$False -ErrorAction Stop
$script:totalChanged++
}
log -text "Removed $($targetSender.distinguishedName) from $($targetGroup.DisplayName)." -color "Green"
}catch{
log -text "Failed to remove $($targetSender.distinguishedName) from $($targetGroup.DisplayName). $res" -color "Red" -append -error
}
}
if($o365GroupAuthorizedSenderChange.mode -eq 4){ #add single group
try{
if(!$readOnly){
$res = Set-ADGroup -Identity $targetGroup.ObjectGuid.Guid -Add @{dLMemSubmitPerms=$targetSender.distinguishedName} -Confirm:$False -ErrorAction Stop
$script:totalChanged++
}
log -text "Added $($targetSender.distinguishedName) to $($targetGroup.DisplayName)." -color "Green"
}catch{
log -text "Failed to add $($targetSender.distinguishedName) to $($targetGroup.DisplayName). $res" -color "Red" -append -error
}
}
}
}
##PROCESS MEMBERSHIP CHANGES
if($O365GroupMemberChanges[0].mode){
foreach($O365MemberChange in $O365GroupMemberChanges){
try{
$targetGroup = searchADForUserOrGroup -searchQuery $O365MemberChange.DisplayName
}catch{
log -text "Failed to find $($O365MemberChange.DisplayName) in AD" -color "Yellow" -warning
continue
}
if($O365MemberChange.mode -eq 1){
try{
$user = searchADForUserOrGroup -searchQuery $O365MemberChange.memberMail -byMail -anyAlias
if(!$readOnly){
$res = Set-ADGroup -Identity $targetGroup.ObjectGuid.Guid -Remove @{'member'=$user.DistinguishedName} -ErrorAction Stop
$script:totalChanged++
}
log -text "Removed $($O365MemberChange.memberMail) from $($targetGroup.DistinguishedName)." -color "Green"
}catch{
log -text "Failed to remove $($O365MemberChange.memberMail) from $($targetGroup.DistinguishedName). $res" -color "Yellow" -append -warning
}
}
if($O365MemberChange.mode -eq 2){
try{
$user = searchADForUserOrGroup -searchQuery $O365MemberChange.memberMail -byMail -anyAlias
if(!$readOnly){
$res = Set-ADGroup -Identity $targetGroup.ObjectGuid.Guid -Add @{'member'=$user.DistinguishedName} -ErrorAction Stop
$script:totalChanged++
}
log -text "Added $($O365MemberChange.memberMail) to $($targetGroup.DistinguishedName)." -color "Green"
}catch{
log -text "Failed to add $($O365MemberChange.memberMail) to $($targetGroup.DistinguishedName). $res" -color "Yellow" -append -warning
}
}
}
}
#RESET CACHE
try{
log -text "Propagated all recent changes from O365 to AD, re-caching data from AD" -color "Green"
[Array]$adDistributionGroups = @(retrieveADGroupsAndMembers -sourceOU $sourceOU | where {$_})
log -text "Loaded $($adDistributionGroups.Count) groups from AD" -color "Green"
$res = Remove-Item $O365CachePath -Force -Recurse -Confirm:$False
log -text "Removed O365 Cache File" -color "Green"
}catch{
log -text "There was a problem resetting the cache while processing changes to AD" -color "Red" -append -error
abort_GS -failed
}
}else{
log -text "Changes detected in O365 since last run, but sync direction is set to AD > O365, ignoring changes in O365" -color "Green" -warning
}
}#endregion
#region do a full comparison of both environments
if($full){
#now run a full sync in stages
$changes = 0
#stage 1: Group Membership AD -> O365 and vice versa
log -text "Performing full comparison of group membership between O365 and AD groups" -color "Green"
[Array]$membersDiff = @(returnDifferentMembersBetweenGroups -o365Groups $o365DistributionGroups -adGroups $adDistributionGroups | where {$_})
if($membersDiff.Count -eq 0){
log -text "No membership differences detected between groups that exist in both locations" -color "Green"
}else{
$changes+=$membersDiff.Count
log -text "$($membersDiff.Count) membership differences detected and queued" -color "Green"
$failed = 0
foreach($memberDiff in $membersDiff){
if($memberDiff.DisplayName.Length -lt 1 -or $memberDiff.memberIdentifier.Length -lt 1 -or $memberDiff.targetIdentifier.Length -lt 1){
log -text "Invalid member change, cannot process $memberDiff because of missing properties" -color "Yellow" -warning
continue
}else{
if($memberDiff.mode -eq 1 -and $syncDirection -ne 2){
try{
if(!$readOnly){
$res = Set-ADGroup -Identity $memberDiff.targetIdentifier -Add @{'member'=$memberDiff.memberIdentifier} -ErrorAction Stop
$script:totalChanged++
}
log -text "Added $($memberDiff.memberIdentifier) to AD Group $($memberDiff.DisplayName) $($memberDiff.targetIdentifier)" -Color "Green"
}catch{
log -text "Failed to add $($memberDiff.memberIdentifier) to AD Group $($memberDiff.DisplayName) $($memberDiff.targetIdentifier) $res" -color "Yellow" -append -warning
$failed++
}
}
if($memberDiff.mode -eq 2 -and $syncDirection -ne 3){
try{
if(!$readOnly){
$res = Add-DistributionGroupMember -Identity $memberDiff.targetIdentifier -Member $memberDiff.memberIdentifier -BypassSecurityGroupManagerCheck -Confirm:$False -ErrorAction Stop -WarningAction Stop
$script:totalChanged++
}
log -text "Added $($memberDiff.memberIdentifier) to O365 Group $($memberDiff.DisplayName) $($memberDiff.targetIdentifier)" -Color "Green"
}catch{
log -text "Failed to add $($memberDiff.memberIdentifier) to O365 Group $($memberDiff.DisplayName) $($memberDiff.targetIdentifier) $res" -color "Yellow" -append -warning
$failed++
}
}
}
}
if($failed -gt 0){
log -text "Processed $($membersDiff.Count - $failed) changes to membership, there were $failed errors, see earlier log entries for details" -color "Yellow"
}else{
log -text "Processed $($membersDiff.Count) changes to membership without any errors" -color "Green"
}
}
#stage 2: check groups that exist in AD, but don't exist in O365 and create them
try{
log -text "Comparing AD groups with O365 groups..." -color "Green"
if($o365DistributionGroups.Count -gt 0){
[Array]$ADNewGroups = @(returnMissingGroups -referenceGroups $o365DistributionGroups -differenceGroups $adDistributionGroups | where {$_})
}else{
[Array]$ADNewGroups = @($adDistributionGroups | where {$_})
}
$changes+=$ADNewGroups.Count
log -text "Found $($ADNewGroups.Count) new groups to provision in O365" -color "Green"
}catch{
log -text "Failed to compare O365 groups with AD groups" -color "Red" -append -error
abort_GS -failed
}
if($ADNewGroups.Count -gt 0){
if($syncDirection -ne 3){
provisionO365Groups -groups $ADNewGroups
log -text "Group provisioning in O365 completed" -color "Green"
}else{
log -text "Groups that exist in AD but not in O365 were detected, but syncDirection is set to 3 so no groups will be provisioned in O365" -color "Yellow" -warning
}
}
$script:totalChanges+=$changes
if($changes -gt 0){
#Reload O365 objects
#region cache O365 objects
try{
log -text "Caching current O365 objects..." -color "Green"
$O365ObjectCache = cacheO365Objects
log -text "Cached $($O365ObjectCache.Mailboxes.Count) mailboxes and $($O365ObjectCache.Groups.Count) groups in O365" -color "Green"
}catch{
log -text "Failed to cache O365 objects" -color "Red" -append -error
abort_GS -failed
}#endregion
try{
log -text "Re-caching distribution groups from O365" -color "Green"
[Array]$o365DistributionGroups = @(retrieveO365GroupsAndMembers | where {$_})
log -text "Loaded $($o365DistributionGroups.Count) groups from O365" -color "Green"
}catch{
log -text "There was a problem loading Distribution Groups from O365" -color "Red" -append -error
abort_GS -failed
}
$changes=0
}
#stage 3: check groups that exist in O365, but don't exist in AD and create them
try{
log -text "Comparing O365 groups with AD groups..." -color "Green"
[Array]$O365NewGroups = @(returnMissingGroups -referenceGroups $adDistributionGroups -differenceGroups $o365DistributionGroups | where {$_})
log -text "Found $($O365NewGroups.Count) new groups to provision in AD" -color "Green"
}catch{
log -text "Failed to compare AD groups with O365 groups" -color "Red" -append -error
abort_GS -failed
}
if($O365NewGroups.Count -gt 0){
$changes++
if($targetOU -and $syncDirection -ne 2){
provisionADGroups -groups $O365NewGroups
}else{
log -text "Groups that exist in O365 but not in AD were detected, but either targetOU is not defined or syncDirection is set to 2 so no groups will be provisioned in AD" -color "Yellow" -warning
}
}
$script:totalChanges+=$changes
if($changes -gt 0){
log -text "Propagated all recent changes from O365 to AD, re-caching data from AD" -color "Green"
[Array]$adDistributionGroups = @(retrieveADGroupsAndMembers -sourceOU $sourceOU | where {$_})
log -text "Loaded $($adDistributionGroups.Count) groups from AD" -color "Green"
}
}#endregion
#region final stage, store cache for next run
#Store AD cache
try{
log -text "Writing AD cache file now..." -color "Green"
if(!$readOnly){
$adDistributionGroups | Export-Clixml -Path $ADCachePath -Depth 10 -Confirm:$False -Force -Encoding UTF8 -ErrorAction Stop
}
log -text "Overwrote AD cache for next run" -color "Green"
}catch{
log -text "Failed to write to AD cache, this will result in failures at next run, it is recommended to delete the cache files and immediately rerun this script" -color "Red" -append -error
}
#Store O365 cache
try{
log -text "Writing O365 cache file now..." -color "Green"
if(!$readOnly){
$o365DistributionGroups | Export-Clixml -Path $O365CachePath -Depth 10 -Confirm:$False -Force -Encoding UTF8 -ErrorAction Stop
}
log -text "Overwrote O365 cache for next run" -color "Green"
}catch{
log -text "Failed to write to O365 cache, this will result in failures at next run, it is recommended to delete the cache files and immediately rerun this script" -color "Red" -append -error
}#endregion
abort_GS