Generating an XML file from Okta

📘

Note

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

Using PowerShell it's possible to use a script to gather user data from your Okta account, using their API. This can then be used to generate an XML file and send this file to interact for purposes of synchronising with general profile sources. The script provided will also synchronise Okta groups into Interact to become Interact security groups, allowing you to manage group membership for permissions purposes in Okta, and have that flow through to Interact.

The script

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

# OKTA variables

$uri = "[FQDN FOR OKTA (e.g. https://dev-620014.oktapreview.com)]"
$apiKey = "[API KEY FOR OKTA (e.g. 00i1po7guY9wFDDoeAXLI7SzA_ZEKyznWKxSMx38El)]"

#Interact variables

$interactUrl = "[URL TO INTERACT UPLOAD (e.g. https://inkconsultants.interactgo.com/api/umi/1001/upload)]"
$authtoken = "[AUTH KEY ENTERED INTO INTERACT - (e.g. 629ehxPT30hZyfi09tE8GX1Uk42zne1r)]"
$domain = "[NAME OF PROFILE SOURCE ADDED TO INTERACT]"
$ldapId = "[ID OF PROFILE SOURCE IN INTERACT]"
$xmlPath = "[FILENAME AND PATH OF XML FILE TO BE PRODUCED]"

### Functions

function Write-SyncOption([System.Xml.XmlElement] $element, [string] $optionName, [string] $optionValue){
    # Write a syncoption element using the passed values
    $ele = $xmlWriter.CreateElement("", "option", "")
    $ele.SetAttribute("name", $optionName) > $null
    $ele.InnerText = $optionValue 
    $element.AppendChild($ele) > $null
}

function Write-DocumentBase(){
    # Write the document root element
    $syncdata = $xmlWriter.CreateElement("", "syncdata", "")
    $syncdata.SetAttribute("version", "1") > $null

    # Write the syncoptions element 
    $syncoptions = $xmlWriter.CreateElement("", "syncoptions", "")
    $syncoptions.SetAttribute("domain", $domain) > $null
    $syncoptions.SetAttribute("ldapid", $ldapid) > $null

    Write-SyncOption $syncoptions 'syncCompanies' 'true'
    Write-SyncOption $syncoptions 'syncLocations' 'true'
    Write-SyncOption $syncoptions 'syncDepartments' 'true'
    Write-SyncOption $syncoptions 'syncManagers' 'true'
    Write-SyncOption $syncoptions 'actionDisabledUsers' 'd'
    Write-SyncOption $syncoptions 'actionMissingDeletedUsers' 'd'
    Write-SyncOption $syncoptions 'loginType' '0'
    Write-SyncOption $syncoptions 'defaultCulture' '1'
    Write-SyncOption $syncoptions 'newUserPasswordBehaviour' 'random'

    $syncdata.AppendChild($syncoptions) > $null
    $xmlWriter.AppendChild($syncdata) > $null
}

function Write-Detail([System.Xml.XmlElement] $element, [string] $optionName, [string] $optionValue){
    # Write a syncoption element using the passed values
    $ele = $xmlWriter.CreateElement("", $optionName, "")
    if($optionValue -ne "") {
      $ele.InnerText = $optionValue 
    }
    $element.AppendChild($ele) > $null
}

function Write-PersonElement([System.Xml.XmlElement] $users, [System.Object] $user){

    $xmluser = $xmlWriter.CreateElement('', 'user', '')
    
    $xmluser.setAttribute('uid', $user.id)
    $xmluser.setAttribute('dn', $user.id)
    $xmluser.setAttribute('username', $user.profile.login)
    $xmluser.setAttribute('email', $user.profile.mail)

    # Call the function to write person specific elements
    $xmlperson = $xmlWriter.CreateElement("", "person", "")

    #$xmlperson.setAttribute('uid', $user.id)
    Write-Detail $xmlperson 'firstname' $user.profile.firstName
    Write-Detail $xmlperson 'surname' $user.profile.lastName
    Write-Detail $xmlperson 'title' ''
    Write-Detail $xmlperson 'initials' ''
    Write-Detail $xmlperson 'jobtitle' $user.profile.title
    Write-Detail $xmlperson 'phone' $user.profile.primaryPhone
    Write-Detail $xmlperson 'mobile' $user.profile.mobilePhone
    Write-Detail $xmlperson 'fax' ''
    Write-Detail $xmlperson 'extension' ''
    Write-Detail $xmlperson 'address' ($user.profile.streetAddress + "`r`n" + $user.profile.city + "`r`n" + $user.profile.state + "`r`n" + $user.profile.zipCode + "`r`n" + $user.profile.countryCode)

    $xmluser.AppendChild($xmlperson)

    # Create user specific elements
    Write-Detail $xmluser 'statusenabled' 'true'
    Write-Detail $xmluser 'password' $defaultUserPassword
    Write-Detail $xmluser 'culture' '1'
        
    $lang = $xmlWriter.CreateElement("", "language", "")
    $lang.setAttribute('id', '1')
    $xmluser.appendChild($lang)
        
    # Create additional fields
    $addfields = $xmlWriter.CreateElement("", "additionalfields", "")
    
    $xmluser.appendchild($addfields)

    # Call the function to write the manager element
    Write-ManagerElement $xmluser $user

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

    # Write a person child element for a user
    $users.AppendChild($xmluser)    
}


function Write-AdditionalField([System.Xml.XmlElement] $fields, [string] $fieldName, [string] $fieldValue){
    # Write an additional field element using the passed values
    $field = $xmlwriter.CreateElement("", "field", "")
    $field.setAttribute('name', $fieldName)
    $field.InnerText = $fieldValue
    $fields.AppendChild($field)
}


function Write-ManagerElement([System.Xml.XmlElement] $xmluser, [System.Object] $user){
    # Write the manager element for a user
    $xmlManager = $xmlWriter.CreateElement("", "manager", "")
    $xmlManager.setAttribute("uid", $user.profile.managerId)
    $xmlManager.SetAttribute("dn", $user.profile.managerId)

    $xmluser.AppendChild($xmlManager)
}


function Write-PrimaryOrganisationElement([System.Xml.XmlElement] $xmlorg, [string] $organisationType, [string] $organisationValue){    
    # Write an organisation element using the passed values
    $org = $xmlWriter.CreateElement("", 'organisation', "")
    $org.SetAttribute('type', $organisationType)
    $org.SetAttribute('primary', 'true')
    $org.InnerText = $organisationValue
    $xmlorg.AppendChild($org)
}


function Write-OrganisationsElement([System.Xml.XmlElement] $xmluser, [System.Object] $user){
    # Write the organisations element for a user
    $xmlorg = $xmlWriter.CreateElement("", "organisations", "")

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

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

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

    $xmluser.AppendChild($xmlorg)
}


function Write-UsersAndGroups(){
    # Write the groups element of the document
    $groups = $xmlWriter.CreateElement("", "groups", "")
    $users = $xmlWriter.CreateElement("", "users", "")
    
    $grpcnt = 0
    $usercnt = 0

    $oktagroups = getOktaData("/api/v1/groups")
    
    foreach ($group in $oktagroups) 
    { 
        Write-Host ""
        Write-Host "Processing group " $group.id $group.profile.name
        # Create group element with attributes
        
        $grp = $xmlWriter.CreateElement("", "group", "")
        $grp.SetAttribute("uid", $group.id)
        $grp.SetAttribute("dn", $group.id)
        $grp.SetAttribute("name", $group.profile.name)
        
        $groupusers = $xmlWriter.CreateElement("", "users", "")
        
        # get users in the group
        $grpUsers = getOktaData($group._links.users.href)
        $grpUsercount = 0

        foreach ($grpUser in $grpUsers)
        {
            Write-Host "Processing member " $grpUser.id $grpUser.profile.login

            if($grpUser.status -eq "ACTIVE"){
              if(!$processedusers.ContainsKey($grpuser.id)){
                $processedusers.Add($grpuser.id, $grpuser.id)
                Write-PersonElement $users $grpUser
                $usercnt+=1
              }
            
              $xmlgrpuser = $xmlWriter.CreateElement("", "user", "")
              $xmlgrpuser.SetAttribute('uid', $grpUser.id)
              $xmlgrpuser.SetAttribute('dn', $grpUser.id)
              $xmlgrpuser.SetAttribute('username', $grpUser.profile.login)
              $xmlgrpuser.SetAttribute('email', $grpUser.profile.email) 
            
              $groupusers.AppendChild($xmlgrpuser)
                
              $grpUsercount+=1
            }
        }
        
        if($grpUsercount -ne 0){
          $grp.SetAttribute("UserCount", $grpUsercount)

          $grp.AppendChild($groupusers)
        
          $groups.AppendChild($grp)

          $grpcnt += 1
        }
    }
    $users.SetAttribute("TotalUsers", $usercnt)
    $groups.SetAttribute("TotalUsers", $usercnt)
    $groups.SetAttribute("TotalGroups", $grpcnt)

    $xmlWriter.DocumentElement.AppendChild($users)
    $xmlWriter.DocumentElement.AppendChild($groups)
}


function getOktaData([string] $apiPath){
  $url = ($uri + $apiPath)
  
  if($apiPath.Contains("http")){
    $url = $apiPath
  }
  
  $response = Invoke-WebRequest -Uri $url -Method GET -Headers @{Authorization= 'SSWS ' + $apiKey} 
  $response = ConvertFrom-Json $response
  return $response
}

###########################################
#
# The main execution sequence of the script
#
$processedusers = @{}

Write-Host "Starting..."

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

# Set up the XML document
$xmlWriter = New-Object System.Xml.XmlDocument
$xmlDeclaration = $xmlWriter.CreateXmlDeclaration("1.0", "UTF-8", $null)
$root = $xmlWriter.DocumentElement
$xmlWriter.InsertBefore($xmlDeclaration, $root) > $null

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

Write-Host "Processing groups..."
Write-UsersAndGroups

# Final tidy up actions
$xmlWriter.Save($xmlpath)

Write-Host "Complete"

# Deliver file to API endpoint
Invoke-RestMethod -Uri $interactUrl -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...

Set up

First, we set up some variables that relate to your particular instance on Interact and the information needed to connect to the Okta API to get user and group information

# OKTA variables

$uri = "[FQDN FOR OKTA (e.g. https://inkconsultants.oktapreview.com)]"
$apiKey = "[API KEY FOR OKTA (e.g. ad0c98bSSMx38El)]"

#Interact variables

$interactUrl = "[URL TO INTERACT UPLOAD (e.g. https://inkconsultants.interactgo.com/api/umi/1001/upload)]"
$authtoken = "[AUTH KEY ENTERED INTO INTERACT - (e.g. 1234567890asdawnc)]"
$domain = "[NAME OF PROFILE SOURCE ADDED TO INTERACT]"
$ldapId = "[ID OF PROFILE SOURCE IN INTERACT]"
$xmlPath = "[FILENAME AND PATH OF XML FILE TO BE PRODUCED]"

Then, a series of functions are defined that generate the XML for a user within Okta and the various synchronization options from within profile sources.

Synchronization options

The functions for generating the XML for users and groups should be fairly self-explanatory so let's 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' '0'
    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 synchronize 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 synchronize 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 synchronize 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 either 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 the appropriate authentication method - please see the schema documentation for more information

  • defaultCulture - this needs to be present and set to the appropriate culture - please see the schema documentation for more information

  • 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
#
$processedusers = @{}

Write-Host "Starting..."

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

# Set up the XML document
$xmlWriter = New-Object System.Xml.XmlDocument
$xmlDeclaration = $xmlWriter.CreateXmlDeclaration("1.0", "UTF-8", $null)
$root = $xmlWriter.DocumentElement
$xmlWriter.InsertBefore($xmlDeclaration, $root) > $null

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

Write-Host "Processing groups..."
Write-UsersAndGroups

# Final tidy up actions
$xmlWriter.Save($xmlpath)

Write-Host "Complete"

# Deliver file to API endpoint
Invoke-RestMethod -Uri $interactUrl -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, builds the and XML elements by calling the relevant functions (Write-UsersAndGroups), 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.