Generate an XML file from Okta
Note: This script is an example of how to generate XML from different sources for use with Interact's profile sources. Customisation of this script is not supported by Interact. Use it as a starting point and tweak it as necessary.
Before you start
- You need a General Profile Source configured in Interact. See General Profile Sources.
- You need PowerShell and an Okta API key.
Overview
With PowerShell, you can gather user data from your Okta account via the Okta API, generate an XML file, and send the file to Interact to synchronise via General Profile Sources. The script also synchronises Okta groups into Interact as security groups, so you can manage group membership for permissions in Okta and have it flow through to Interact.
Note: The Okta API returns large result sets across multiple pages. This example retrieves a single page of groups and members; for larger directories, extend
getOktaDatato follow the pagination links Okta returns in the responseLinkheaders.
The script
# 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)
$ele.InnerText = $optionValue
$element.AppendChild($ele) > $null
}
function Write-DocumentBase(){
# Write the document root (syncdata) and syncoptions elements
$syncdata = $xmlWriter.CreateElement("", "syncdata", "")
$syncdata.SetAttribute("version", "1")
$syncoptions = $xmlWriter.CreateElement("", "syncoptions", "")
$syncoptions.SetAttribute("domain", $domain)
$syncoptions.SetAttribute("ldapid", $ldapId)
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] $elementName, [string] $elementValue){
# Write a named child element, leaving it empty when no value is supplied
$ele = $xmlWriter.CreateElement("", $elementName, "")
if($elementValue -ne "") {
$ele.InnerText = $elementValue
}
$element.AppendChild($ele) > $null
}
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) > $null
}
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) > $null
}
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) > $null
}
function Write-OrganisationsElement([System.Xml.XmlElement] $xmluser, [System.Object] $user){
# Write the organisations element for a user, including only fields that have a value
$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) > $null
}
function Write-PersonElement([System.Xml.XmlElement] $users, [System.Object] $user){
# Build a user element with its person, language, additional fields, manager and organisations children
$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.email)
$xmlperson = $xmlWriter.CreateElement("", "person", "")
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) > $null
Write-Detail $xmluser 'statusenabled' 'true'
Write-Detail $xmluser 'password' ''
Write-Detail $xmluser 'culture' '1'
$lang = $xmlWriter.CreateElement("", "language", "")
$lang.SetAttribute("id", "1")
$xmluser.AppendChild($lang) > $null
# Additional fields: call Write-AdditionalField $addfields 'field_name' $value to populate these
$addfields = $xmlWriter.CreateElement("", "additionalfields", "")
$xmluser.AppendChild($addfields) > $null
Write-ManagerElement $xmluser $user
Write-OrganisationsElement $xmluser $user
$users.AppendChild($xmluser) > $null
}
function getOktaData([string] $apiPath){
# Call the Okta API and return the parsed response
$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
}
function Write-UsersAndGroups(){
# Query Okta for groups and their members, building the users and groups elements
$groups = $xmlWriter.CreateElement("", "groups", "")
$users = $xmlWriter.CreateElement("", "users", "")
$grpcnt = 0
$usercnt = 0
$oktagroups = getOktaData("/api/v1/groups")
foreach ($group in $oktagroups)
{
$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 the users in the group
$grpUsers = getOktaData($group._links.users.href)
$grpUsercount = 0
foreach ($grpUser in $grpUsers)
{
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) > $null
$grpUsercount += 1
}
}
if($grpUsercount -ne 0){
$grp.SetAttribute("UserCount", $grpUsercount)
$grp.AppendChild($groupusers) > $null
$groups.AppendChild($grp) > $null
$grpcnt += 1
}
}
$users.SetAttribute("TotalUsers", $usercnt)
$groups.SetAttribute("TotalUsers", $usercnt)
$groups.SetAttribute("TotalGroups", $grpcnt)
$xmlWriter.DocumentElement.AppendChild($users) > $null
$xmlWriter.DocumentElement.AppendChild($groups) > $null
}
###########################################
#
# The main execution sequence of the script
#
$processedusers = @{}
# 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
# Build the syncoptions
Write-DocumentBase
Write-Host "Processing users and groups..."
Write-UsersAndGroups
# Save the document to disk
$xmlWriter.Save($xmlPath)
Write-Host "Complete"
# Deliver the file to the API endpoint
Invoke-RestMethod -Uri $interactUrl -Method Post -InFile $xmlPath -ContentType "multipart/form-data" -Headers @{'X-ApiKey'=$authtoken}
Step 1: Set up the variables
At the top of the script, set the variables that relate to your Interact instance and the information needed to connect to the Okta API:
# 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]"
The Write- functions that follow generate the XML for each user and group, and the synchronisation options. The getOktaData function wraps the Okta API calls. You should not need to change these unless you want to map different Okta profile attributes.
Step 2: Understand the synchronisation options
The synchronisation options are set in the Write-DocumentBase function. When you use profile sources, Interact takes these options from the XML source itself rather than from the screens within Interact. Each option is described below.
- syncCompanies — whether to synchronise user companies. If a company does not exist in Interact, it is created automatically and the user is assigned to it.
- syncLocations — whether to synchronise user locations. If a location does not exist in Interact, it is created automatically and the user is assigned to it.
- syncDepartments — whether to synchronise user departments. If a department does not exist in Interact, it is created automatically and the user is assigned to it.
- syncManagers — whether to synchronise the manager relationship between users.
- actionDisabledUsers — what to do with users marked as disabled in the XML:
x— do nothing; do not update the Interact status of those users.d— deactivate those users in Interact.a— deactivate and archive users in Interact.
- actionMissingDeletedUsers — what to do with users previously created in Interact with this profile source, or not assigned to any groups in the XML. The options are the same as above.
- loginType — must be present and set to the appropriate authentication method. See the schema documentation.
- defaultCulture — must be present and set to the appropriate culture. See the schema documentation.
- newUserPasswordBehaviour — how Interact creates passwords for new users:
strict— use the password specified in the user part of the XML.random— create a random password for each user. This example usesrandom, so no password is set in the user XML.
Step 3: Understand the main execution sequence
The main part of the script (under The main execution sequence of the script) ties everything together. It sets up the XML document and declaration, writes the synchronisation options with Write-DocumentBase, then calls Write-UsersAndGroups, which queries Okta for the groups and their members and builds the <users> and <groups> elements. Finally it saves the file to disk and posts it to your profile source endpoint with Invoke-RestMethod.
Troubleshooting
PowerShell reports errors from the script itself, and Interact returns useful error messages if the XML is invalid, so troubleshooting is straightforward. See Troubleshooting for more information.