Generating an XML file from Active Directory

📘

Note

This script is provided as an example of how XML can be generated from different sources for use with Interacts profile sources. Any customization to this script is not supported by Interact, therefore, you should feel free to use this as a starting point and tweak as necessary.

Using PowerShell (version 3) it's possible to use a script to read your Active Directory, generate an XML file and send this file to interact for purposes of synchronising with general profile sources.

The script

Let's start by looking at the script before breaking it down into more detail...

Import-Module ActiveDirectory

# Set up connection details
$server = "URI OF ACTIVE DIRECTORY DOMAIN CONTROLLER"
$userName = "USERNAME OF USER WITH READ ACCESS TO ACTIVE DIRECTORY"
$password = "PASSWORD OF USER ABOVE"
$userDn = "DN FOR THE OU OF THE USERS TO BE IMPORTED FROM ACTIVE DIRECTORY"
$groupDn = "DN FOR THE OU OF GROUPS TO BE CREATED IN INTERACT"

$uri = "URL TO PROFILE SOURCE IN INTERACT"
$defaultUserPassword = "DEFAULT PASSWORD FOR NEW USERS"
$authtoken = "AUTH TOKEN CONFIGURED IN INTERACT"
$domain = "NAME OF PROFILE SOURCE IN INTERACT"
$ldapId = "ID OF PROFILE SOURCE IN INTERACT"
$xmlPath = "PATH TO XML FILE TO CREATE"

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

function Write-SyncOption([System.Xml.XmlTextWriter] $writer, [string] $optionName, [string] $optionValue){
    # Write a syncoption element using the passed values
    $writer.WriteStartElement('option')
    $writer.WriteAttributeString('name', $optionName)
    $writer.WriteString($optionValue)
    $writer.WriteEndElement()
}

function Write-DocumentBase([System.Xml.XmlTextWriter] $writer){
    # Write the document root element
    # Write the syncoptions element 
    $xmlWriter.WriteStartElement('syncdata')
    $xmlWriter.WriteAttributeString('version', '1')

    $xmlWriter.WriteStartElement('syncoptions')
    $xmlWriter.WriteAttributeString('domain', $domain)
    $xmlWriter.WriteAttributeString('ldapid', $ldapId)

    Write-SyncOption $xmlWriter 'syncCompanies' 'true'
    Write-SyncOption $xmlWriter 'syncLocations' 'true'
    Write-SyncOption $xmlWriter 'syncDepartments' 'true'
    Write-SyncOption $xmlWriter 'syncManagers' 'true'
    Write-SyncOption $xmlWriter 'actionDisabledUsers' 'x'
    Write-SyncOption $xmlWriter 'actionMissingDeletedUsers' 'x'
    Write-SyncOption $xmlWriter 'loginType' '1'
    Write-SyncOption $xmlWriter 'defaultCulture' '1'
    Write-SyncOption $xmlWriter 'newUserPasswordBehaviour' 'strict'

    $xmlWriter.WriteEndElement()
}

function Write-PersonElement([System.Xml.XmlTextWriter] $writer, [System.Object] $user){
    # Write a person child element for a user
    $xmlWriter.WriteStartElement('person')
    $xmlWriter.WriteElementString('firstname', $user.GivenName)
    $xmlWriter.WriteElementString('surname', $user.Surname)
    $xmlWriter.WriteElementString('title', $user.personalTitle)
    $xmlWriter.WriteElementString('initials', $user.Initials)
    $xmlWriter.WriteElementString('jobtitle', $user.Title)
    $xmlWriter.WriteElementString('phone', $user.telephoneNumber)
    $xmlWriter.WriteElementString('mobile', $user.mobile)
    $xmlWriter.WriteElementString('fax', $user.facsimileTelephoneNumber)
    $xmlWriter.WriteElementString('extension', $user.ipPhone)
    $xmlWriter.WriteElementString('address', $user.homePostalAddress)
    $xmlWriter.WriteEndElement()
}

function Write-AdditionalField([System.Xml.XmlTextWriter] $writer, [string] $fieldName, [string] $fieldValue){
    # Write an additional field element using the passed values
    #$writer.WriteStartElement('field')
    #$writer.WriteAttributeString('name', $fieldName)
    #$writer.WriteString($fieldValue)
    #$writer.WriteEndElement()
}

function Write-ManagerElement([System.Xml.XmlTextWriter] $writer, [System.Object] $user, [string] $searchBase, [string] $serverName){
    # Write the manager element for a user
    # Check that the AD property has a value and use it as the DN of the manager
    # Query AD for the two specific properties required to build the element
    $xmlWriter.WriteStartElement('manager')
    if (![string]::IsNullOrWhiteSpace($user.Manager)) {
        $manager = Get-ADUser -Filter {DistinguishedName -eq $user.Manager} -SearchBase $searchBase -SearchScope Subtree  -Server $serverName -Credential $credential  -Properties DistinguishedName, objectGUID
        $xmlWriter.WriteAttributeString('uid', $manager.ObjectGUID.ToString())
        $xmlWriter.WriteAttributeString('dn', $manager.DistinguishedName)
    } else {
        $xmlWriter.WriteAttributeString('uid', '')
        $xmlWriter.WriteAttributeString('dn', '')
    }
    $xmlWriter.WriteEndElement()        
}

function Write-PrimaryOrganisationElement([System.Xml.XmlTextWriter] $writer, [string] $organisationType, [string] $organisationValue){    
    # Write an organisation element using the passed values
    $xmlWriter.WriteStartElement('organisation')
    $xmlWriter.WriteAttributeString('type', $organisationType)
    $xmlWriter.WriteAttributeString('primary', 'true')
    $xmlWriter.WriteString($organisationValue)
    $xmlWriter.WriteEndElement()
}

function Write-OrganisationsElement([System.Xml.XmlTextWriter] $writer, [System.Object] $user, [string] $searchBase, [string] $serverName){
    # Write the organisations element for a user
    # Check if the AD fields have values and then write the specific element
    $xmlWriter.WriteStartElement('organisations')

    if (![string]::IsNullOrWhiteSpace($user.department)) {
        Write-PrimaryOrganisationElement $xmlWriter 'department' $user.department
    }

    if (![string]::IsNullOrWhiteSpace($user.company)) {
        Write-PrimaryOrganisationElement $xmlWriter 'company' $user.company
    }

    if (![string]::IsNullOrWhiteSpace($user.physicalDeliveryOfficeName)) {
        Write-PrimaryOrganisationElement $xmlWriter 'location' $user.physicalDeliveryOfficeName
    }

    $xmlWriter.WriteEndElement()
}

function Write-UsersElement([System.Xml.XmlTextWriter] $writer, [string] $searchBase, [string] $serverName){
    # Write the users element of the document
    # Query AD for the users and iterate round the array to create an element for each user

    $xmlWriter.WriteStartElement('users')
    Write-Host "Executing user query..."
    $users = Get-ADUser -Filter * -SearchBase $searchBase -SearchScope Subtree  -Server $serverName -Credential $credential  -Properties *
    $xmlWriter.WriteAttributeString('TotalUsers', $users.Count)
    foreach ($user in $users) 
    { 
        Write-Host "Processing user " $user.ObjectGUID.ToString() $user.SamAccountName $user.mail
        # Create user element with attributes
        $xmlWriter.WriteStartElement('user')
        $xmlWriter.WriteAttributeString('uid', $user.ObjectGUID.ToString())
        $xmlWriter.WriteAttributeString('dn', $user.DistinguishedName)
        $xmlWriter.WriteAttributeString('username', $user.SamAccountName)
        $xmlWriter.WriteAttributeString('email', $user.mail)

        # Call the function to write person specific elements
        Write-PersonElement $xmlWriter $user

        # Create user specific elements
        $xmlWriter.WriteElementString('statusenabled', $user.Enabled.ToString().ToLower())
        $xmlWriter.WriteElementString('password', $defaultUserPassword)
        $xmlWriter.WriteElementString('culture', '1')
        $xmlWriter.WriteStartElement('language')
        $xmlWriter.WriteAttributeString('id', '1')
        $xmlWriter.WriteEndElement()
        
        # Create additional fields examples
        $xmlWriter.WriteStartElement('additionalfields')
        #Write-AdditionalField $xmlWriter 'first_aider' 'true'
        #Write-AdditionalField $xmlWriter 'car_registration' 'AB 66 XYZ'
        $xmlWriter.WriteEndElement()

        # Call the function to write the manager element
        Write-ManagerElement $xmlWriter $user $searchBase $serverName

        # Call the function to write the organisations element
        Write-OrganisationsElement $xmlWriter $user

        $xmlWriter.WriteEndElement()
    }
    $xmlWriter.WriteEndElement()
}

function Write-GroupsElement([System.Xml.XmlTextWriter] $writer, [string] $searchBase, [string] $serverName){
    # Write the groups element of the document
    # Query AD for the groups and iterate round the array to create an element for each group
    # Within each group, query AD for the members and iterate that array creating a user element for each member

    $xmlWriter.WriteStartElement('groups')
    Write-Host "Executing group query..."
    $groups = Get-ADGroup -Filter * -SearchBase $searchBase -SearchScope Subtree  -Server $serverName -Credential $credential  -Properties *
    $users = Get-ADUser -Filter * -SearchBase $searchBase -SearchScope Subtree  -Server $serverName -Credential $credential  -Properties *

    $userCnt = 1
    if($users.Count) 
    {
      $userCnt = $users.Count
    }

    $grpCnt = 1
    if($groups.Count) 
    {
      $grpCnt = $groups.Count
    }

    $xmlWriter.WriteAttributeString('TotalUsers', $userCnt)
    $xmlWriter.WriteAttributeString('TotalGroups', $grpCnt)
    foreach ($group in $groups) 
    { 
        Write-Host ""
        Write-Host "Processing group " $group.ObjectGUID.ToString() $group.Name
        # Create group element with attributes
        $xmlWriter.WriteStartElement('group')
        $xmlWriter.WriteAttributeString('uid', $group.ObjectGUID.ToString())
        $xmlWriter.WriteAttributeString('dn', $group.DistinguishedName)
        $xmlWriter.WriteAttributeString('name', $group.Name)

        # Create users element and query AD for group members
        # Create a user element for each member
        Write-Host "Executing member query..."
          $members = Get-ADGroupMember -Identity $group.ObjectGUID -Server $serverName -Credential $credential
        
        $memberCnt = 1
        if($members.Count) 
        {
            $memberCnt = $members.Count
        }
        $xmlWriter.WriteAttributeString('UserCount', $memberCnt)
        $xmlWriter.WriteStartElement('users')
        foreach ($member in $members)
        {
            Write-Host "Processing member " $member.ObjectGUID.ToString() $member.SamAccountName
            $xmlWriter.WriteStartElement('user')
            $xmlWriter.WriteAttributeString('uid', $member.ObjectGUID.ToString())
            $xmlWriter.WriteAttributeString('dn', $member.DistinguishedName)
            $xmlWriter.WriteAttributeString('username', $member.SamAccountName)
            $xmlWriter.WriteAttributeString('email', $member.mail) # doesn't include this on member
            $xmlWriter.WriteEndElement()
        }
        $xmlWriter.WriteEndElement()

        $xmlWriter.WriteEndElement()
    }
    $xmlWriter.WriteEndElement()
}

###########################################
#
# The main execution sequence of the script
#

Write-Host "Starting..."

Write-Host "Setting up XML document..."

# Set up the XML document
$xmlWriter = New-Object System.Xml.XmlTextWriter($xmlPath, $null)
$xmlWriter.Formatting = 'Indented'
$xmlWriter.Indentation = 1
$XmlWriter.IndentChar = "`t"
$xmlWriter.WriteStartDocument()

# Call the major work functions to build the document
Write-DocumentBase $xmlWriter

Write-Host "Checking credentials..."
$secstr = New-Object -TypeName System.Security.SecureString
$password.ToCharArray() | ForEach-Object {$secstr.AppendChar($_)}
$credential = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $secstr

Write-Host "Processing users..."
Write-UsersElement $xmlWriter $userDn $server

Write-Host "Processing groups..."
Write-GroupsElement $xmlWriter $groupDn $server

# Final tidy up actions
$xmlWriter.WriteEndElement()
$xmlWriter.Flush()
$xmlWriter.Close()

Write-Host "Complete"

# Deliver file to API endpoint
Invoke-RestMethod -Uri $uri -Method Post -InFile $xmlPath -ContentType "multipart/form-data" -Headers @{'X-ApiKey'=$authtoken; }

Script walkthrough

Let's look at the script in a little more detail...

First, we set up some variables that relate to your particular instance on Interact, Active Directory and Profile Sources

Set up

Import-Module ActiveDirectory

# Set up connection details
$server = "URI OF ACTIVE DIRECTORY DOMAIN CONTROLLER"
$userName = "USERNAME OF USER WITH READ ACCESS TO ACTIVE DIRECTORY"
$password = "PASSWORD OF USER ABOVE"
$userDn = "DN FOR THE OU OF THE USERS TO BE IMPORTED FROM ACTIVE DIRECTORY"
$groupDn = "DN FOR THE OU OF GROUPS TO BE CREATED IN INTERACT"

$uri = "URL TO PROFILE SOURCE IN INTERACT"
$defaultUserPassword = "DEFAULT PASSWORD FOR NEW USERS"
$authtoken = "AUTH TOKEN CONFIGURED IN INTERACT"
$domain = "NAME OF PROFILE SOURCE IN INTERACT"
$ldapId = "ID OF PROFILE SOURCE IN INTERACT"
$xmlPath = "PATH TO XML FILE TO CREATE"

Then a series of functions are defined that generate the XML for a user within AD, a group within AD and it's members, the various synchronisation options from within profile sources.

Synchronization options

The functions for generating the XML for users and groups should be fairly self explanatory so lets look at the various options

function Write-DocumentBase([System.Xml.XmlTextWriter] $writer){
    # Write the document root element
    # Write the syncoptions element 
    $xmlWriter.WriteStartElement('syncdata')
    $xmlWriter.WriteAttributeString('version', '1')

    $xmlWriter.WriteStartElement('syncoptions')
    $xmlWriter.WriteAttributeString('domain', $domain)
    $xmlWriter.WriteAttributeString('ldapid', $ldapId)

    Write-SyncOption $xmlWriter 'syncCompanies' 'true'
    Write-SyncOption $xmlWriter 'syncLocations' 'true'
    Write-SyncOption $xmlWriter 'syncDepartments' 'true'
    Write-SyncOption $xmlWriter 'syncManagers' 'true'
    Write-SyncOption $xmlWriter 'actionDisabledUsers' 'x'
    Write-SyncOption $xmlWriter 'actionMissingDeletedUsers' 'x'
    Write-SyncOption $xmlWriter 'loginType' '1'
    Write-SyncOption $xmlWriter 'defaultCulture' '1'
    Write-SyncOption $xmlWriter 'newUserPasswordBehaviour' 'strict'

    $xmlWriter.WriteEndElement()
}

When using profile sources, Interact takes the options from the XML source itself rather than from the screens within Interact. Each option above is described a little below

  • syncCompanies - this tells Interact whether or not to synchronise user companies. If a company does not exist in Interact it will be automatically created and the user assigned to that company

  • syncLocations - this tells Interact whether or not to synchronise user Locations. Again, if a location does not already exist in Interact it will be automatically created and the user assigned to that location.

  • syncDepartments - this tells Interact whether or not to synchronise user departments. As above, if a department does not already exist in Interact it will be automatically created and the user assigned to that location.

  • actionDisabledUsers - this tells Interact what to do with users that are marked in the XML as disabled. Options here are:

    • x - do nothing - do not update the interact status of those users
    • d - deactive those users within Interact
    • a - deactivate and archive users within Interact
  • actionMissingDeletedUsers - this tells Interact what to do with users that have eithe been previously created in Interact with this profile source, or are not assigned to any groups within the XML. Options for this are the same as above.

  • loginType - this needs to be present and set to 1

  • defaultCulture - this needs to be present and set to 1

  • newUserPasswordBehaviour - this tells Interact how to create passwords for new users. Options here are

    • strict - Use the password that is specified in the user part of the XML
    • random - Create a random password for each user

The main part of the script

The main part of the script is found at the bottom which actually handles the generation of the XML file and sending the data to Interact

#
# The main execution sequence of the script
#

Write-Host "Starting..."

Write-Host "Setting up XML document..."

# Set up the XML document
$xmlWriter = New-Object System.Xml.XmlTextWriter($xmlPath, $null)
$xmlWriter.Formatting = 'Indented'
$xmlWriter.Indentation = 1
$XmlWriter.IndentChar = "`t"
$xmlWriter.WriteStartDocument()

# Call the major work functions to build the document
Write-DocumentBase $xmlWriter

Write-Host "Checking credentials..."
$secstr = New-Object -TypeName System.Security.SecureString
$password.ToCharArray() | ForEach-Object {$secstr.AppendChar($_)}
$credential = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $secstr

Write-Host "Processing users..."
Write-UsersElement $xmlWriter $userDn $server

Write-Host "Processing groups..."
Write-GroupsElement $xmlWriter $groupDn $server

# Final tidy up actions
$xmlWriter.WriteEndElement()
$xmlWriter.Flush()
$xmlWriter.Close()

Write-Host "Complete"

# Deliver file to API endpoint
Invoke-RestMethod -Uri $uri -Method Post -InFile $xmlPath -ContentType "multipart/form-data" -Headers @{'X-ApiKey'=$authtoken; }

This part of the script sets up the XML writer to generate the file, then sets up the Active Directory credentials, builds the users elements and groups elements by calling the relevant functions, and finally sends the file to Interact as a POST request with the appropriate headers set.

Troubleshooting

Powershell is surprisingly good at reporting errors from the script itself, and Interact returns useful error messages if the XML is invalid, so troubleshooting should be fairly straightforward.

Take a look at Troubleshooting XML data for more information