BACKGROUND
When implementing Identity Security Cloud, unique usernames for identities (the uid
identity attribute) will ideally be aggregated directly from the authoritative source, and this is what SailPoint best practices dictate. But sometimes clients have constraints that do not allow this. For example, the HR system’s unique attribute is a personal identification number, and regulations in their country prevent the use of personal information to be used as any attribute of a system profile including username. Additionally, clients can have very complex algorithms for generating unique usernames during user onboarding (think: combining department codes, date of birth, etc.). When things get this complex, the out-of-the-box Username Generator transform just won’t cut it.
That’s where Identity Attribute Rules come to the rescue, but they come with their own set of challenges.
CHALLENGE
Developing a cloud rule for a use case like this can feel like juggling flaming swords. Why? Because we don’t have full control over deployment or logging like we would with a connector rule. (But don’t worry—we’ve got a workaround!)
First things first. As SailPoint documentation explains, when developing necessary customizations, it is desirable to try transforms, connector rules that execute on the Virtual Appliance (VA), or both of them before falling in the cloud rule universe. This has two extremely important benefits:
- Your logic and deployment schedule are under your control. You don’t have to depend on SailPoint support deployment times, a very important matter, mostly when roll out time is near.
- These solutions are much easier to develop, debug and deploy than cloud rules.
But, sometimes (very often to me), the client’s business requirements are much too complex, and a cloud rule is the only way.
SOLUTION
We’ve determined that we need to develop a cloud rule that generates the identity username value when a new identity is aggregated from its HR authoritative source. We are going to use an Identity Attribute Rule, which is documented here:
Identity Attribute rule documentation
But Wait… Isn’t the Documentation Enough?
Although the rule documentation is pretty good overall, and the official docs are technically complete, if you don’t have a lot of Java development experience, the Java Docs can sometimes be a little obtuse. I have spent a good amount of time testing and troubleshooting, so I decided to share the way I develop this kind of rule by breaking it down with a real-world example I’ve tackled.
With that said, let’s start from the beginning. Let’s grab your favorite IDE and begin with the rule structure.
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<Rule name="GenerateUserid" type="IdentityAttribute">
<Description>Generates username value.</Description>
<Source><![CDATA[
.................................
]]></Source>
</Rule>
You will only have to change the name attribute to your desired rule name. In this case, our rule will be named “GenerateUserid”. Then, save the rule with a name that conforms to the expected naming convention:
"Rule - " + <rule type> + " - " + <rule name> + ".xml"
This convention is mandatory, and neither SailPoint nor the validator tool will allow a rule to pass validation without this format. Be careful to use the correct case.
According to this convention, our rule file name will be “Rule - IdentityAttribute - GenerateUserid.xml”.
Inside <![CDATA … ]> we are going to put our BeanShell code. BeanShell is a scripting language derived from Java. Our code will have the following building blocks:
- Imports
- Procedural logic
- Returns
Imports must be at the beginning of the file. All classes that are used must be present in this section.
I called the second section “procedural logic,” because it is in this point where BeanShell differs from Java. When developing my very first rule, I looked up where I should put my class definition as well as where the start or main sections should be placed. The fact is that BeanShell is not Java, although they share syntax. The first line to execute will be the first one following the last import statement. From then on, it will execute every line in sequence.
Our final block is the return statement. This rule expects a string to be returned, and this string is the value that will be assigned to the identity attribute that the rule is attached to. In our case, it will be the uid
attribute, but, in fact, it can be used for any attribute on any identity profile.
Returns statements are not reserved for last line of code. We will see shortly that having several return statements will help us to debug the rule.
INPUT VARIABLES
Before beginning coding, we need to understand what inputs we have available when the rule is called. Remember that this rule will run immediately after an account is aggregated from the authoritative source. Looking at the documentation, SailPoint says we have the following variable objects available:
We can use any of these variables after the import block. We do not need to initialize them. For example, we can have:
import ....
log.info(identity.getStringAttribute("fistname"));
This block will execute and print the identity’s firstname to the log.
Let me explain each variable in detail:
- log: This object represents the connector logger. It is not too useful for cloud rules because the log file is not on the VA like it is for connector rules. To see any debug statement using this logger, you need to submit a ticket to SailPoint support.
- idn: Its methods contain a lot of functionality that can be performed in the tenant. For example, we can obtain an identity with
idn.getIdentityById(someId)
. Consult the IdnRuleUtil docs to see other available methods. - identity: This object represents the actual identity. For example, we can obtain the identity’s email with the
identity.getEmail()
method. - oldValue: A string containing the last value of the identity attribute.
BEING PROACTIVE FACING ERRORS
As mentioned before, this rule is a cloud rule. That means that the rule executes on the tenant side, not on the VA like connector rules. Also, any log statement will be printed out in SailPoint’s internal systems. This makes cloud rule development a bit complex because for every change we have to update the code and ask support to redeploy the rule. If the rule still does not work as expected, we have to open a support ticket, ask for the logs, and try to find the error event at the time the test was performed.
To facilitate troubleshooting, we can be proactive and verify all possible scenarios and return identifiable error messages. Remember that this rule returns some string value, and that value gets assigned to the mapped identity attribute where the rule is applied. So, if an error happens for a particular identity, we would be able to see it on the identity’s attribute. Of course, after testing phase ends, we should remove all error messages so users don’t see them on the UI.
For example, let say that we need to obtain the identity’s social name (preferred name) and subtract the first character. If the technical name of this attribute is socialName
, we would write:
String socialName = idn.getAccountAttribute(applicationName, link.getNativeIdentity(), "socialName");
String firstLetter = socialName.substring(0,1);
But what happens if socialName
is null? The answer is that an exception is thrown at the substring line. This will cause rule to return nothing, but we will have no clue what is going on.
In order to validate socialName
, we can return some identifiable string:
String socialName = idn.getAccountAttribute(applicationName, link.getNativeIdentity(), "socialName");
if(socialName == null)
return "socialNameIsNull";
if(socialName.trim().equalsIgnoreCase(""))
return "socialNameIsEmpty";
String firstLetter = socialName.substring(0,1);
If socialName
does not exists, or if it has a value of empty string (“”), it will return each corresponding message, and those messages will be assigned to the identity’s uid
. After performing aggregation, we can easily find in Search which identities have “socialNameIsNull” or “socialNameIsEmpty” values. If socialName is not null nor empty, the value of the firstLetter
variable can be assigned to the first letter of the socialName
.
USE OF TRY/CATCH TO “LOG” UNEXPECTED ERRORS
As mentioned, we can use return statements in order to return an attribute value with some data validation information. Of course, there’s a chance that an unexpected error may still occur. What we should do in this case is to wrap the entire rule code in a try block. Later in the catch section, we return the exception message. This allows to assign the exception message to the identity attribute (uid
), so if an error occurs when an identity attribute values are being calculated, the error will be shown as the attribute value:
...............
import sailpoint.tools.GeneralException;
try {
<rule code>
} catch (Exception e) {
return e.getMessage();
}
CONTROLLING RULE EVALUATION
The rule will be evaluated after an aggregation during identity attribute calculation and in identity profile refresh. When generating username for the uid
attribute, this can lead to errors. Let’s suppose that a new employee, named Julian Sosa, comes into the organization, and HR adds him to the system. Next, aggregation of that HR source will create his identity, and the rule will give us his new employee username. The rule will search ISC and run a uniqueness check in order to find out if some other identity has the generated username value. As no other identities are found with the generated username, the rule will return the string “julian.sosa”. A moment later, the identity profile is refreshed and Rule is evaluated again. As before, the rule will search for an identity with username"julian.sosa". The previously created identity will be returned, and the rule will assume that this username is taken, returning “j.sosa” as the uid
value. This will result in infinite username changes.
To avoid this situation, we will begin our code by checking to see if the current identity already has a username (uid
value). If this happens, it means that rule has already been evaluated for this identity, and we simply return the previously generated username:
try {
if (identity != null && identity.getStringAttribute("uid") != null
&& !identity.getStringAttribute("uid").trim().equals(""))
return identity.getStringAttribute("uid");
CONCRETE EXAMPLE
As the final section, we will create a rule for generating a username for identities with the following requirements:
- Identities will come from HR with first name, last name and social name. Social name is the display name of a person that is not always reflected in his or her id. For example, a wife that takes her husband’s last name, or some person that prefers to be called by his middle name. The rule has to split tokens in the social name to obtain the social first and last name.
- Users may come from two authoritative sources: Web Services HR source for employees, and a Non-Employee form for contractors. Contractors are not registered with social names, just real names. Sources are named “HR” and “Contractors”, respectively.
- Both identity profiles have an attribute called “employeeType” with values “employee” or “contractor”, depending on which source the identity comes from.
- For contractors, plain first name and last name will be used.
- Users may have compound social last name. For example, “Julian De Sosa”. If the rule encounters this case, it must bypass last name prefixes.
- The rule has to search ISC for other identities that may already be using the generated username and propose a new one. When no identities have the generated username, the rule will return this value, and it will be assigned to the
uid
identity attribute. - Business logic for generating username is as follows:
- first name + “.” + last name (julian.sosa)
- first character of first name + “.” + last name (j.sosa)
- first two characters of first name + “.” + first two characters of last name (ju.so)
- first two characters of first name + “.” + last name (ju.sosa)
- first name + “.” + first two characters of last name (julian.so)
- If no unique username is found from above logic:
- first name + “.” + lastname + consecutive number from 1 to 15
Let’s start the code. As shown before, our very first line will be a catch. And right after that we check if the rule has already been evaluated for this identity. For this, we use the identity object that is available:
try {
if (identity != null && identity.getStringAttribute("uid") != null
&& !identity.getStringAttribute("uid").trim().equals(""))
return identity.getStringAttribute("uid");
...............................................................................................................
} catch (Exception e) {
return "exception" + e.getMessage();
}
Next, we define some string variables. We initialize them with a string value that helps us debug if some error occurs (remember we always return a string value as output, so any error will be displayed on identity attribute value).
String socialName = "NullSocialName";
String firstName = "NullFirstName";
String lastName = "NullLastName";
String employeeType = "NullEmpType";
To identity what type of employee it is, we need to know if authoritative source name is “HR” or “Contractors”. To do that, we must navigate through the identity’s source accounts. We will do it with the help of the Links object. A link object represents an identity account. We assume that any identity will not have more than 20 accounts, and traverse through them with an iterator:
List links = identity.getLinks();
if (links == null)
return "identity.getLinks.null";
Iterator iterator = links.iterator();
if (iterator == null)
return "iterator.null";
int max = 0;
while (max < 20 && iterator.hasNext()) {
Link link = (Link) iterator.next();
String applicationName = link.getApplicationName();
Application source = link.getApplication();
if (source == null)
return "source.null";
if (idn == null)
return "idn.null";
if (applicationName.toLowerCase().contains("HR")) {
...............
} else if (applicationName.toLowerCase().contains("Contractors")) {
...............
}
max++;
}
As mentioned earlier, first names and last names will be derived differently depending on the source. So we complete this code inside the block handling the respective sources:
if (applicationName.toLowerCase().contains("HR")) {
try {
if (idn.getAccountAttribute(applicationName, link.getNativeIdentity(), "socialName") != null
&& !idn.getAccountAttribute(applicationName, link.getNativeIdentity(), "socialName")
.trim().equals(""))
socialName = idn
.getAccountAttribute(applicationName, link.getNativeIdentity(), "socialName")
.trim().toLowerCase();
} catch (GeneralException e) {
return e.getMessage() + " Application=" + applicationName.toLowerCase();
}
firstName = socialName.substring(0, socialName.indexOf(" ")).trim().toLowerCase();
lastName = socialName.substring(socialName.indexOf(" ")).trim().toLowerCase();
break;
} else if (applicationName.toLowerCase().contains("Contractors")) {
try {
firstName = idn.getAccountAttribute(applicationName, link.getNativeIdentity(), "firstName")
.trim().toLowerCase();
lastName = idn.getAccountAttribute(applicationName, link.getNativeIdentity(), "lastName").trim()
.toLowerCase();
} catch (GeneralException e) {
return e.getMessage() + "Application=" + applicationName.toLowerCase();
}
break;
}
After getting first name and last name, we do some normalization in order to bypass some last name prefixes:
String[] exceptions = { "de", "dos", "da", "das", "do", "von", "mac"};
for (int i = 0; i < exceptions.length; i++) {
firstName = firstName.replaceAll(" " + exceptions[i] + " ", " ");
firstName = firstName.replaceAll("^" + exceptions[i] + " ", "");
firstName = firstName.replaceAll(" " + exceptions[i] + "$", "");
lastName = lastName.replaceAll(" " + exceptions[i] + " ", " ");
lastName = lastName.replaceAll("^" + exceptions[i] + " ", "");
lastName = lastName.replaceAll(" " + exceptions[i] + "$", "");
}
Our experience with some clients is that when certain data comes from an HR source, it generally does not have separate first name and last names. So, we assume that the first word is the first name, and any other word after first space will be the last name:
String[] lastNameArray = lastName.split(" ");
String firstLastName = lastNameArray[0].trim();
String lastLastName = firstLastName;
if (lastNameArray.length > 1) {
lastLastName = lastNameArray[lastNameArray.length - 1].trim();
}
Then, having defined first and last names, we will proceed to generate all username candidate values:
List options = new ArrayList();
options.add(firstName + "." + lastName);
options.add(firstName.substring(0, 1) + "." + lastLastName);
options.add(firstName.substring(0, 2) + "." + lastLastName.substring(0, 2));
options.add(firstName.substring(0, 2) + "." + firstLastName);
options.add(firstName + "." + lastLastName.substring(0, 2));
String lastChance = "NullLastChance";
for (int i = 0; i < 15; i++) {
lastChance += firstName + "." + lastName;
options.add(lastChance);
}
Finally, we will iterate through the options array. For each entry, we will search identities with that uid
value. If 0 is returned, that means the value is free to use and will be returned. For that, we use the method idn.countIdentitiesBySearchableIdentityAttribute in this way:
String uid = "allUIDOptionsExhausted";
for (int i = 0; i < options.size(); i++) {
int count = idn.countIdentitiesBySearchableIdentityAttribute("uid", "Equals", (String) options.get(i));
if (count == 0) {
uid = (String) options.get(i);
break;
}
}
return uid;
Now that we have the BeanShell code, we must put it inside the rule definition. As previously explained, the filename must be “Rule - IdentityAttribute - GenerateUserId.xml”. Here is the complete code:
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<Rule name="GenerateUserid" type="IdentityAttribute">
<Description>Generates external username value.</Description>
<Source><![CDATA[
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Iterator;
import sailpoint.object.Application;
import sailpoint.object.Identity;
import sailpoint.object.Link;
import sailpoint.server.IdnRuleUtil;
import sailpoint.tools.GeneralException;
try {
if (identity != null && identity.getStringAttribute("uid") != null
&& !identity.getStringAttribute("uid").trim().equals(""))
return identity.getStringAttribute("uid");
String socialName = "NullSocialName";
String firstName = "NullFirstName";
String lastName = "NullLastName";
String employeeType = "NullEmpType";
List links = identity.getLinks();
if (links == null)
return "identity.getLinkst.null";
Iterator iterator = links.iterator();
if (iterator == null)
return "iterator.null";
int max = 0;
while (max < 20 && iterator.hasNext()) {
Link link = (Link) iterator.next();
String applicationName = link.getApplicationName();
Application source = link.getApplication();
if (source == null)
return "source.null";
if (idn == null)
return "idn.null";
if (applicationName.toLowerCase().contains("HR")) {
try {
if (idn.getAccountAttribute(applicationName, link.getNativeIdentity(), "socialName") != null
&& !idn.getAccountAttribute(applicationName, link.getNativeIdentity(), "socialName")
.trim().equals(""))
socialName = idn
.getAccountAttribute(applicationName, link.getNativeIdentity(), "socialName")
.trim().toLowerCase();
} catch (GeneralException e) {
return e.getMessage() + " Application=" + applicationName.toLowerCase();
}
firstName = socialName.substring(0, socialName.indexOf(" ")).trim().toLowerCase();
lastName = socialName.substring(socialName.indexOf(" ")).trim().toLowerCase();
break;
} else if (applicationName.toLowerCase().contains("Contractors")) {
try {
firstName = idn.getAccountAttribute(applicationName, link.getNativeIdentity(), "firstName")
.trim().toLowerCase();
lastName = idn.getAccountAttribute(applicationName, link.getNativeIdentity(), "lastName").trim()
.toLowerCase();
} catch (GeneralException e) {
return e.getMessage() + "Application=" + applicationName.toLowerCase();
}
break;
}
max++;
}
String[] exceptions = { "de", "dos", "da", "das", "do" };
for (int i = 0; i < exceptions.length; i++) {
firstName = firstName.replaceAll(" " + exceptions[i] + " ", " ");
firstName = firstName.replaceAll("^" + exceptions[i] + " ", "");
firstName = firstName.replaceAll(" " + exceptions[i] + "$", "");
lastName = lastName.replaceAll(" " + exceptions[i] + " ", " ");
lastName = lastName.replaceAll("^" + exceptions[i] + " ", "");
lastName = lastName.replaceAll(" " + exceptions[i] + "$", "");
}
String[] lastNameArray = lastName.split(" ");
String firstLastName = lastNameArray[0].trim();
String lastLastName = firstLastName;
if (lastNameArray.length > 1) {
lastLastName = lastNameArray[lastNameArray.length - 1].trim();
}
List options = new ArrayList();
options.add(firstName + "." + lastName);
options.add(firstName.substring(0, 1) + "." + lastLastName);
options.add(firstName.substring(0, 2) + "." + lastName.substring(0, 2));
options.add(firstName.substring(0, 2) + "." + lastName);
options.add(firstName + "." + lastLastName.substring(0, 2));
String lastChance = "NullLastChance";
for (int i = 1; i <= 15; i++) {
lastChance += firstName + "." + lastName + i;
options.add(lastChance);
}
String uid = "allUIDOptionsExhausted";
for (int i = 0; i < options.size(); i++) {
int count = idn.countIdentitiesBySearchableIdentityAttribute("uid", "Equals", (String) options.get(i));
if (count == 0) {
uid = (String) options.get(i);
break;
}
}
return uid;
} catch (Exception e) {
return "exception" + e.getMessage();
}
]]></Source>
</Rule>
VALIDATING THE RULE
Before submitting the rule to SailPoint for review, it is necessary to check for errors with the SailPoint Rule Validator:
You can download the Rule Validator and learn how to use it here.
./sp-rv -v -f 'Rule - IdentityAttribute - GenerateUserid.xml'
________________________________________________________________________________
SailPoint SaaS Rule Validator v3.0.26
By the SaaS Acceleration Team
(c)2022-23 SailPoint Technologies Inc
Command line arguments:
--verbose {-v}
--file {-f} = "Rule - IdentityAttribute - GenerateUserid.xml"
Executed from: /home/jsosa/sailpoint-saas-rule-validator-3.0.26
Jar location : /home/jsosa/sailpoint-saas-rule-validator-3.0.26
________________________________________________________________________________
File name : /home/jsosa/sailpoint-saas-rule-validator-3.0.26/Rule - IdentityAttribute - GenerateUserid.xml
Rule name : GenerateUserid
Process date : Mon Feb 10 17:05:29 GMT-03:00 2025
JAR import for directory: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/javax.json-1.1.jar
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/identityiq-idn-stub.jar
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/json.jar
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/javax.json-api-1.1.4.jar
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/httpcore-4.4.13.jar
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/commons-logging-1.2.jar
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/connector-bundle.jar
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/httpclient-4.5.13.jar
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/commons-lang-2.6.jar
JAR import for directory: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/javax.json-1.1.jar
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/identityiq-idn-stub.jar
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/json.jar
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/javax.json-api-1.1.4.jar
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/httpcore-4.4.13.jar
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/commons-logging-1.2.jar
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/connector-bundle.jar
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/httpclient-4.5.13.jar
Imported: /home/jsosa/sailpoint-saas-rule-validator-3.0.26/bsh-lib/commons-lang-2.6.jar
________________________________________________________________________________
No errors found.
Warnings: (4)
Line 33 - [LintValidatorWhileStatement(31)] While statement detected: while ( max < 20 && iterator .hasNext ( ) ) { . . "While Loops" can cause issues when looping with incorrect exit conditions. Ensure Looping exit conditions are correct.
33: while ( max < 20 && iterator .hasNext ( ) ) {
Line 77 - [LintValidatorForStatement(31)] For statement detected: for ( int i = 0 ; . . "For Loops" can cause issues when looping with incorrect exit conditions. Ensure Looping exit conditions are correct.
77: for ( int i = 0 ; i < exceptions .length ; i ++ ) {
Line 102 - [LintValidatorForStatement(31)] For statement detected: for ( int i = 0 ; . . "For Loops" can cause issues when looping with incorrect exit conditions. Ensure Looping exit conditions are correct.
102: for ( int i = 0 ; i < 15 ; i ++ ) {
Line 109 - [LintValidatorForStatement(31)] For statement detected: for ( int i = 0 ; . . "For Loops" can cause issues when looping with incorrect exit conditions. Ensure Looping exit conditions are correct.
109: for ( int i = 0 ; i < options .size ( ) ; i ++ ) {
________________________________________________________________________________
Variables:
idn == class sailpoint.server.IdnRuleUtil
environment == interface java.util.Map
log == interface org.apache.commons.logging.Log
e == class java.lang.Exception
identity == class sailpoint.object.Identity
bsh == null
context == interface sailpoint.api.SailPointContext
this == class bsh.LintBSHThis
oldValue == class java.lang.Object
Methods:
pathToFile(java.lang.String)
eval(java.lang.String)
addClassPath(Unknown)
Imported classes: {Link=sailpoint.object.Link, Application=sailpoint.object.Application, Map=java.util.Map, Interpreter=bsh.Interpreter, GeneralException=sailpoint.tools.GeneralException, Iterator=java.util.Iterator, Identity=sailpoint.object.Identity, IdnRuleUtil=sailpoint.server.IdnRuleUtil, BshClassManager=bsh.BshClassManager, URL=java.net.URL, ArrayList=java.util.ArrayList, HashMap=java.util.HashMap, EvalError=bsh.EvalError, List=java.util.List}
Imported packages: [javax.swing.event, javax.swing, java.awt.event, java.awt, java.net, java.util, java.io, java.lang, bsh]
Imported statics: null
Imported class static: null
________________________________________________________________________________
Runtime stats:
Started validation at Mon Feb 10 17:05:29 GMT-03:00 2025
1 Rules Analyzed
0 Errors
4 Warnings
Finished validation at: Mon Feb 10 17:05:30 GMT-03:00 2025
Process completed in 1 second.
________________________________________________________________________________
Validation status: SUCCESS
When we get the SUCCESS message, we can open a Customer Support case at Sailpoint Support. It can be an Advisory/Setup Services or Expert Services request and will incur a cost depending on which kind of contract is in place. We simply attach the xml file, and ask for review and deployment of the rule in our tenant.
After support has deployed the rule, it will be available to use on the Identity Profile Mappings page by selecting “Complex Data Source” as source:
RULE EXAMPLE FILE
Rule - IdentityAttribute - GenerateUserid.xml (4.0 KB)
CONSIDERATIONS
As noted in the documentation, during identity attribute calculation each identity evaluation is performed in its own thread. This means that if two users with the same name are processed at the exact same time, the rule will return the same value for both of them. It is a scenario that is very unlikely to happen, but assume it may happen. As always, fully test all solutions in a sandbox tenant before deploying to production.