Executing PowerShell Rules in IdentityIQ

The code snippet can trigger the PowerShell Rule within workflow in IIQ for various business use cases and we can also validate the Powershell code.

  1. Rule Runner task for launching the Workflow with the config parameters from the task.
 import sailpoint.object.WorkflowLaunch;
  import sailpoint.object.Workflow;
  import sailpoint.api.Workflower;
 //config.get() parameters can be passed in the task definition
  HashMap launchArgsMap = new HashMap(); 
  launchArgsMap.put("identityName",config.get("identity")); 
  launchArgsMap.put("launcher", "spadmin");
  launchArgsMap.put("powerShellRuleName", "PowerShell-Rule-UAT");
  launchArgsMap.put("adApplicationName", "Active Directory");
  launchArgsMap.put("identityDisplayName",config.get("identity"));
  WorkflowLaunch wflaunch = new WorkflowLaunch();
  Workflow wf = null;  
  try { 
  wf = (Workflow) context.getObjectByName(Workflow.class, config.get("workflowName"));  
  } 
  catch (GeneralException e) {
    log.error(e);  
  } 
  if (wf != null) {  
    Workflower workflower = new Workflower(context);
    try {  
      WorkflowLaunch launch = workflower.launchSafely(wf, wf.getName()+" for Identity  :"+config.get("identity"), launchArgsMap);
    } catch (GeneralException eGeneralException) {  
      log.error(eGeneralException);  
    }  
  }
  1. The workflow will execute and return the RPC response to the root workflow context.
import sailpoint.object.RpcRequest;
import sailpoint.object.RpcResponse;
import sailpoint.connector.RPCService;
import sailpoint.object.Identity;
import sailpoint.object.Link;
import sailpoint.object.Rule;
import sailpoint.object.Custom;
import sailpoint.object.Application;
import sailpoint.object.ProvisioningPlan.AccountRequest;
import sailpoint.object.ProvisioningPlan.AccountRequest.Operation;
import sailpoint.object.ProvisioningPlan.AttributeRequest;
import sailpoint.object.ProvisioningPlan.Operation;
import sailpoint.object.ProvisioningPlan;
import sailpoint.object.ProvisioningProject;
import sailpoint.object.Attributes;
import sailpoint.tools.Util;
import sailpoint.tools.Message;
import sailpoint.tools.GeneralException;
import sailpoint.api.IdentityService;
import org.apache.log4j.Logger;

//Map of data to be passed to the RPCRequest
Map data = new HashMap();
Identity identity = context.getObjectByName(Identity.class, identityName);
if (identity == null)
throw new GeneralException("Could not find identity for name [" + identityName + "] in worklfow Workflow-InvokePowerShell");

Rule powershellRule = context.getObjectByName(Rule.class, powerShellRuleName);
if (powershellRule == null)
throw new GeneralException("Could not find PowerShell rule for name: " + powerShellRuleName);

Application application = context.getObjectByName(Application.class, adApplicationName);
if (application == null)
throw new GeneralException("Could not find application for name [" + adApplicationName + "] in worklfow Workflow-InvokePowerShell");

if (application != null && powershellRule != null) {
IdentityService idService = new IdentityService(context);
data.put("Application", application.getAttributes());

//Fake or real account request
AccountRequest accountRequest = new AccountRequest();
Link adLink;
//Attempt to get the Active Directory AccountRequest for the project if it exists
if (project != null) {
logger.debug("Project passed in is non-null, getting AD request from it:\n");
for (ProvisioningPlan plan : Util.safeIterable(project.getIntegrationPlans())) {
for (AccountRequest acctReq : Util.safeIterable(plan.getAccountRequests(adApplicationName))) {
accountRequest = acctReq;
logger.debug("Found AccountRequest for AD:\n" + acctReq.toXml());
break;
}
}
if (accountRequest != null) {
//If we already have an AccountRequest, let's throw in some additional attributes from and existing link
List adLinks = idService.getLinks(identity, application);
if (!Util.isEmpty(adLinks)) {
adLink = adLinks.get(0);
}
}
} else {
//If a project was not passed in, get the current AD account from the identity
logger.debug("Creating a fake account request and adding some reference attributes to it");
List adLinks = idService.getLinks(identity, application);
if (!Util.isEmpty(adLinks)) {
adLink = adLinks.get(0);
accountRequest.setApplication(adApplicationName);
accountRequest.setNativeIdentity(adLink.getNativeIdentity());
accountRequest.setOperation(AccountRequest.Operation.Modify);
}
}
//Add some helpful attributes purely for PowerShell reference purposes
if (adLink != null) {
if (Util.isNotNullOrEmpty(adLink.getAttribute("userPrincipalName"))) {
accountRequest.add(new AttributeRequest("userPrincipalName", ProvisioningPlan.Operation.Set, adLink.getAttribute("userPrincipalName")));
}
if (Util.isNotNullOrEmpty(adLink.getAttribute("sAMAccountName"))) {
accountRequest.add(new AttributeRequest("sAMAccountName", ProvisioningPlan.Operation.Set, adLink.getAttribute("sAMAccountName")));
}
if (Util.isNotNullOrEmpty(adLink.getAttribute("givenName"))) {
accountRequest.add(new AttributeRequest("givenName", ProvisioningPlan.Operation.Set, adLink.getAttribute("givenName")));
}
if (Util.isNotNullOrEmpty(adLink.getAttribute("sn"))) {
accountRequest.add(new AttributeRequest("sn", ProvisioningPlan.Operation.Set, adLink.getAttribute("sn")));
}
if (powerShellRuleName.equals("Rule-PowerShell-LitigationHoldMailbox-UAT")) {
Attributes attr = application.getAttributes();
Map map = attr.getMap();
String completeUserName = map.get("onlineExchangeUser");
String password = context.decrypt(map.get("onlineExchangePassword"));
accountRequest.add(new AttributeRequest("user", ProvisioningPlan.Operation.Set, completeUserName));
accountRequest.add(new AttributeRequest("password", ProvisioningPlan.Operation.Set, password));
accountRequest.add(new AttributeRequest("email", ProvisioningPlan.Operation.Set, identity.getEmail()));
}
}
logger.debug("Final AccountRequest to be passed to the RPCRequest:\n" + accountRequest.toXml());
// Add to the IQService params
logger.debug("Adding IQService params");
data.put("postScript", powershellRule);
data.put("Request", accountRequest);
logger.debug("Added script and request objects to data map successfully");
RpcResponse rpcResponse;
try {
List iqServiceConfig = application.getAttributeValue("IQServiceConfiguration");
if (iqServiceConfig != null && !iqServiceConfig.isEmpty()) {
Map iqServiceDetailsMap = iqServiceConfig.get(0);
if (iqServiceDetailsMap != null && !iqServiceDetailsMap.isEmpty()) {
String iqServiceHost = iqServiceDetailsMap.get("IQServiceHost");
int iqServicePort = Integer.parseInt(iqServiceDetailsMap.get("IQServicePort"));
RPCService service = new RPCService(iqServiceHost, iqServicePort, false, false);
service.setConnectorServices(new sailpoint.connector.DefaultConnectorServices());
RpcRequest request = new RpcRequest("ScriptExecutor", "runAfterScript", data);
logger.debug("Executing RPC Request...");
rpcResponse = service.execute(request);
logger.debug("RPC Request completed");

if (rpcResponse != null) {
logger.debug("RPCResponse:\n" + rpcResponse.toXml());
if (!Util.isEmpty(rpcResponse.getErrors())) {
logger.debug("Errors from the RPC Response:\n" + rpcResponse.getErrors().toString());
//wfcontext.getRootWorkflowCase().addMessage(Message.error("IQService Error: " + rpcResponse.getErrors().toString()), null);
}
} else {
logger.debug("RPCResponse is null!");
}
}
}
} catch(Exception e) {
/*
* If there are errors in the RPCResponse object, an exception will be throw so we need to add them
* to the root workflowcase here so the TaskResult shows them
*/
logger.error("RPC Request compelted with error: " + e.getMessage());
//wfcontext.getRootWorkflowCase().addMessage(Message.error("IQService Error: " + e.getMessage(), null));
if (rpcResponse != null) {
logger.debug("RPCResponse returned from script:\n" + rpcResponse.toXml());
if (!Util.isEmpty(rpcResponse.getErrors())) {
logger.debug("Errors from the RPC Response:\n" + rpcResponse.getErrors().toString());
//You can handle custom errors for creating Icnident or Email to Support Teams
Custom errorMsg = context.getObjectByName(Custom.class,"Custom-PowerShell-Errors");
Map customErrors = errorMsg.getAttributes();
wfcontext.getRootWorkflowCase().addMessage(Message.error(customErrors.get(powerShellRuleName) + rpcResponse.getErrors().toString(), null));
}
} else {
logger.debug("RPCResponse is null!");
}
}
}
  1. The Reference PowerShell script
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<Rule created="" id="" language="beanshell" modified="" name="Example-PowerShell-LitigationHold-Script" type="ConnectorAfterCreate">
<Attributes>
<Map>
<entry key="ObjectOrientedScript" value="true" />
<entry key="disabled" value="false" />
<entry key="extension" value=".ps1" />
<entry key="program" value="powershell.exe" />
<entry key="timeout" value="1200" />
</Map>
</Attributes>
<Source>
#Refer to SailPoint class library. Requires V3 EXO PowerShell installed on the system.
#ErrorActionPreference is important to add in the ps scripts to "stop" the execution on exceptions or failures
Add-type -Path D:\IQService\Utils.dll
$ErrorActionPreference = "Stop";
$LoggingEnabled = $true;

#Read our application for tokenized attributes
#parseObject will return application object as a Hashtable
#get xmlFactory object
$xmlFactory = [sailpoint.Utils.xml.XmlFactory]::Instance;

#Read the environment variables
$sReader1 = [System.String]$env:Application;

#Remove any line containing '&lt;Date>' because IQService was expecting date in milliseconds
#but application contains date in the format MM/DD/YY HH:MM:SS AM
$escaped = $sReader1 -split "`n" | Select-String -Pattern "Date" -NotMatch

#Convert String array to String
$content = $escaped | Out-String

#Create stringReader object from app xml string
$stringReader = New-Object -TypeName System.IO.StringReader -ArgumentList $content

#New xml reader object
$xmlreaderApp = [System.Xml.XmlTextReader] [sailpoint.Utils.xml.XmlUtil]::getReader([System.IO.TextReader]$stringReader);

#This step is missing in existing IQservice
$xmlreaderApp.MoveToContent()

#parsObject will return application object as a Hashtable
$appObject = $xmlFactory.parseObject($xmlreaderApp)

#Get variables from app object for mailbox creation
$environmentUpn = $appObject.upnDomainName
$exchangeServer = $appObject.exchangeServer
$exchangeTargetAddress = $appObject.exchangeTargetAddress

#Build some variables
$IQServiceUtilsPath = "D:\IQService\Utils.dll"
$IQServiceLogPath = "D:\IQService\IQOutput.txt"

#Read the environment variables
$stringRequest = New-Object System.IO.StringReader([System.String]$env:Request);
$stringResult = New-Object System.IO.StringReader([System.String]$env:Result);

# Form the xml reader objects
$xmlReader = [System.xml.XmlTextReader]([sailpoint.utils.xml.XmlUtil]::getReader($stringRequest));
$xmlReader_Result = [System.xml.XmlTextReader]([sailpoint.utils.xml.XmlUtil]::getReader($stringResult));

# Create SailPoint objects
$requestObject = New-Object Sailpoint.Utils.objects.AccountRequest($xmlReader);
$resultObject = New-Object Sailpoint.Utils.objects.ServiceResult($xmlReader_Result);
$objectType = "AccountRequest";
$sAMAccountName = "";
$userPrincipalName = "";
$givenName = "";
$sn = "";
$username = "";
$password = "";
$email = "";
#This error list is important to catch the errors in a list and return them #to root context
$Error_List = New-Object -TypeName 'System.Collections.ArrayList';

# Check if the request was processed successfully
foreach ($attribute in $requestObject.AttributeRequests) {
if ($LoggingEnabled) {
Write-Output ("Attribute Name: " + $attribute.Name + "; Attribute Value: " +  $attribute.Value) | Out-File -Append $IQServiceLogPath;
}
if ($attribute.Name -eq "userPrincipalName") {
$userPrincipalName = $attribute.Value;
Write-Output ("userPrincipalName: " + $userPrincipalName) | Out-File -Append $IQServiceLogPath;
}
if ($attribute.Name -eq "sAMAccountName") {
$sAMAccountName = $attribute.Value;
Write-Output ("sAMAccountName: " + $sAMAccountName) | Out-File -Append $IQServiceLogPath;
}
if ($attribute.Name -eq "user") {
$username = $attribute.Value;
}
if ($attribute.Name -eq "password") {
$password = $attribute.Value;
}
if ($attribute.Name -eq "email") {
$email = $attribute.Value;
Write-Output ("email: " + $email) | Out-File -Append $IQServiceLogPath;
}

}
# The log file should be created dynamically rather than appending to the the same file,if same file is used the log may lead to dead lock situations # in concurrent operations.
# Start-transcript is a Microsoft module for logging the powershell code.
# This creates a record of all or part of a PowerShell session to a text file.

if ($resultObject.Errors.count -eq 0) {
Start-transcript -path "D:\ExchangeLogs\$sAMAccountName.txt" -Append
if ($objectType -eq "AccountRequest") {
$server = $resultObject.Attributes["createdOnServer"];
Try {
if ($email) {
#The authentioncation method is changed from Basic to Certificate thumbprint as per guidelines
Connect-ExchangeOnline -AppId XXXXXXXXXXXXXXXXXXXX -CertificateThumbprint XXXXXXXXXXXXXXXXXXXX -Organization ACME.onmicrosoft.com
try {
Set-Mailbox $email -LitigationHoldEnabled $true -ErrorAction Stop
if ($LoggingEnabled) {
Write-Output "Remote mailbox LitigationHoldEnabled successfully"
}
} Catch {
$Error_List.Add($_.Exception.Message)
Write-Output ("An error occurred while enabling litigation hold: $_.Exception.Message")
}

try {
Set-CalendarProcessing -Identity $email -ResourceDelegates $null  -Confirm:$false -ErrorAction Stop
if ($LoggingEnabled) {
Write-Output "CalendarProcessing ResourceDelegates successfully"
}
} Catch {
Write-Output $_.Exception
}

$calendar = $email + ":\Calendar"

try {
Remove-MailboxFolderPermission -Identity $calendar -ResetDelegateUserCollection -Confirm:$false -ErrorAction Stop
if ($LoggingEnabled) {
Write-Output "Remove MailboxFolderPermission successfully"
}
} Catch { Write-Output $_.Exception}

if ($LoggingEnabled) {
try {
Set-CASMailbox -Identity $email -OWAforDevicesEnabled $false -ErrorAction stop
Write-Output "CASMailbox OWAforDevicesEnabled disabled successfully"
}
catch {Write-Output $_.Exception}

try {
Set-CASMailbox -Identity $email -OWAEnabled $false -ErrorAction stop
Write-Output "CASMailbox OWAEnabled disabled successfully"
}
catch {Write-Output $_.Exception}

try {
Set-CASMailbox -Identity $email -ActiveSyncEnabled $false -ErrorAction stop
Write-Output "CASMailbox ActiveSyncEnabled disabled successfully"
}
catch {Write-Output $_.Exception}
try {
Set-CASMailbox -Identity $email -MAPIEnabled $false -ErrorAction stop
Write-Output "CASMailbox MAPIEnabled disabled successfully"
}
catch {Write-Output $_.Exception}

try {
Set-CASMailbox -Identity $email -ImapEnabled $false  -ErrorAction stop
Write-Output "CASMailbox ImapEnabled disabled successfully"
}
catch {Write-Output $_.Exception}

try {
Set-CASMailbox -Identity $email -PopEnabled $false  -ErrorAction stop
Write-Output "CASMailbox PopEnabled disabled successfully"
}
catch {Write-Output $_.Exception} $CASMailboxFeaturesStatus=Get-CASMailbox -Identity $email|Select @{N="Email";E={$_.PrimarySmtpAddress}},OWAforDevicesEnabled,OWAEnabled,ActiveSyncEnabled,MAPIEnabled,ImapEnabled,PopEnabled
Write-Output $CASMailboxFeaturesStatus
}
}

$groupToAdd = 'AP_Applicaion-Leaver-Group'
Try {
Write-Output ("Adding "+$email+" to "+ $groupToAdd)
Add-DistributionGroupMember -Identity $groupToAdd -Member $email -BypassSecurityGroupManagerCheck -confirm:$false -ErrorAction stop
if ($LoggingEnabled) {
Write-Output ("DistributionGroupMember " + $groupToAdd + " added successfully")
}
} Catch {
# The powershell exceptions are hard to interpret in some situations. so, it is always better to have the custom message returned to workflow.
Write-Output $_.Exception
}
Write-Output ("************END of LitigationHold Script***************")
}
Catch {
Try {
Disconnect-ExchangeOnline -Confirm:$false
} Catch {
Write-Output "PowerShell-LitigationHoldMailbox script was not successfuly completed"
}
$ErrorMessage = $_.Exception.Message
$FailedItem = $_.Exception.ItemName
if ($LoggingEnabled) {
Write-Output $ErrorMessage
Write-Output $resultObject.toxml()
}
$resultObject.Errors.Add($ErrorMessage);
} Finally {
#This is must to Disconnect-ExchangeOnline session in finally block and catch the erros in this block
Disconnect-ExchangeOnline -Confirm:$false
if($Error_List -ne $null){
$resultObject.Errors.Add($Error_List);
}

# Dump the result object back to the out-file passed in as arg 0 so any updates are fed back to IIQ
$resultObject.toxml() | out-file $args[0];
}
} else {
if ($LoggingEnabled) {
Write-Output ("Request type was not an AccountRequest, skipping Remove-RemoteMailbox command")
}
}
Stop-transcript
} else {
# Add an error to let IIQ know nothing was done since there were errors as the connector level
$resultObject.Errors.Add("IQService: Errors occured during provisioning, AfterScript took no action");

# Dump the result object back to the out-file passed in as arg 0 so any updates are fed back to IIQ
$resultObject.toxml() | out-file $args[0];
}
</Source>
</Rule>

@Akhil thanks for sharing :+1: