BeforeCreate script not returning updated XML AccountRequest correctly

Greetings! New to SailPoint, so I feel like this is going to be a question with either a very simple fix or expose my lack of deep understanding of the platform. I’m fine with either, any knowledge helps!

I am trying to create BeforeCreate rule/script for an on-prem Active Directory source that generates a sAMAccountName value (and by extension a new nativeIdentity value) based on some custom logic. I’ve gathered from extensive reading of the discussions here that this might not be the preferred or optimal method of accomplishing this, but it’s unlikely that my org will be exposing all of the attributes that we look at when determining a sAMAccountName to ISC, so we’re left with IQService, PowerShell, and a BeforeCreate rule.

To start, I’m keeping it simple: I want to be able to provision an account with a static username. I’ll worry about our more complicated logic once I’ve proven the simple example works.

The documentation on this process is not as clear as I’d like it to be. Reading through a combination of that and other discussions here, I think I’ve almost got it: rule was created and attached to the connector, rule successfully fires and executes a local script on the IQService host, local script successfully generates the new XML structure to feed back to IQservice. Where this falls apart is getting the modified AccountRequest back to IQservice.

SailPoint documentation (and other discussions here) states that this is done with a statement like this:

$requestObject.toxml()|out-file $args[0];

This statement produces an error when executed by IQService: “Cannot index on a null array” and IQService proceeds to provision the account with the original attributes passed to it by the rule. I’m at a loss to understand why $args[0] would be empty, or if there’s some other mechanism I should be using to pass the modified request back to IQService.

Here’s the existing rule. Most of this is taken directly from SailPoint documentation for creating rules:

$logDate = Get-Date -UFormat "%Y%m%d"
$logFile = "D:\IQServiceLogs\BeforeCreateRule\BeforeCreate_$logDate.log"
$command = "D:\IQservice\Scripts\New-sAMAccountName.ps1"
$enableDebug = $true

#====================-------Helper functions-------====================
function LogToFile([String] $info) {
    $info | Out-File $logFile -Append
}

#====================-------Get the request object-------====================
Try{
    if($enableDebug) {
        LogToFile("Entering SailPoint rule")
    }

    Add-type -path utils.dll;
    $sReader = New-Object System.IO.StringReader([System.String]$env:Request);
    $xmlReader = [System.xml.XmlTextReader]([sailpoint.utils.xml.XmlUtil]::getReader($sReader));
    $requestObject = New-Object Sailpoint.Utils.objects.AccountRequest($xmlReader);
    $requestAsString = $env:Request

    if($enableDebug) {
        LogToFile("Request as XML object is: $requestAsString")
    }

    #Call the client script
    $command = -join ($command, " -requestString '$requestAsString'")
    Invoke-Expression $command

}Catch{
    $ErrorMessage = $_.Exception.Message
   $ErrorItem = $_.Exception.ItemName
   LogToFile("Error: Item = $ErrorItem -> Message = $ErrorMessage")
}

if($enableDebug) {
    LogToFile("Exiting SailPoint rule")
}

And here’s the PowerShell script that executes on the IQService host:

##############################################################################################################################
# SETUP
# Instructions (for each IQService host that could run the script):
#   - Update the path to Utils.dll (can be an unqualified path like "Utils.dll" since script is copied to IQService folder for execution)
#   - Make sure Utils.dll is in the specified folder on each IQService host
#   - Be sure the account that runs IQService has appropriate permissions to create directories and set permissions on them
#   - Be sure to set the "run as" account for the IQService in Windows Service to the above-specified account instead of just the "logged on" user
#   - Set a proper location for the $logFile variable
#   - Set the $enableDebug flag to $true or $false to toggle debug mode
###############################################################################################################################
 
param (
[Parameter(Mandatory=$true)][System.String]$requestString
)

#include SailPoint library
Add-type -path "D:\IQService\utils.dll";
 
#import AD cmdlets
Import-Module activeDirectory;
 
#log file info
$logDate = Get-Date -UFormat "%Y%m%d";
$logFile = "D:\IQServiceLogs\BeforeCreate\AD-BeforeCreate_$logDate.log";
$enableDebug = $true;
 
#save logging files to a separate txt file
function LogToFile([String] $info) {
	$info | Out-File $logFile -Append;
}

try{
	LogToFile("Starting processing now")
	$sReader = New-Object System.IO.StringReader([System.String]$requestString);
	$xmlReader = [System.xml.XmlTextReader]([sailpoint.utils.xml.XmlUtil]::getReader($sReader));
	$requestObject = New-Object Sailpoint.Utils.objects.AccountRequest($xmlReader);

	$sAMAccountName = "newusername";
	$originalUsernameAttribute = $null;

	LogToFile("Finding the sAMAccountName attribute request")
	foreach ($attribute in $requestObject.AttributeRequests) {
		if ($attribute.Name -eq "sAMAccountName")
		{
			LogToFile("Found sAMAccountName request of $($attribute.Value)")
			$originalUsernameAttribute = $attribute;
		} 
	}

	## Create new username attribute
	LogToFile("Creating new sAMAccountName object")
	$newAttributeValue = New-Object SailPoint.Utils.objects.AttributeRequest;
	$newAttributeValue.Operation = "Add";
	$newAttributeValue.Name = "sAMAccountName";
	$newAttributeValue.Value = $sAMAccountName;

	## Remove the old attribute from the account request
	LogToFile("Removing old sAMAccountName object")
	$requestObject.AttributeRequests.Remove($originalUsernameAttribute);

	## Add the new attribute to the account request
	LogToFile("Adding new sAMAccountName object")
	$requestObject.AttributeRequests.Add($newAttributeValue);

	## Update the CN for the object with the new username.
	$requestObject.nativeIdentity = "CN=$sAMAccountName,OU=AdminAccounts,OU=Users,OU=_LAB,DC=uslab,DC=lab,DC=local";

	## Pass the new account request to SailPoint
	LogToFile("Passing updated attributes to Sailpoint")
	LogToFile($requestObject.toxml())
	$requestObject.toxml()|out-file $args[0];
	LogToFile("End of processing")

} catch {
	$ErrorMessage = $_.Exception.Message
	$ErrorItem = $_.Exception.ItemName
	LogToFile("Error: Item = $ErrorItem -> Message = $ErrorMessage")
}

Hoping the community can help a newbie out! Thanks!

I suspect this may be because you have the wrapper script running the main script. I don’t really see any advantage to this and not sure why sailpoint suggests it. Try putting all your logic in one script and try again.

That was actually the first thing I did, before I saw the documentation that stated that the wrapper should contain only the minimal code required to execute a script local to the IQService host. That also had the same behavior: XML seems to update correctly, but when attempting to pass back to IQService, it fails with the same error message.

I suppose the SailPoint recommendation to split the code makes sense from a maintainability standpoint. There’s fewer hoops to jump through if you need to make a simple change to the do-er script IQService runs if it’s locally hosted. Perhaps for an org with more mature CI/CD capabilities keeping it all in a single script attached to the rule keeps things simpler from a code standpoint.

I suppose that the crux of my question is: what value is IQService expecting PowerShell to provide for $args[0] in this line:

$requestObject.toxml()|out-file $args[0];

Out-File is expecting $args[0] to be some form of file path here. $args[0] would be populated if a parameter is passed into a script absent being defined in a param() block in the script or function.

If the above are correct assumptions, then:

  1. What should be generating the file path Out-File is expecting, and
  2. Where does that value come from in the first place?

@bussdw Are you able to generate logs inside your log file at below location
D:\IQServiceLogs\BeforeCreate\AD-BeforeCreate_$logDate.log

Can you see your updated requestObject, or do you only get the error message ‘Cannot index on a null array’?

Yes, in the log I can see the XML output of the AccountRequest logged with with new nativeIdentity value and modified attribute value for sAMAccountName. Immediately after that is the Cannot index into null array error (see below for log snippet).

Starting processing now
Empty requestObject created from env:Request. Attempting to log object.
Finding the sAMAccountName attribute request
Found sAMAccountName request of <redacted>
Creating new sAMAccountName object
Removing old sAMAccountName object
Adding new sAMAccountName object
Passing updated attributes to Sailpoint
<AccountRequest application="USLAB [source]" op="Create" nativeIdentity="CN=newusername,OU=AdminAccounts,OU=Users,OU=_LAB,DC=uslab,DC=lab,DC=local">
  <AttributeRequest op="Add" name="memberOf" value="CN=usweb_CSGP-DFSDATA_ITOps_NetScripts_read,OU=SailpointManaged,OU=Groups,OU=_LAB,DC=uslab,DC=lab,DC=local" />
  <AttributeRequest op="Add" name="ObjectType" value="User" />
  <AttributeRequest op="Add" name="displayName" value="(Redacted)" />
  <AttributeRequest op="Add" name="givenName" value="(Redacted)" />
  <AttributeRequest op="Add" name="sn" value="(Redacted)" />
  <AttributeRequest op="Add" name="password" value="(Redacted)" />
  <AttributeRequest op="Add" name="employeeID" value="(Redacted)" />
  <AttributeRequest op="Add" name="sAMAccountName" value="newusername" />
</AccountRequest>
Error: Item =  -> Message = Cannot index into a null array.

For clarity, the arbitrary sAMAccountName I’m attempting to use is “newusername”, so the value above is what I would expect the request to look like. I’ve removed some sensitive info, but the other attributes are all as I would expect, and the only attributes we’re modifying are nativeIdentity and sAMAccountName.

@bussdw You can just remove this line $requestObject.toxml()|out-file $args[0]; from powershell script and try. I hope it will work.

Removing that line has the same effect: no change in what is provisioned.