There have been scenarios wherein the business requirement needs a feed to be generated out of IdentityIQ periodically and the feed will be consumed by some other downstream application(s). The scenario is quite easy to be implemented in IdentityIQ by means of Rule runner task.
Another scenario could be that you have to generate a detailed task result report after a rule runner task execution completed just like we do get a detailed report after account aggregation task completion (E.g. Account ID, Correlate Type, Action etc.) if configured in the task definition.
So far so good… Generating a report file from within Rule runner task is not difficult. The file will be written on a task server on which the rule runner task ran through scheduler. Now the situation here is some customers follows a manual process wherein Ops team gets hold of that report file by raising some request ticket to a different team having access to the servers. So, the pain point here is a manual intervention is always needed to get the file available with Ops team for further consumption.
Why not enable a self-service for them?!! That would reduce a considerable number of tickets.
The answer to the question is adding custom JasperReport with the rule runner task. Not only rule runner task, but the solution would work with custom tasks as well. Since rule runner task is frequently used by SailPoint developers to implement these scenarios, hence the example has used the same.
SailPoint IdentityIQ uses a third party framework namely JasperReport in reporting module. SailPoint community may not be right place to discuss the framework in detail. When I implemented the solution, I found this very good for beginner (even I was also beginner!!).
In a nutshell, developers need to design a JasperReport XML (JRXML in short) which contains the definition of the report i.e. what all column will be present, from where to fetch data dynamically, visual layout etc. Once JRXML is ready, the XML would need to be imported into IdentityIQ which in turn will convert the JRXML into JasperTemplate. The task result report is saved as jasperResult in IdentityIQ database. [Here I made my first mistake when started with this as when I looked in the debug page, it shows JasperTemplate, not JasperReport. So, I thought I needed to formulate a JasperTemplate just like Workflow, Rule, and Identity object. But later I realized that was wrong!! IdentityIQ uses its own Jasper compiler to convert a JRXML to JasperTemplate. So, JRXML is your deliverable, not JasperTemplate]
The example use case to showcase Jasper reporting implementation is that there will be a task which will be responsible for generating CSV file containing all correlated identity details e.g. first name, last name etc. I know this is very simple example and people may argue that this can be implemented out of the box using IdentityIQ reporting module as well. But let me tell you the scenario for which I started my research – For one customer, we needed to automate CSV file (the files being used in delimited file connector based business applications) transfer from a FTP location into IdentityIQ task servers. To do that, we wrote a task which will securely copy the files from FTP into UNIX task servers boxes and at the same time, the task result must keep the status record per file whether transfer executed successfully or with failure (if so, what is the failure message). The result report will then be revisited to troubleshoot and resolve the failure.
The attached zipped folder contains two files:
JRXML: CustomUserReport
Rule: Generate User Report
The first of the rule is quite straightforward. You have just write code as per your scenario to derive the data to be made available in task result report.
The second part of the rule does the actual thing i.e. formulating JasperResult and attaching that with the task result so that the report can be downloaded from Task results page IdentityIQ
The rule has a reusable function called generateTaskReport() which takes the report data in the form of List and does the above
The above function uses a few unexposed IdentityIQ API to achieve the goal as stated below:
reporting.ReportingUtil
reporting.datasource.DelimitedFileDataSource
reporting.datasource.TaskResultDataSource
The rule passes a List object containing field reference names while instantiating DelimitedFileDataSource object. The names in the list MUST match with the field reference names configured in JRXML. [I made mistake here that field names I was passing was completely different from the ones present in JRXML. But no worry!! We know the right thing now!!]
The real painful portion in this implementation was to adjust row height and width. It depends on the data length and number of fields. Need to be little patient to bring the report look and feel to the intended visual layout.
Once all coding is done, then run the rule runner task. You would be able to see the task result report like below. The report file can be downloaded in either pdf/excel format by clicking on the icon.
Hi Kalyan,
Where we need to place the JRXML file ..?
Regards,
Akhil
Please refer the web inf folder in IIQ
Hi Santosh and Kalyan,
I'm getting the below error , when I'm running the Task. I have place the file in webapps\identityiq\WEB-INF\config\jrxml\ it is not working though..
java.security.PrivilegedActionException: org.apache.bsf.BSFException: The application script threw an exception: sailpoint.tools.GeneralException: Unabled to load JasperTemplate class:CustomUserReport from the database BSF info: Generate User Report at line: 0 column: columnNo
at java.security.AccessController.doPrivileged(Native Method)
at org.apache.bsf.BSFManager.eval(BSFManager.java:442)
at sailpoint.server.BSFRuleRunner.eval(BSFRuleRunner.java:224)
at sailpoint.server.BSFRuleRunner.runRule(BSFRuleRunner.java:194)
at sailpoint.server.InternalContext.runRule(InternalContext.java:1207)
at sailpoint.server.InternalContext.runRule(InternalContext.java:1179)
at sailpoint.task.RuleExecutor.runRule(RuleExecutor.java:169)
at sailpoint.task.RuleExecutor.execute(RuleExecutor.java:114)
at sailpoint.api.TaskManager.runSync(TaskManager.java:890)
at sailpoint.api.TaskManager.runSync(TaskManager.java:719)
at sailpoint.scheduler.JobAdapter.execute(JobAdapter.java:128)
at org.quartz.core.JobRunShell.run(JobRunShell.java:202)
at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573)
Caused by: org.apache.bsf.BSFException: The application script threw an exception: sailpoint.tools.GeneralException: Unabled to load JasperTemplate class:CustomUserReport from the database BSF info: Generate User Report at line: 0 column: columnNo
at bsh.util.BeanShellBSFEngine.eval(BeanShellBSFEngine.java:193)
at org.apache.bsf.BSFManager$5.run(BSFManager.java:445)
... 13 more
Could please help me how this file will be compiled and inserted into spt_jasperResult
Thanks,
Hi Kalyan,
Below is the code. When I add Joiner code, then report is not getting generated, although there is no error.
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<Rule language="beanshell" name="Brand-Rule-Global-BulkMasterRule">
<ReferencedRules>
<Reference class="sailpoint.object.Rule" name="Brand-RuleLibrary-Global-BulkUpload"/>
<Reference class="sailpoint.object.Rule" name="Brand-Rule-Global-IdentityCreateAndUpdate-RuleLibrary"/>
<Reference class="sailpoint.object.Rule" name="Brand-Rule-CommonUtilRule"/>
<Reference class="sailpoint.object.Rule" name="Brand-Rule-Global-Joiner-RuleLibrary"/>
</ReferencedRules>
<Source>
import java.util.Iterator;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import sailpoint.object.Filter;
import sailpoint.object.Custom;
import sailpoint.object.Rule;
import sailpoint.object.QueryOptions;
import org.apache.log4j.Logger;
Logger logger = Logger.getLogger("Rule.Global.BulkMasterRule");
String gid = null;
String requestor = null;
List reportData = new ArrayList();
Filter myFilter = Filter.like("name", "BulkUser_",Filter.MatchMode.ANYWHERE);
QueryOptions qo = new QueryOptions();
qo.addFilter(myFilter);
Iterator results = context.search(Custom.class, qo);
logger.debug("********Before while*******");
while(results.hasNext()){
logger.debug("Inside while");
String validatorMethodName = null;
String userType = null;
List inputData = null;
List errorMsgList = null;
List errorList = null;
String inputLine = null;
String errorMsg = "";
String createUserMethod = null;
String joinerProvisionerMethod = null;
Custom custObj = results.next();
if(custObj.get("runState").equalsIgnoreCase("ready")) {
logger.debug("Processing::"+custObj.getName());
custObj.put("runState","processed");
context.saveObject(custObj);
context.commitTransaction();
userType = custObj.get("userType");
requestor = custObj.get("requestor");
errorList = new ArrayList();
Custom allowedValues = context.getObjectByName(Custom.class,"Brand-Custom-BulkUpload");
validatorMethodName = allowedValues.get("ValidatorMethod").get(userType);
createUserMethod = allowedValues.get("UserCreationMethod").get(userType);
joinerProvisionerMethod = allowedValues.get("ProvisionerMethod").get("User");
inputData = custObj.get("content");
logger.debug("Method that needs to be called for validation is"+validatorMethodName+":::"+createUserMethod);
for(int i=1;i<inputData.size();i++) {
argument = inputData.get(i);
logger.debug("The data sent is:"+argument);
errorMsgList = eval(validatorMethodName+"(argument)");
if(errorMsgList.isEmpty()){
logger.debug("Identity Creation Starts");
//Below line create user in IIQ
gid = eval(createUserMethod+"(argument)");
if(gid != null){
argument1 = gid;
//Below line triggers Joiner. Because of below line report is no getting generated in Task result
String result = eval(joinerProvisionerMethod+"(argument1)");
Identity identityObj = context.getObjectByName(Identity.class,gid);
List idenLinkList = identityObj.getLinks();
for (Link adLink : idenLinkList){
String appName = adLink.getApplicationName();
if(appName.contains("AD")){
String lineItem = identityObj.getFirstname()+","+identityObj.getLastname()+","+identityObj.getAttribute("ntid")+","+identityObj.getAttribute("identityType")+","+identityObj.getName()+","+"Success";
reportData.add(lineItem);
}
}
}
}
if(!errorMsgList.isEmpty()){
inputLine = argument;
errorMsg = inputLine + ",";
for(String error : errorMsgList){
errorMsg = errorMsg + "|" + error;
}
if(errorMsg.length()>0){
errorList.add(errorMsg.replace(",|",","));
}
errorMsg = null;
errorMsgList.clear();
}
}
if(!errorList.isEmpty()) {
logger.debug("errorList********* "+errorList);
sendStatusEmail(errorList,requestor,userType);
}
}
}
if(reportData != null && !reportData.isEmpty())
generateTaskReportAndSendEmail(reportData,requestor);
reportData.clear();
config.clear();
context.decache();
return "Success";
</Source>
</Rule>
=========================================================================================
public void generateTaskReportAndSendEmail(List reportData, String requestor)
{
List reportColumn = new ArrayList();
reportColumn.add("sno");
reportColumn.add("firstname");
reportColumn.add("lastname");
reportColumn.add("ntid");
reportColumn.add("identitytype");
reportColumn.add("globalid");
reportColumn.add("status");
String folderName = "/saildata/IIQ/BulkUpload/BulkOnboardingResultReports/";
String fileName = "ProcessedRecords" + System.currentTimeMillis() + ".csv";
String fileString = folderName + "/" + fileName;
File file = new File(fileString);
if (file != null){
FileWriter writer = new FileWriter(file);
if (writer != null){
int i = 0;
Iterator it = reportData.iterator();
while(it.hasNext()){
writer.write((++i)+","+it.next().toString());
writer.write("\n");
}
writer.close();
}
}
String taskResultName = "Brand-TaskDefinition-RunRule-BulkMasterRule";
TaskResult taskResult = context.getObjectByName(TaskResult.class,taskResultName);
DelimitedFileDataSource ds = new DelimitedFileDataSource(fileString, reportColumn);
TaskDefinition taskDef = context.getObjectById(TaskDefinition.class,taskResult.getDefinition().getId());
Signature sig = taskDef.getEffectiveSignature();
HashMap params = new HashMap();
params.put("taskResultDataSource",new TaskResultDataSource(sig != null ? sig.getReturns() : null, taskResult.getAttributes()));
JasperResult reportResult = ReportingUtil.fillReport(context, "Brand-JasperReport-BulkUserOnboarding-ProcessTaskReport", ds, params);
context.startTransaction();
context.saveObject(reportResult);
taskResult.setReport(reportResult);
context.saveObject(taskResult);
context.commitTransaction();
System.out.println("Report file saved successfully!!");
emailProcessedUserDetails(reportData,requestor);
}
public void emailProcessedUserDetails(List reportData, String requestor){
String folderName = "/saildata/IIQ/BulkUpload/BulkOnboardingResultReports/";
String fileName = "UserDetails" + System.currentTimeMillis() + ".csv";
String fileString = folderName + "/" + fileName;
File file = new File(fileString);
if (file != null){
FileWriter writer = new FileWriter(file);
if (writer != null){
int i = 0;
writer.write("S.No"+","+"First Name"+","+"Last Name"+","+"Network Id"+","+"IdentityType"+","+"Global Id"+","+"Status");
writer.write("\n");
Iterator it = reportData.iterator();
while(it.hasNext()){
writer.write((++i)+","+it.next().toString());
writer.write("\n");
}
writer.close();
}
}
byte[] byteFile = new byte[(int) file.length()];
FileInputStream fileInputStream = null;
try{
fileInputStream = new FileInputStream(file);
fileInputStream.read(byteFile);
fileInputStream.close();
for (int i = 0; i < byteFile.length; i++){
System.out.print((char) byteFile[i]);
}
}
catch (Exception e){
e.printStackTrace();
}
Identity requesterNameObj = context.getObjectByName(Identity.class,requestor);
String emailDest = requesterNameObj.getEmail();
String requesterFName = requesterNameObj.getFirstname();
String requesterLName = requesterNameObj.getLastname();
Map args = new HashMap();
args.put("requesterName",requesterFName+" "+requesterLName);
EmailFileAttachment attachment = new EmailFileAttachment(fileName, EmailFileAttachment.MimeType.MIME_CSV, byteFile);
EmailOptions ops = new EmailOptions(emailDest, args);
ops.addAttachment(attachment);
EmailTemplate template = context.getObject(EmailTemplate.class, "Brand-EmailTemplate-Global-BulkUserProcessedReport");
context.sendEmailNotification(template, ops);
System.out.print("Email sent for report");
}
Below is the method which calls Joiner.
public String method_ProcessJoiner(String identityName){
Identity identity = context.getObjectByName(Identity.class,identityName);
String brand = identity.getAttribute("compName");
String identityType = identity.getAttribute("identityType");
Custom brandMapping = context.getObject(Custom.class, "Brand-Custom-Brands");
String brandCode = getValueByKeyForBrand(brand,"brandCode");
String emailId = null;
if(brandCode.equalsIgnoreCase("***")){
emailId = generateCCLUserEmailId(identityName);
identity.setAttribute("email",emailId);
context.saveObject(identity);
context.commitTransaction();
}
if(brandCode.equalsIgnoreCase("***") || brandCode.equalsIgnoreCase("***")){
emailId = generateHALUserEmailId(identityName);
identity.setAttribute("email",emailId);
context.saveObject(identity);
context.commitTransaction();
}
Custom joinerLCMCustomConfiguration = context.getObjectByName(Custom.class,"Brand-Custom-Global-LCM-Joiner");
List planList = new ArrayList();
ProvisioningPlan provisioningPlan = new ProvisioningPlan();
String brightRightAssignment = joinerLCMCustomConfiguration.getString(brandCode+"_Joiner");
if(brightRightAssignment != null){
Map birthRightApplicationMap = joinerLCMCustomConfiguration.get(brightRightAssignment);
if(birthRightApplicationMap != null) {
List birthRightApplicationList = birthRightApplicationMap.get(identityType);
if(birthRightApplicationList != null && !birthRightApplicationList.isEmpty()) {
for(String applicationName : birthRightApplicationList) {
AccountRequest accountRequest = new AccountRequest();
accountRequest.setApplication(applicationName);
accountRequest.setOperation(ProvisioningPlan.AccountRequest.Operation.Create);
planList.add(accountRequest);
}
}
}
}
provisioningPlan.setIdentity(identity);
provisioningPlan.setAccountRequests(planList);
HashMap launchArgsMap = new HashMap();
launchArgsMap.put("identityDisplayName",identityName);
launchArgsMap.put("identityName",identityName);
launchArgsMap.put("identityRequestId","");
launchArgsMap.put("foregroundProvisioning","true");
launchArgsMap.put("plan",provisioningPlan);
launchArgsMap.put("trace","true");
launchArgsMap.put("approvingIdentities","none");
launchArgsMap.put("approvalScheme","none");
launchArgsMap.put("approvalSet","");
launchArgsMap.put("notificationScheme","none");
launchArgsMap.put("flow","BulkJoiner");
launchArgsMap.put("policyScheme","none");
launchArgsMap.put("workItemDescription","none");
launchArgsMap.put("workItemEscalationRule","none");
launchArgsMap.put("workItemHoursTillEscalation","none");
launchArgsMap.put("source","LCM");
WorkflowLaunch wflaunch = new WorkflowLaunch();
Workflow wf = (Workflow) context.getObjectByName(Workflow.class,"Brand-Workflow-LCM-Provisioning");
wflaunch.setWorkflowName(wf.getName());
wflaunch.setWorkflowRef(wf.getName());
wflaunch.setCaseName("Bulk Joiner :"+identityName);
wflaunch.setVariables(launchArgsMap);
//Create Workflower and launch workflow from WorkflowLaunch
Workflower workflower = new Workflower(context);
WorkflowLaunch launch = workflower.launch(wflaunch);
logger.debug("User AD link is processed");
return "Success";
}
Hello @Akhil ,
Not sure if you are interested in this topic.
So my understanding is that the compiled jrxml (.jasper) can be stored under the JasperTemplate object.
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperReport;
import sailpoint.object.JasperTemplate;
String sourceFileName = "//path//to//CustomUserReport.jrxml";
try {
JasperReport jasperReport = JasperCompileManager.compileReport(sourceFileName);
JasperTemplate jasperTemplate = new JasperTemplate(jasperReport);
context.saveObject(jasperTemplate);
context.commitTransaction();
} catch (JRException e) {
e.printStackTrace();
}
return "success";
Hope this is helpful to you.
- Mike