So you’ve been writing and using simple BeanShell rules in SailPoint IdentityIQ but you’ve come to a point in your solving of use cases where you’ve got code replication in various places. This, as in other development situations outside of SailPoint IdentityIQ, is a perfect scenario for consolidating such code into a library of some sort (you are thinking, right?!) and calling that code from the rules you are writing.
Code consolidation is just good, universally accepted development practice. But can this be done in SailPoint IdentityIQ, and if so, how? Glad you asked. Here’s how you do it. We’ll use an over-simplified example in a very easy use case to illustrate.
Creating A Rule Library
The easiest way to create a rule library from scratch is to go into the SailPoint IdentityIQ debug pages and grab a rule you already have. Grab the rule XML from the text area and cut and paste it into your favorite editor. Then pare your XML down to this:
<?xml version='1.0' encoding='UTF-8'?> <!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd"> <Rule language="beanshell" name="My Library"> <Source> // My Library - only a comment for now... :-) </Source> </Rule>
Ha, well I guess… there you go. You can now just use the above as a rule library template instead of digging this out of your SailPoint IdentityIQ debug pages. 🙂
Save this to an XML file on your local hard drive. Make sure you change the name of the library on the 3rd line above to something that makes sense for you. Then import this XML into SailPoint IdentityIQ. You can import this XML in one of two ways:
(1) Navigate to the System Setup page and choose the “Import From File” option, or…
(2) Import from the IIQ console using the import command.
Now, re-navigate to your debug pages, re-list your rules and you should see a rule named “My Library” (or whatever else you might have named your rule). For updating this rule and actually adding code, you’ll need to edit this rule from right here in the debug pages as it’s not going to show up anywhere else, really. We’ll keep that in mind for later.
The Background/Sample Use Case
Okay, so now you’ve created a rule library — simply a place to stick code that will be shared by other rules. How to we reference this library?
Before we get into that, let’s look at our use case code. We have two build map rules for aggregation — one build map rule called from a CSV connector and the other build map rule from a JDBC connector. In both cases, we’re going to say each needs to build a string formatted a certain way, and we want to isolate this formatting to one place — in our new rule library — and call that code from both rules.
Here is the CSV build map rule:
// Imports. import sailpoint.object.Schema; import sailpoint.connector.Connector; import sailpoint.connector.DelimitedFileConnector; // Build an initial map from the current record. HashMap map = DelimitedFileConnector.defaultBuildMap( cols, record ); // Only perform these steps for account aggregations. if (schema.getObjectType().compareTo( Connector.TYPE_ACCOUNT ) == 0) { String path = map.get( "path" ); String filename = map.get( "filename" ); String filespec = path + "/" + filename; map.put( "filespec", filespec ); } // Return the resulting map. For group aggregations, the default // map falls through and is returned. For account maps, we return // the modified map. return map;
The JDBC build map is very similar, only it uses a JDBC class method to build the initial, default map instead of a delimited file class method:
// Imports. import sailpoint.object.Schema; import sailpoint.connector.Connector; import sailpoint.connector.JDBCConnector; // Build an initial map from the Java ResultSet object. HashMap map = JDBCConnector.buildMapFromResultSet( result ); // Only perform these steps for account aggregations. if (schema.getObjectType().compareTo( Connector.TYPE_ACCOUNT ) == 0) { String path = map.get( "path" ); String filename = map.get( "filename" ); String filespec = path + "/" + filename; map.put( "filespec", filespec ); } // Return the resulting map. For group aggregations, the default // map falls through and is returned. For account maps, we return // the modified map. return map;
It turns out, we have an upcoming requirement for these connectors. The incoming data is going to be augmented to include a flag field which denotes “U” for “Unix” or “W” for Windows. Based on this flag, the filespec needs to be returned, formated properly with the correct delimiter — “/” for Unix and “\” for Windows. (We will assume the “path” field/variable will always have the correct filespec delimiter in its value. In a real use case, this may or may not be a good thing to assume.)
So taking our CSV code, we could enhance like this:
// Imports. import sailpoint.object.Schema; import sailpoint.connector.Connector; import sailpoint.connector.DelimitedFileConnector; // Build an initial map from the current record. HashMap map = DelimitedFileConnector.defaultBuildMap( cols, record ); // Only perform these steps for account aggregations. if (schema.getObjectType().compareTo( Connector.TYPE_ACCOUNT ) == 0) { // Initial fields. String path = map.get( "path" ); String filename = map.get( "filename" ); String filesystemFlag = map.get( "filesystem" ); // By default, we'll just assume Unix. String filespec = path + "/" + filename; // We'll correct for Windows if the filesystem is Windows. if (filesystemFlag.toUpperCase().equals( "W" )) filespec = path + "\" + filename; // We have a "filespec" in our account schema that this // value from the map will be stuffed into. map.put( "filespec", filespec ); } // Return the resulting map. For group aggregations, the default // map falls through and is returned. For account maps, we return // the modified map. return map;
That’s great and it works, but… Now we need to add the same thing to our JDBC build map. And we “hear” there may be more changes coming for these connectors. Older legacy systems such as VMS may need to be implemented later. This is starting to look ugly. Time to refactor this into callable code which we reference from a rule library.
Let’s add the correct code to the rule library we created (that just has a comment in it right now) and then call this code from our two build map rules.
Adding Code To Library & Referencing
Remember what I said before about how you will need to edit your library code? Your build map rules are accessible from the dropdowns on the application definition pages in the SailPoint IdentityIQ GUI. Your rule library code must be accessed from the debug pages. To edit your library code then and add the correct code, go to your debug pages, list your libraries, and select the “My Library” rule. We’ll place this code in it, and I’ll leave the full XML in the listing which follows as this is about what it will look like for you:
<?xml version='1.0' encoding='UTF-8'?> <!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd"> <Rule created="1346904363361" id="4028804238c8e33f013999c1916129b5" language="beanshell" modified="1347373888659" name="Aggregation Library"> <Source> public String formatFilespec( String path, String file, String flag ) { // Assuming Unix pathing by default. String filespec = path + "/" + file; // Correct for Windows systems. if (flag.toUpperCase().equals( "W" )) filespec = path + "\" file; return filespec; } </Source> </Rule>
Great. Now we can just call this code from both rules.* For CSV we would have:
// Imports. import sailpoint.object.Schema; import sailpoint.connector.Connector; import sailpoint.connector.DelimitedFileConnector; // Build an initial map from the current record. HashMap map = DelimitedFileConnector.defaultBuildMap( cols, record ); // Only perform these steps for account aggregations. if (schema.getObjectType().compareTo( Connector.TYPE_ACCOUNT ) == 0) { // Initial fields. String path = map.get( "path" ); String filename = map.get( "filename" ); String filesystemFlag = map.get( "filesystem" ); // We have a "filespec" in our account schema that this // value from the map will be stuffed into. map.put( "filespec", formatFilespec( path, filename, filesystemFlag ) ); } // Return the resulting map. For group aggregations, the default // map falls through and is returned. For account maps, we return // the modified map. return map;
We could even, if we wanted to, shorten the build map rule code to look something like this:
// Imports. import sailpoint.object.Schema; import sailpoint.connector.Connector; import sailpoint.connector.DelimitedFileConnector; // Build an initial map from the current record. HashMap map = DelimitedFileConnector.defaultBuildMap( cols, record ); // Only perform these steps for account aggregations. if (schema.getObjectType().compareTo( Connector.TYPE_ACCOUNT ) == 0) { // We have a "filespec" in our account schema that this // value from the map will be stuffed into. map.put( "filespec", formatFilespec( map.get( "path" ), map.get( "filename" ), map.get( "filesystem" ) )); } // Return the resulting map. For group aggregations, the default // map falls through and is returned. For account maps, we return // the modified map. return map;
Now, that’s much tighter and much more readable and concise.
But how does SailPoint IdentityIQ know to use the code in our library? Does it just “see” the library some how? It turns out, no, it’s not quite that easy. We have to tell SailPoint IdentityIQ what libraries we want to reference from within each rule. This unfortunately is slightly involved, but it’s the only way (that I know of anyway!).
First, we have to edit our build map rules in XML format from the debug pages. You cannot perform this step from the application definition pages in the regular GUI. So go back to the debug pages and list your rules. Find your build map rules. They should look very similar to this:
<?xml version='1.0' encoding='UTF-8'?> <!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd"> <Rule created="1346967271470" id="4028804238c8e33f01399d81782d2a8b" language="beanshell" modified="1347373870815" name="BuildMap - My CSV Application" type="BuildMap"> <Description>This rule is used by the delimited file connector to build a map representation of the delimited data.</Description> <Signature returnType="Map"/> <Source>// Imports. import sailpoint.object.Schema; import sailpoint.connector.Connector; import sailpoint.connector.DelimitedFileConnector; // Build an initial map from the current record. HashMap map = DelimitedFileConnector.defaultBuildMap( cols, record ); // Only perform these steps for account aggregations. if (schema.getObjectType().compareTo( Connector.TYPE_ACCOUNT ) == 0) { // We have a "filespec" in our account schema that this // value from the map will be stuffed into. map.put( "filespec", formatFilespec( map.get( "path" ), map.get( "filename" ), map.get( "filesystem" ) )); } // Return the resulting map. For group aggregations, the default // map falls through and is returned. For account maps, we return // the modified map. return map;</Source> </Rule>
What you want to do is add a special XML tag section in your existing rule to reference your rule library. This will tell SailPoint IdentityIQ specifically to reference your library code. You want to add this XML section:
<ReferencedRules> <Reference class="sailpoint.object.Rule" name="My Library"/> </ReferencedRules>
You want to add this XML section right after the </Description>
closing tag in your build map rule. So it will look like this:
<?xml version='1.0' encoding='UTF-8'?> <!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd"> <Rule created="1346967271470" id="4028804238c8e33f01399d81782d2a8b" language="beanshell" modified="1347373870815" name="BuildMap - My CSV Application" type="BuildMap"> <Description>This rule is used by the delimited file connector to build a map representation of the delimited data.</Description> <ReferencedRules> <Reference class="sailpoint.object.Rule" name="My Library"/> </ReferencedRules> <Signature returnType="Map"/> <Source>// Imports. import sailpoint.object.Schema; import sailpoint.connector.Connector; import sailpoint.connector.DelimitedFileConnector; // Build an initial map from the current record. HashMap map = DelimitedFileConnector.defaultBuildMap( cols, record ); // Only perform these steps for account aggregations. if (schema.getObjectType().compareTo( Connector.TYPE_ACCOUNT ) == 0) { // We have a "filespec" in our account schema that this // value from the map will be stuffed into. map.put( "filespec", formatFilespec( map.get( "path" ), map.get( "filename" ), map.get( "filesystem" ) )); } // Return the resulting map. For group aggregations, the default // map falls through and is returned. For account maps, we return // the modified map. return map;</Source> </Rule>
Save that build map rule! Now, of course you want to test your code, probably from IIQ console using connector debug (eg. connectorDebug
). You should, if you’ve done everything correctly, see the results in connector debug and in any real aggregation that you are expecting to see, called from a regular IIQ task definition.
Finally, one more thing that I do when I am referencing code from a library. I add a comment at the top of the code to indicate it has a library reference. You can do this from the debug pages, editing the rule XML inside the <Source>
tag, or… you can edit the code from the application definition page in the GUI. But I always add a comment like this to the top:
// Referenced Libraries: // - My Library // // Imports. : :
Now when I or anyone else opens this code in the GUI from the application definition page, I can tell it has a library reference. Short of editing your code from the debug pages (which some people do!), you can’t tell from the GUI that your code is referencing a library. So comment accordingly.
Ease Of Refactoring
So, let’s take this one step further, and we’ll see how refactoring our code in this way and calling it from both rules (eg. JDBC and CSV) is really going to save us time and heartache.
Our use case requirements have changed again. It’s possible that our file system type could be Unix, Windows, or… a nasty legacy system such as VMS. Under these new requirements, we could read a “U” for Unix, “W” for Windows, or… “V” for “VMS”. But now, instead of writing these changes to two rules, we make the changes in our rule library (from the debug pages) and both of our connectors should still work.
Here’s the requirement for the VMS filespec:
The path coming from the outlying system will be in the following format:
/vms-device/path/to/file
This has has to be changed into VMS format and returned as “filespec” so, this would be:
path=/dua1/accounting/payables/customer001 filename=ajax_payables.csv
Becomes: DUA1:[ACCOUNTING.PAYABLES.CUSTOMER001]AJAX_PAYABLES.CSV;
Now, because we’ve used a rule library, this is going to be fairly easy. Here’s our new library code:
<?xml version='1.0' encoding='UTF-8'?> <!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd"> <Rule created="1346904363361" id="4028804238c8e33f013999c1916129b5" language="beanshell" modified="1347373888659" name="Aggregation Library"> <Source> public String formatFilespec( String path, String file, String flag ) { // Assuming Unix pathing by default. String filespec = path + "/" + file; // Correct for Windows systems. if (flag.toUpperCase().equals( "W" )) { filespec = path + "\" file; // Correct for VMS systems. Return in VMS format: // DEVICE:[DIRECTORY]FILENAME.EXT; } else if (flag.toUpperCase().equals( "V" )) { String tokens[] = path.split( "/" ); String device = tokens[0]; String directory = ""; for (int i=1; i<tokens .length; i++) { if (directory.length() == 0) { directory = tokens[i]; } else { directory = directory + "." + tokens[i]; } } filespec = device.toUpperCase() + ":" + "[" + directory.toUpperCase() + "]" + file.toUpperCase() + ";"; } return filespec; }</Source> </Rule>
Now that’s not code you want to be replicating between multiple rules. Someone’s going to get hurt that way. 🙂 Much easier to isolate that kind of logic, as you guessed, and keep it all in one safe place.
Incidentally, I would also add, when you have complex logic like this, it’s even better to write this kind of code in Eclipse, NetBeans or some other popular IDE. Stub out what you have to, and make sure the core logic works in a simple Java perspective before moving it into SailPoint IdentityIQ for usage there.
Hope this explains fully how to create, implement and leverage rule libraries in your SailPoint IdentityIQ development exploits!
Cheers from the Twin Cities!
* – Notice how SailPoint IdentityIQ automatically created an “id” and “create” and “modified” dates for the rule library we created? This happened when you imported the plain rule library code, without that information in the XML. SailPoint IdentityIQ created an internal id for this code as well as other rule attributes it uses internally. You will not need to include these attributes yourself. In fact, if you try to, you will get an error at import.