Create a gist now

Instantly share code, notes, and snippets.

Granfeldt PSMA Azure AD Groups w/ Diff Sync, Paging and Group Membership
param (
$Username,
$Password,
$Credentials,
$OperationType,
[bool] $usepagedimport,
$pagesize
)
# Debug Logfile
$DebugFilePath = "C:\PROGRA~1\MICROS~4\2010\SYNCHR~1\EXTENS~2\AzureA~3\Debug\AADGroupsImport.txt"
if(!(Test-Path $DebugFilePath))
{
$DebugFile = New-Item -Path $DebugFilePath -ItemType File
}
else
{
$DebugFile = Get-Item -Path $DebugFilePath
}
"Starting of Import Script : " + $OperationType + (Get-Date) | Out-File $DebugFile -Append
# Adding the AD library to the PowerShell Session.
# Update for your version and path
Add-Type -Path 'C:\Program Files\WindowsPowerShell\Modules\AzureADPreview\2.0.0.50\Microsoft.IdentityModel.Clients.ActiveDirectory.dll'
# Importing the Lithnet MIIS Automation PS Module
Import-Module LithnetMiisAutomation
# This is the tenant id of you Azure AD. You can use tenant name instead if you want.
$tenantID = "customer.com.au"
$authString = "https://login.microsoftonline.com/$tenantID"
# The resource URI for your token.
$resource = "https://graph.windows.net/"
# This is the common client id.
$client_id = "1950a258-227b-4e31-a9cf-717495945fc2"
# ********************** Authentication to Azure ***************************
# ********** Need to AuthN each time the Import Script runs ****************
# *********** So we can query Azure for Group Membership ********************
# ********** For efficiency make the PageSize large-ish in your Run Profile *****
# ************The username must be MFA disabled user*******************
# Create a client credential with the above common client id, username and password from the Connectivity tab on the MA.
$creds = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserCredential" `
-ArgumentList $Username,$Password
# Create a authentication context with the above authentication string.
$authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" `
-ArgumentList $authString
# Acquire access token from server.
$authenticationResult = $authContext.AcquireToken($resource,$client_id,$creds)
# Use the access token to setup headers for your http request.
$authHeader = $authenticationResult.AccessTokenType + " " + $authenticationResult.AccessToken
$headers = @{"Authorization"=$authHeader; "Content-Type"="application/json"}
# **************************************************************
# Run Once (Initilise and get data section)
if(!$global:relevantObjects){
# First run so we need to do our query and get our objects
# Do all the things we only need to do once.
# ************** Differential Sync Settings ************************
# What Objects are we interested in.
#$Searchfilter ="`$filter=isof('Microsoft.DirectoryServices.User') or isof('Microsoft.DirectoryServices.Group') or isof('Microsoft.DirectoryServices.Contact')"
$Searchfilter ="`$filter=isof('Microsoft.DirectoryServices.Group')"
# Object Type (eg. Users, Groups, Contacts, DirectoryObjects)
$object = "groups"
# Output Directory and file for Differential Cookie
$downloadDirectory = "C:\PROGRA~1\MICROS~4\2010\SYNCHR~1\EXTENS~2\AzureA~3"
$cookieFile = "\AADGroupsDeltaCookie.txt"
$cookiefilepath = $downloadDirectory +$cookieFile
# Reset results var
$query = $null
# Read in Delta Cookie if it exists, if not create the file for storing the cookie
if(!(Test-Path $cookiefilepath))
{
$cookie = New-Item -Path $cookiefilepath -ItemType File
"Creating new Cookie File" | Out-File $DebugFile -Append
}
else
{
$cookie = Get-Item -Path $cookiefilepath
"Cookie File present" | Out-File $DebugFile -Append
}
# Delta or Full Sync based on Run Type and Cookie File ?
if((Get-Item $cookie).length -gt 0kb -and $OperationType -eq "Delta"){
# Delta cookie value exists. Get it
$url = Get-Content $cookie.FullName
$url += '&ocp-aad-dq-include-only-changed-properties=true&api-version=1.6' -f $authenticationResult.TenantId
"Delta Import" | Out-File $DebugFile -Append
}
else
{
# no Delta Cookie, so first run OR Full Sync so return everything
$url = "https://graph.windows.net/{0}/$($object)?&$($Searchfilter)&api-version=1.6&deltaLink="
"Full Import" | Out-File $DebugFile -Append
}
# *********************** IMPORT **********************************
# Get first set of results
$groups = Invoke-RestMethod -Method Get -Headers @{
Authorization = $authenticationResult.CreateAuthorizationHeader()
'Content-Type' = "application/json"
} -Uri ($url -f $authenticationResult.TenantId)
"Retrieved Groups " + $groups.value.Count | Out-File $DebugFile -Append
# ***************************************************************
# Counter to know where we are up to processing the Import
# Starting at minus 1 as our first object is 0 and I'm incrementing at the start of the loop.
[int]$global:objectsImported = -1
# An Array for the retuned objects to go into
$global:tenantObjects = @()
# Add in our first objects
$global:tenantObjects += $groups.value
$moreObjects = $groups
# Get all the remaining objects in batches
if ($groups.'aad.nextLink'){
$moreObjects.'aad.nextLink' = $groups.'aad.nextLink'
do
{
$moreObjects = Invoke-RestMethod -Method Get -Headers @{
Authorization = $authenticationResult.CreateAuthorizationHeader()
'Content-Type' = "application/json"
} -Uri ($moreObjects.'aad.nextLink'+'&api-version=1.6' -f $authenticationResult.TenantId)
"Retrieved Groups " + $moreObjects.value.count | Out-File $DebugFile -Append
$global:tenantObjects += $moreObjects.value
} while ($moreObjects.'aad.nextLink')
}
"Total Groups " + $global:tenantObjects.count | Out-File $DebugFile -Append
# store the DeltaLink in a file for next time we run the MA
$moreObjects.'aad.deltaLink' | Out-File $cookie
# Group Membership Users that need to be added to the Connector Space for Group Membership Reference
$global:newusers = @()
# Refining the returned results
$global:relevantObjects = @()
$global:discardedObjects = @()
# We're only interested in Active Groups
# This also discards Directory Link Objects
foreach ($object in $global:tenantObjects){
if ($object.ObjectType -eq "Group" -and (!$object.deletionTimestamp -and !$object.'aad.isDeleted' )) {
$global:relevantObjects += $object
}
else
{
$global:discardedObjects += $object
}
}
$global:relevantObjects.Count
$global:discardedObjects.Count
}
# ***************** Group Members Functions ***********************
# Get the members of a group
function GroupMembers($ID)
{
$grpMembersURL = "https://graph.windows.net/{0}/groups/" +$ID +'/$links/members?api-version=1.6'
$members = Invoke-RestMethod -Method Get -Headers @{
Authorization = $authenticationResult.CreateAuthorizationHeader()
'Content-Type' = "application/json"
} -Uri ($grpMembersURL -f $authenticationResult.TenantId)
return $members
}
# ********************* Process Groups into the MA *******************
# Process each object and pass to the MA
# Know where we are at in relation to the pagesize from the Run Profile
[int]$objectpagecount = 0
$global:lastobject = $global:relevantObjects[$global:objectsImported]
ForEach($global:group in $global:relevantObjects)
{
# continue from where we got to from the previous page of objects processed
$global:group = $global:relevantObjects[$global:objectsImported + 1]
# if we are at the end then set MoreToImport to False and quit
if (!$global:group -or ($global:objectsImported +1 -gt $global:relevantObjects.count))
{
# nothing left to process
"Reached the end. Group null or Objects Imported greater than Relevant Objects count: " + $global:group | Out-File $DebugFile -Append
$global:MoreToImport = $false
break
}
# Sanity check and Import Group
if ($global:group.objectId -and !$global:group.'aad.isDeleted' -eq "True")
{
$obj = @{}
$obj.Add("ID", $global:group.objectId)
$obj.Add("objectID", $global:group.objectId)
$obj.Add("objectClass", "AADGroup")
$obj.Add("AADMailEnabled",$global:group.mailEnabled)
$obj.Add("AADSecurityEnabled",$global:group.securityEnabled)
$obj.Add("AADDirSyncEnabled",$global:group.dirSyncEnabled)
$obj.Add("AADDisplayName",$global:group.displayName)
$obj.Add("AADMail",$global:group.mail)
$obj.Add("AADMailNickname",$global:group.mailNickname)
$obj.Add("AADLastDirSyncTime",$global:group.lastDirSyncTime)
$obj.Add("AADonPremiseSID",$global:group.onPremisesSecurityIdentifier)
if ($global:group.proxyAddresses)
{
$proxyAddresses = @()
foreach($address in $global:group.proxyAddresses) {
$proxyAddresses += $address
}
$obj.Add("AADProxyAddresses",($proxyAddresses))
}
# Group Membership
$membership = @()
$usermemberurl = @()
"GroupObjectID: "+$global:group.objectId | Out-File $DebugFile -Append
# Get Members
$getmembers = GroupMembers $global:group.objectId
# Iterate through members
foreach ($member in $getmembers.value){
$memberURL = [uri]$member.url
$usermemberurl += $member.url
$memberObjectType = $memberURL.Segments[4]
[string]$memberObjectID = $memberURL.Segments[3] -split "/"
$membership += $memberObjectID
# see if the object exists in the Connector Space
# mostly groups have users as members. They need to exist on the MA so we can add the reference to them
# Only need to do that for users as we're enumerating all groups anyway, so they will exist by the end of the run
"Checking to see if object exists in CS already: " + $memberObjectType +" ID: " + $memberObjectID | Out-File $DebugFile -Append
# Check the MA CS to see if user already exists from a previous group membership
$existencetest = Get-CSObject -DN $memberObjectID -MA "AzureAD Groups"
if (!$existencetest -and $memberObjectType -eq "Microsoft.DirectoryServices.User"){
"Doesn't exist in CS" | Out-File $DebugFile -Append
if (!$global:newusers.Contains($memberObjectID)) {
"Not already added in this run, so adding user" | Out-File $DebugFile -Append
$objuser = @{}
$objuser.Add("ID", $memberObjectID)
$objuser.Add("objectID", $memberObjectID)
$objuser.Add("objectClass", "AADUser")
$objuser
"Added User to CS: " +$memberObjectID | Out-File $DebugFile -Append
$global:newusers += $memberObjectID
}
}
else
{
$memberObjectType +" already exists in the Connector Space" | Out-File $DebugFile -Append
}
}
$obj.Add("AADMembers",$membership)
"GroupMembers: " +$membership | Out-File $DebugFile -Append }
# Pass the User Object to the MA
$obj
# Increase the object count
$objectpagecount++
# for logging how many Groups we've processed
$global:objectsImported++
# Debug Logging where we are up to
"Page count: " + $objectpagecount | Out-File $DebugFile -Append
"Objects Imported count: " +$global:objectsImported | Out-File $DebugFile -Append
"Objects Remaining count: "+($global:relevantObjects.count - $global:objectsImported -1) | Out-File $DebugFile -Append
# Clear our objects
$global:group = $null
$membership = $null
$getmembers = $null
# Refresh token if it is going to expire in the next 5 minutes. Most likely only happen if the page size is humungous
# or the connection to Azure is sloooooow, or groups have 1000's of members or a combo of all of the above
if ($authenticationResult.ExpiresOn.AddMinutes(5).ToString("G") -lt (Get-Date -format G))
{
# Acquire a new access token from server.
$authenticationResult = $authContext.AcquireToken($resource,$client_id,$creds)
# Refresh the headers
$authHeader = $authenticationResult.AccessTokenType + " " + $authenticationResult.AccessToken
$headers = @{"Authorization"=$authHeader; "Content-Type"="application/json"}
}
# if number of processed objects equals the pagesize set MoreToImport as true and break
if ($objectpagecount -eq $pagesize)
{
$global:MoreToImport = $true
"More to Import" | Out-File $DebugFile -Append
break
}
}
# if we are at the end then set MoreToImport to False and quit
if ($global:objectsImported -eq $global:relevantObjects.count)
{
# nothing left to process
"Reached the end. Objects Imported greater than Relevant Objects count: " + (Get-Date) | Out-File $DebugFile -Append
$global:MoreToImport = $false
break
}
# ***********************************************************
"End of Import Script : " + $OperationType + (Get-Date) | Out-File $DebugFile -Append
#endregion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment