Hi @kompala,
That’s an excellent question, and you’ve correctly identified a key behavior of Identity Security Cloud’s provisioning engine. The short answer is yes, this is the expected behavior, and your AfterModify script should be designed to handle it. Let’s break down why this happens and how to solve both of your questions.
Part 1: Why Your AfterModify Script Runs Twice
The behavior you’re seeing is a result of two distinct actions that ISC performs when you disable an identity:
-
The Disable Operation: First, ISC sends a Disable operation to the connector to disable the account on the target system (Active Directory in your case). Your AfterModify hook correctly fires after this operation completes.
-
Attribute Sync Enforcement: Immediately following the disable operation, ISC’s Attribute Sync feature kicks in. It performs a real-time check to ensure all account attributes are synchronized with the identity attributes defined in your configuration. This sync triggers a separate Modify operation, which also causes your AfterModify hook to run a second time.
This is documented behavior and ensures data consistency between ISC and the target source. The key is to make your script “operation-aware” so it only executes your custom logic when the desired operation occurs.
Solution: Check the Operation Type
As the Ambassador mentioned, the solution is to inspect the AccountRequest object within your PowerShell script. This object contains an Operation property that tells you exactly which action triggered the script. You can read this from the $env:Request environment variable.
Here is a PowerShell code example of how to make your script operation-aware:
# Add the SailPoint Utils DLL to parse the request object
Add-Type -Path "C:\SailPoint\IQService\utils.dll"
# Parse the XML request string from the environment variable
$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)
# Get the operation type (e.g., "Disable", "Modify", "Enable", etc.)
$operation = $requestObject.Operation.ToString()
# --- Your Custom Logic Here ---
# Only execute your custom logic for the 'Disable' operation
if ($operation -eq "Disable") {
LogToFile "Operation is 'Disable'. Running custom downstream logic..."
#
# YOUR CUSTOM LOGIC GOES HERE
# (e.g., calling other applications)
#
} else {
LogToFile "Operation is '$operation'. Skipping custom downstream logic."
}
By implementing this check, your custom logic will only run for the initial Disable operation, and it will gracefully skip the subsequent Modify operation from Attribute Sync.
Part 2: Solving Multi-Threaded Logging
Your second question about logging in a multi-threaded environment is a common challenge when dealing with parallel provisioning in IQService. When multiple threads try to write to the same log file simultaneously, you get file locking errors and jumbled logs.
Solution: Use a Named Mutex for Thread-Safe Logging
The best practice for this scenario is to use a named Mutex (Mutual Exclusion object). A mutex ensures that only one thread can access the log file at a time, preventing conflicts. Other threads will wait patiently until the file is released.
Here is a robust, thread-safe logging function you can add to your script. It uses a global named mutex and includes the Thread ID and Process ID in each log entry, making it easy to trace the execution flow for each request.
# --- Thread-Safe Logging Function using a Mutex ---
# Create a globally named mutex to ensure it's shared across all sessions and processes
$logMutex = New-Object System.Threading.Mutex($false, "Global\\SailPointADAfterModifyLog")
function LogToFile([String] $message) {
# Wait up to 5 seconds to acquire the mutex
if ($logMutex.WaitOne(5000)) {
try {
$logFile = "C:\SailPoint\Scripts\Logs\AfterModify_$(Get-Date -Format 'yyyy-MM-dd').log"
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
$threadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId
$processId = $PID
# Include the Account ID and Operation for easy tracing
$accountId = $requestObject.AccountId
$operation = $requestObject.Operation.ToString()
$logEntry = "[$timestamp] [TID:$threadId] [PID:$processId] [Account:$accountId] [Op:$operation] - $message"
$logEntry | Out-File -FilePath $logFile -Append
} finally {
# ALWAYS release the mutex, even if errors occur
[void]$logMutex.ReleaseMutex()
}
} else {
Write-Warning "Failed to acquire log mutex within 5 seconds. Log entry was lost."
}
}
# --- Example Usage ---
LogToFile "Script started."
if ($operation -eq "Disable") {
LogToFile "Running custom logic for Disable operation."
# ... your code ...
} else {
LogToFile "Skipping custom logic for $operation operation."
}
LogToFile "Script finished."
This approach gives you a single, clean, and chronologically ordered log file where you can easily filter and trace the actions for each parallel request without any data loss or file locking errors.
References
- Before and After Operations on Source Account Rule: Before and after operations on source account Rule | SailPoint Developer Community
- ProvisioningPlan.AccountRequest.Operation Enum: ProvisioningPlan.AccountRequest.Operation
- Handling Multiple Threads in PowerShell (Community Discussion): Handling of Multiple Threads in PowerShell Script in Connector After Create Rule
- Using Mutexes for Thread-Safe Logging in PowerShell (External Blog): Using Mutexes to Write Data to the Same Logfile Across Processes With PowerShell | Learn Powershell | Achieve More