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.
Updated over 1 year ago