How to Search Non-Indexed Account Attributes in IDN Rules for Uniqueness
This article will show how to use the recently released feature on creating indexed attributes which can be referenced in rules to do uniqueness searches and generate attributes like sAMAccountName / email / userPrincipalName etc.
It allows you to search accounts across sources to determine if a specific attribute value is already in use in those sources and help generate a new unique value.
Lets take a use case and a walkthrough on how you can set this up
Use Case
We want to generate a new email address which must have a unique prefix (firstname.lastname@) by checking against the “mail”, “userPrincipalName”, “proxyAddresses” attributes across 3 x AD connectors.
Note: Sources don’t have to be AD explicitly and can be virtually any source (AAD, ServiceNow, Okta, Workday etc)
Design
High Level Steps are
- Identify Source ID and attributes
- Create Searchable Attributes
- Do an unoptimised aggregation if source already exists (like production tenant) to populate these searchable attributes.
- Use new methods in rules to search for uniqueness
Identify Source ID and Attributes
Now we have 3 x AD source in our design. For each of them we need to get their sourceID. You can fetch them with an API call
GET {{api-url}}/cc/api/source/get/{{source-id}}
Where
- {{api-url}}: This is your tenant URL in form of https://tenantName.api.identitynow.com
- {{source-id}}: This is the number you see in your browser URL when visiting the source via UI
In the response you will get an externalId with a value like 2c9180867745f3b10177469563be7451d
Gather the externalId for all three sources you want to build it for.
Create Searchable Attributes
Now when you have all the SourceID’s we need to map and create searchable attributes. We will design 3 attributes (one for each – mail, userPrincipalName, proxyAddresses). It takes care of multivalued attributes as well (like proxyAddresses). The below table explains the design
Search Attribute Name | Account Attribute | Source ID | Source Name |
---|---|---|---|
allMailAddresses | 2c9180867745f3b10177469563be7451d | AD Source 1 | |
2c9180867745f3b10177469563be7451e | AD Source 2 | ||
2c9180867745f3b10177469563be7451f | AD Source 3 | ||
allProxyAddresses | proxyAddresses | 2c9180867745f3b10177469563be7451d | AD Source 1 |
2c9180867745f3b10177469563be7451e | AD Source 2 | ||
2c9180867745f3b10177469563be7451f | AD Source 3 | ||
allUserPrincipalNames | userPrincipalName | 2c9180867745f3b10177469563be7451d | AD Source 1 |
2c9180867745f3b10177469563be7451e | AD Source 2 | ||
2c9180867745f3b10177469563be7451f | AD Source 3 |
Create each of the attribute via Create Search Attribute Config API
POST {{api-url}}/beta/accounts/search-attribute-config/
BODY
{
"name": "allMailAddresses",
"displayName": "All Mail Attributes",
"applicationAttributes": {
"2c9180867745f3b10177469563be7451d": "mail",
"2c9180867745f3b10177469563be7451e": "mail",
"2c9180867745f3b10177469563be7451f": "mail"
}
}
Similarly do it for other 2 attributes as well
POST {{api-url}}/beta/accounts/search-attribute-config/
BODY
{
"name": "allProxyAddresses",
"displayName": "All Proxy Addresses Attributes",
"applicationAttributes": {
"2c9180867745f3b10177469563be7451d": "proxyAddresses",
"2c9180867745f3b10177469563be7451e": "proxyAddresses",
"2c9180867745f3b10177469563be7451f": "proxyAddresses"
}
}
POST {{api-url}}/beta/accounts/search-attribute-config/
BODY
{
"name": "allUserPrincipalNames",
"displayName": "All Mail Attributes",
"applicationAttributes": {
"2c9180867745f3b10177469563be7451d": "userPrincipalName",
"2c9180867745f3b10177469563be7451e": "userPrincipalName",
"2c9180867745f3b10177469563be7451f": "userPrincipalName"
}
}
Once all are created you can do a GET call to check all attributes
GET {{api-url}}/beta/accounts/search-attribute-config/
You should see all the above listed in the response.
Populate Searchable Attributes
Now if these are existing sources with accounts in them, simply do a once off unoptimized aggregation of each source via the following API call.
POST {{api-url}}/cc/api/source/loadAccounts/{{source-id}}
BODY form-data
KEY: disableOptimization
VALUE: true
- Once done for all sources, the search attributes get populated in the backend.
- Currently you can’t check them with the UI or API calls.
- Any new accounts which get created after this or come as aggregation (delta or full) will automatically keep updating the search attribute.
- New account created is populated immediately. So if you are creating multiple users in concurrent – uniqueness check will still capture the previous value calculated – no need to wait for aggregation.
Use New Methods in the Rules
Our IDNRuleUtil guide has been updated with few new methods. Two in particular which use these attributes are
attrSearchCountAccounts(): This will be helpful to use for uniqueness search
attrSearchGetIdentityName(): This will be helpful in say a correlation rule.
The link above has a bit more technical in-depth on what parameters are required by these methods and what is the return. But I will show you how to use the attrSerachCountAccounts() method in an example of uniqueness search.
AttributeGenerator Rule
import sailpoint.object.Identity;
import org.apache.commons.lang.StringUtils;
List proxyAddressSources = new ArrayList(Arrays.asList(new String[] {
"2c9180867745f3b10177469563be7451d",
"2c9180867745f3b10177469563be7451e",
"2c9180867745f3b10177469563be7451f"
}));
List upnSources = new ArrayList(Arrays.asList(new String[] {
"2c9180867745f3b10177469563be7451d",
"2c9180867745f3b10177469563be7451e",
"2c9180867745f3b10177469563be7451f"
}));
List mailSources = new ArrayList(Arrays.asList(new String[] {
"2c9180867745f3b10177469563be7451d",
"2c9180867745f3b10177469563be7451e",
"2c9180867745f3b10177469563be7451f"
}));
public String generateUniqueEmail(String fName, String lName, int iteration) throws Exception {
if (iteration > 99) {
throw new Exception("emailPrefix counter limit 99!");
}
switch ( iteration ) {
case 0:
String emailPrefix = fName + "." + lName;
break;
default:
String emailPrefix = fName + "." + lName + String.valueOf(iteration)
break;
}
if (isUnique(emailPrefix)) {
return emailPrefix;
} else {
return generateUniqueEmail(fName, lName, iteration + 1);
}
}
public boolean isUnique(String emailPrefix) {
String startWithOp = "StartsWith";
boolean isUnique = true;
List searchValues = new ArrayList(Arrays.asList(new String[] {
"smtp:" + emailPrefix + "@", "sip:" + emailPrefix + "@"
}));
// check proxy addresses
if (idn.attrSearchCountAccounts(proxyAddressSources, "allProxyAddresses", startWithOp, searchValues) == 0) {
// check UPNs
searchValues = new ArrayList(Arrays.asList(new String[] {
emailPrefix + "@"
}));
if (idn.attrSearchCountAccounts(upnSources, "allUserPrincipalNames", startWithOp, searchValues) == 0) {
// check mails
if (idn.attrSearchCountAccounts(mailSources, "allMailAddresses", startWithOp, searchValues) > 0) {
isUnique = false;
}
} else {
isUnique = false;
}
} else {
isUnique = false;
}
return isUnique;
}
String generatedUniqueEmail = null;
if (identity != null) {
String emailSuffix = StringUtils.trimToNull(identity.getAttribute("emailSuffix"));
String fname = StringUtils.trimToNull(identity.getAttribute("firstname"));
String lname = StringUtils.trimToNull(identity.getAttribute("lastname"));
if (fname != null && lname != null && emailSuffix != null) {
fname = fname.replaceAll("[^a-zA-Z0-9]", "");
fname = fname.toLowerCase();
lname = lname.replaceAll("[^a-zA-Z0-9]", "");
lname = lname.toLowerCase();
generatedUniqueEmail = generateUniqueEmail(fname, lname, 0) + emailSuffix;
}
}
return generatedUniqueEmail;
- Create a list of 3 attributes with sourceID in them. This is required to pass the list to the new method.
- Get the firstName and lastName from identity attribute, sanitize it and pass it to generateUniqueEmail() method
- emailSuffix is also brought from identity attribute. This logic is already done via Transforms on what user email domain or suffix is suppose to be
- In generateUniqueEmail() create a emailPrefix attribute and call isUnique() method.
- isUnique() logic
- We are using StartsWith option (there is Equals also available). Reason being all the data coming from AD will be in some form of “firstname.lastname@suffix.com, firstname.lastname@suffix2.com”. We want to match “firstname.lastname@” only as we don’t care about exact match or equals match. Remember both are case-insensitive checks.
- First check with proxyAddressSources in the idn.attrSearchCountAccounts() call
- Here we have appended SMTP / SIP to do a startsWith check as we know that these are the known values we need to check in proxy address. We don’t have a contains. Also since its case-insensitive we don’t need to worry about camel casing or other such things. In real world, data normally looks like “smtp:firstname.lastname@domain.com” , “SIP:firstname.lastname@domain2.com”.
- There are other such prefix found if proxyAddress attribute which we didn’t care about check but if your use case does, just add them and search as above.
- If return == 0 means nothing is found, thus unique and move on to next check
- Else isUnique == false and will return that value
- Else check with upnSources source. Same logic as above
- Else check with mailSources source. Same logic as above
- If no account found here, isUnique is set to true and thus isUnique() has passed and the new email address is generated
- If still a value is found, isUnique is set to false and thus isUnique() failed and new iteration starts
- Max Iteration is set to 99 and then fail
Use case
Rehire an account who comes back after 5 years with the same AD account.
Design
Now the account could be sitting as an uncorrelated account in IDN and we want to resurrect it. We can only search against the accountID or accountName historically and say if the correlating factor is employeeID which is not a part of either of these attributes then we couldn't do it.
Correlation Rule
import sailpoint.object.*;
import java.util.*;
import org.apache.commons.lang.StringUtils;
List adSource = new ArrayList(Arrays.asList(new String[] {
"2c9180867745f3b10177469563be7451d"
}));
String employeeNumberSearchAttribute = "allADEmployeeNumbers";
Map returnMap = new HashMap();
String employeeNumber = StringUtils.trimToNull(account.getStringAttribute("employeeNumber"));
if (employeeNumber != null) {
String identityName = null;
List searchValues = new ArrayList(Arrays.asList(new String[] {
employeeNumber
}));
identityName = idn.attrSearchGetIdentityName(adSource, employeeNumberSearchAttribute, "Equals", searchValues);
if (identityName != null) {
returnMap.put("identityAttributeName", "name");
returnMap.put("identityAttributeValue", identityName);
} else {}
}
return returnMap;
- We create an attribute like above called “allADEmployeeNumbers” and populate that with AD – employeeID
- We do an unoptimised aggregation to populate the search attribute
- On an aggregation of an authoritative source, we have a correlation rule attached
- It gets the “employeeNumber” coming in from the source
- Calls attrSearchGetIdentityName() to find that employeeNumber in the searchable attribute.
- If found it returns an IdentityName of the cube and this can be used to correlate
- If not found it returns an empty returnMap i.e. creates a new identity.
- It should take care of the following scenarios
- It should return null if it found multiple names or no names.
- It should return one identityName even if multiple links were found but single identityName (i.e. say if you had multiple employeeNumbers in links but all are attached to one cube). Like in a daisy chain scenario.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Content to Moderator
Hello,
I think something is missing on the increment part.
With this, the increment is never used :
public String generateUniqueEmail(String fName, String lName, int iteration) throws Exception {
if (iteration > 99) {
throw new Exception("emailPrefix counter limit 99!");
}
String emailPrefix = fName + "." + lName;
if (isUnique(emailPrefix)) {
return emailPrefix;
} else {
return generateUniqueEmail(fName, lName, iteration + 1);
}
}
Maybe you should update with adding a switch between :
throw new Exception("emailPrefix counter limit 99!");
}
and
if (isUnique(emailPrefix)) {
Which give you someting like that :
throw new Exception("emailPrefix counter limit 99!");
}
switch ( iteration ) {
case 0:
String emailPrefix = fName + "." + lName;
break;
default:
String emailPrefix = fName + "." + lName + String.valueOf(iteration)
break;
}
if (isUnique(emailPrefix)) {
Please check and update or tell me if i'm wrong.
Best regards.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Content to Moderator
@pedrolit0 I feel like you are right. But I am saying just by looking at the code and not really running it. The iteration needs to run with an append of the new iteration count.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Content to Moderator
@pedrolit0 you are right. I had removed some specific logic from code to use as example here which caused the issue. Updated with your code. Thanks for pointing it out.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Content to Moderator
@piyush_khandelwal I just need to generate a unique id between 2000 and 5000, I was hoping there will be an attributGenerator OOB for that. If you know of one, can you please help.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Content to Moderator
Hello,
Is this possible to use on a WebServiceBeforeOperationRule?
I would like to create an email for the user in the target and validate that it is unique.
I'm getting:
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Content to Moderator
Instead of
POST {{api-url}}/cc/api/source/loadAccounts/{{source-id}}
use https://developer.sailpoint.com/docs/api/beta/import-accounts/
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Content to Moderator
@piyush_khandelwal can attrSearchCountAccounts handle proxyaddresses if you don't put them in a searchable attribute?