Sync Office365 with SyncroRMM contacts

Like many of you – I always got stuck with syncing my contact to all users. After going nuts for weeks I decided to write a script to have it auto-sync daily. This script contains multiple sections and I will try to explain it here. Please be understanding – English is my 3rd language lol

In order to sync data from all clients to Syncro, we will need access to Microsoft Partner Portal data accessible via a graph and Syncro API. Part of accessing graph data in Office365 is based on CyberDrain (you are my hero – huge respect for all your contributions) https://www.cyberdrain.com/using-the-secure-application-model-with-partnercenter-2-0-for-office365/

The above link will help you get authorization tokens from Microsoft. We will need ApplicationID, ApplicationSecret, TenantID, RefreshToken – this info and Syncro API must be set up before running the script.

$ApplicationId         = ' Enter your Application ID'
$ApplicationSecret     = ' Enter your Application Secret ' | ConvertTo-SecureString -Force -AsPlainText
$TenantID              = ' Enter your Tenant ID '
$RefreshToken          = ' Enter your Refresh Token'

$apiKey                = "Bearer <enter your API from syncro>"
$apiUri                = "https://<enter your subdomain>.syncromsp.com/api/v1/"

To eliminate manual creation of schedule – we are setting up script to be run by Scheduler daily at 6:45am. Feel free to tweak as you need or skip it and just manually setup scheduler to run as you wish..

# Setup schedule
$taskExists = Get-ScheduledTask | Where-Object {$_.TaskName -like $ScriptName}
if($taskExists) {
if ((Get-ScheduledTask -TaskName $ScriptName).state -like "Disabled") {Write-Host "Schedule is disabled for this script..." -ForegroundColor Red}

} else {
$securePwd = Read-Host "Enter password to schedule under $($env:UserName) (leave blank to disable task):" -AsSecureString
$taskPrincipal = New-ScheduledTaskPrincipal -UserId $env:UserName -RunLevel Highest
$taskAction = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-File $($ScriptDir+'\'+$ScriptName+'.ps1')"
$taskTrigger = New-ScheduledTaskTrigger -Daily -At 6:45am
Register-ScheduledTask -TaskName $ScriptName -Action $taskAction -Trigger $taskTrigger -Description "script to run $ScriptName" -Principal $taskPrincipal | Out-Null
try {
Set-ScheduledTask -TaskName $ScriptName -User $taskPrincipal.UserID -Password $([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePwd))) | Out-Null

} catch {
if (([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePwd))) -eq "") {
Disable-ScheduledTask -TaskName $ScriptName | Out-Null
} else {
Write-Host "Incorrect password" -ForegroundColor Red
}
}

Write-Host "Schedule $ScriptName $((Get-ScheduledTask -TaskName $ScriptName).state)" -ForegroundColor Green
}

Next part – this is where we setup/install missing modules needed to access MsOnline, AzureAD, PartnerCenter.  You will need to run this script at least once with admin privileges.

Second part of the script is getting all credentials created based on the info from first section, connecting to portals, retrieve customers and mapping fields for fields that don’t match between Office365 and Syncro

If (Get-Module -ListAvailable -Name "MsOnline") { Import-module "Msonline" } Else { install-module "MsOnline" -Force; import-module "Msonline" }
If (Get-Module -ListAvailable -Name "AzureAD") { Import-module "AzureAD" } Else { install-module "AzureAD" -Force; import-module "AzureAD" }
If (Get-Module -ListAvailable -Name "PartnerCenter") { Import-module "PartnerCenter" } Else { install-module "PartnerCenter" -Force; import-module "PartnerCenter" }

$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default'

Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract
$fieldMaps = @{name='displayName';address1='streetAddress';city='city';zip='postalCode';state='state';phone='businessPhones'}

function compareValues{
   param([string]$o365,[string]$syncro)

   if ($o365 -ne $null -and $o365 -ne $syncro) {
      $return += " - $o365 is $syncro"
   }
   return $return
}

Here we take each customer, query against names in Syncro (names must match to your records) . Grab current contacts from Syncro and Office365 and compare them.

If field in Office365 is empty, script will not override data entered in Syncro but if Office365 contains data (ex: position), this will be synced to Syncro.

New users from Office365 will be created in Syncro.

Users that don’t exists in Office365  or don’t have valid license (ex: admins, empty accounts, shared mailboxes) will not be propagated to Syncro and they will be removed from Syncro if Office365 user no longer exists. Please be aware of it.

If you don’t want users to be auto-deleted – you should remove lines 145-152  (I could setup option but had no need for it – IMO if user is no longer with company – why keep him in Syncro

foreach ($customer in $customers) {
    $CustomerToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -Tenant $customer.TenantID
    $headers = @{ "Authorization" = "Bearer $($CustomerToken.AccessToken)" }
    #Swrite-host "Collecting data for $($Customer.Name) [$($Customer.defaultdomainname)] " -ForegroundColor Green

    $query = [System.Web.HTTPUtility]::UrlEncode($($Customer.Name))
    $companySyncroID = (Invoke-RestMethod -Uri "$apiUri/customers?query=$query" -Method Get -Header @{ "Authorization" = $apiKey } -ContentType "application/json")[0].customers.id

    if ($companySyncroID -eq $null) {
        write-host "Client $($Customer.Name) not found SyncroMSP" -ForegroundColor Red 
    } else {
        write-host "Getting client ID# $companySyncroID for $($Customer.Name) from SyncroMSP" -ForegroundColor Green 
        $domains = Get-MsolDomain
        $allusersO365 = (Invoke-RestMethod -Uri 'https://graph.microsoft.com/beta/users?$top=999' -Headers $Headers -Method Get -ContentType "application/json").value | Where-Object {$_.mail -ne $null -and $_.assignedLicenses -ne $null}
        $Syncro = (Invoke-RestMethod -Method Get -Uri "$apiUri/contacts?customer_id=$companySyncroID" -Header @{ "Authorization" = $apiKey } -ContentType "application/json")
        $allusersSyncro = $syncro.contacts

        $totalPages = $Syncro.meta.total_pages
        if ($totalPages -ne 1) {
            for($i=2; $i -le $totalPages; $i++){
                $allusersSyncro += (Invoke-RestMethod -Method Get -Uri "$apiUri/contacts?customer_id=$companySyncroID&page=$i" -Header @{ "Authorization" = $apiKey } -ContentType "application/json").contacts
            }
        }

        ### Search for users in Syncro and compre with Office365 ####
        $UserObj = foreach ($userO365 in $allusersO365) {
            $userSyncro = $allusersSyncro | Where-Object{$_.email -like $($userO365.mail)}
            if ($userSyncro -ne $null){
                Try {
                    $street = $userO365.streetAddress.split(",",2)
                    $street[1] = $street[1].trim()
                    if ($street[1] -ne $null -and $street[1] -ne $userSyncro.address2) {$userSyncro.address2 = $street[1]; $changed += $street[1]}
                } catch {
                    $street[0] = $userO365.streetAddress     
                }
                try {
                $changed = @()
                ### Check if AD field is empty, if yes - don't overwrite it in Syncro ###
                if ($userO365.displayName -ne $null -and $userO365.displayName -ne $userSyncro.name) {$userSyncro.name = $userO365.displayName; $changed += $userO365.displayName}
                if ($street[0] -ne $null -and $street[0] -ne $userSyncro.address1) {$userSyncro.address1 = $street[0]; $changed += $street[0]}
                if ($userO365.city -ne $null -and $userO365.city -ne $userSyncro.city) {$userSyncro.city = $userO365.city; $changed += $userO365.city}
                if ($userO365.postalCode -ne $null -and $userO365.postalCode -ne $userSyncro.zip) {$userSyncro.zip = $userO365.postalCode; $changed += $userO365.postalCode}
                if ($userO365.state -ne $null -and $userO365.state -ne $userSyncro.state) {$userSyncro.state = $userO365.state; $changed += $userO365.state}
                if ($userO365.businessPhones[0] -ne $null -and $userO365.businessPhones[0] -ne $userSyncro.phone) {$userSyncro.phone = $userO365.businessPhones[0]; $changed += $userO365.businessPhones[0]}
                if ($userO365.mobilePhone -ne $null -and $userO365.mobilePhone -ne $userSyncro.mobile) {$userSyncro.mobile = $userO365.mobilePhone; $changed += $userO365.mobilePhone}
                } catch {
                    Write-Output $userSyncro 
                }

                if ($changed -ne $null) {
                    Write-Host "Contact updated for $($userSyncro.name)"
                    $editUserSyncroStatus = Invoke-RestMethod -Method PUT -Uri "$apiUri/contacts/$($userSyncro.id)" -Header @{ "Authorization" = $apiKey } -ContentType "application/json" -Body (ConvertTo-Json $userSyncro)

                }

            } else {
                #### Found new user - adding to Syncro ####
                Write-Host "$($userO365.displayname) not found in SyncroMSP - Creating...." -ForegroundColor Red
                # add new user
                #Write-Host $userO365
                Try {
                    $street = $userO365.streetAddress.split(",",2)
                    $street[1] = $street[1].trim()
                    if ($street[1] -ne $null -and $street[1] -ne $userSyncro.address2) {$userSyncro.address2 = $street[1]; $changed += $street[1]}
                } catch {
                    $street[0] = $userO365.streetAddress     
                }

                $properties = [PSCustomObject]@{ 
                    'title'                  = $userO365.jobTitle
                    'notification_billing'   = 'false'
                    'notification_marketing' = 'true'
                }
                $newSyncroUser = [PSCustomObject]@{
                    'customer_id' = $companySyncroID
                    'name'        = $userO365.displayname
                    'address1'    = $street[0]
                    'address2'    = $street[1]
                    'city'        = $userO365.city
                    'state'       = $userO365.state
                    'zip'         = $userO365.postalCode
                    'email'       = $userO365.mail
                    'phone'       = $userO365.businessPhones[0]
                    'mobile'      = $userO365.mobilePhone
                    'properties'  = @($properties)
                    'opt_out'     = 'False'
                }

                $newUserSyncro = (Invoke-RestMethod -Method POST -Uri "$apiUri/contacts" -Header @{ "Authorization" = $apiKey } -ContentType "application/json" -Body (ConvertTo-Json $newSyncroUser))      
            }

        }

        #### Search for contact in Syncro that no longer exists in Office365 ####
        $UserSyncroObj = foreach ($userSyncro in $allusersSyncro) {
            $userO365 = $allusersO365 | Where-Object{$_.mail -like $userSyncro.email}
            if ($userO365 -eq $null){
                  Write-Host "$($userSyncro.name) was not found in Office365 - Deleting...." -ForegroundColor Red  
                  $newUserSyncro = (Invoke-RestMethod -Method DELETE -Uri "$apiUri/contacts/$($userSyncro.id)" -Header @{ "Authorization" = $apiKey } -ContentType "*/*" )      
            }
        }
    }

}

If you have any questions or issues – leave a comment on FB or send me email mariusz (at) interactiveavit.com