Technical Artifacts to be Developed to perform Name Change in Microsoft AD

Problem

What are the Technical Artifacts we have to create in SailPoint ISC to perform Name Change in Microsoft AD?

Diagnosis

The Name Change process execution for MS AD is not a very straight forward one but requires combination of ISC artifacts to be developed to do it seamlessly. Hence, we have to create the following artifacts.

  1. Identity Attributes
  2. Transforms
  3. Before Provisioning Cloud Rule
  4. Create Profile changes in AD source
  5. Attribute Sync Enablement in AD source.

The detailed solution will be followed in the Solution section of this article.

Solution

Introduction

In the world of Identity Governance, a “simple” name change is rarely simple. When a user has a legal name change in your HR system, the ripple effect across Active Directory—affecting Distinguished Names (DN), UPNs, and Common Names—can often lead to broken profiles or orphaned accounts if not handled with precision.

Refer to this post to understand that which all SailPoint ISC artifacts must be developed/created to make it work. Note that this is not the only approach to achieve the Name Changes in AD but one of them.

Scenario Details

1. We have an HRMS Authoritative Source which is integrated with SailPoint ISC using Generic JDBC Connector. The Name of source is :: JDBC-HRMS

2. Then, we have Active Directory integrated with SailPoint ISC using OOTB connector of “MS Active Directory”. The name of source is :: Active Directory

3. Refer the below block diagram to understand the setup.

  1. In JDBC based HRMS source, the Name of Account attribute which stores “Legal Name” of the User is “Name”.
  2. In AD source, if the name change happens in HRMS source, then, we must change the below attributes in AD.
  • a. CN

    b. Mail

    c. UPN

    d. SamAccountName

    e. First Name

    f. Last Name

  • proxyAddresses Change

  1. extensionAttribute2 (which stores the Legal of the user extracted from “Name” attribute of JDBC HRMS source. Note that you can use any other extension Attribute or custom attribute in AD to store the Legal Name details).

The way the AD attributes are generated are as per below table.

Development Steps

Create an Identity Attribute which Maintains History of Identity Names

1. The SailPoint ISC must know what was the Old Full Name and the new Full Name. Hence, to do that, you need to create a separate identity attribute.

2. The Name of the identity attribute is : Legal Name History

3. The identity attribute JSON is as follows.

{
    "sources": [
        {
            "type": "rule",
            "properties": {
                "ruleType": "IdentityAttribute",
                "ruleName": "Cloud Promote Identity Attribute"
            }
        }
    ],
    "name": "legalNameHistory",
    "displayName": "Legal Name History",
    "standard": false,
    "type": "string",
    "multi": false,
    "searchable": false,
    "system": false
}

  1. This attribute will store a value populated using TRANSFORM using format of :: OldLegalName<>NewLegalName. For example: If Old Legal Name of user is “Rohit Wekhande” and new changed legal name of user is “Rohit Smith”, then, value in Legal Name History attribute will be saved post manipulation through transform as :: Rohit Wekhande<>Rohit Smith

Create an Identity Attribute which will act as Flag to allow Name change or not.

1. The SailPoint ISC must know whether we must allow the name change in AD. This identity attribute will act as flag.

2. The Name of the identity attribute is : Allow Name Change

3. The identity attribute JSON is as follows.

{
    "sources": [
        {
            "type": "rule",
            "properties": {
                "ruleType": "IdentityAttribute",
                "ruleName": "Cloud Promote Identity Attribute"
            }
        }
    ],
    "name": "allowNameChange",
    "displayName": "Allow Name Change",
    "standard": false,
    "type": "string",
    "multi": false,
    "searchable": false,
    "system": false
}

Create a Transform for Legal Name History attribute for storing data in OldLegalName<>NewLegalName format.

1. The Transform details are.

{
    "name": "JDBCHRMSSource - Static - LegalNameHistory",
    "type": "static",
    "attributes": {
        "currentAValue": {
            "attributes": {
                "input": {
                    "attributes": {
                        "input": {
                            "attributes": {
                                "expression": "$oldv eq -1",
                                "positiveCondition": {
                                    "attributes": {
                                        "values": [
                                            "none",
                                            "<<ChangedTo>>",
                                            {
                                                "attributes": {
                                                    "values": [
                                                        "$oldValue",
                                                        {
                                                            "type": "static",
                                                            "attributes": {
                                                                "value": {
                                                                    "attributes": {
                                                                        "id": "JDBCHRMSSource - FirstValid - LegalName"
                                                                    },
                                                                    "type": "reference"
                                                                }
                                                            }
                                                        }
                                                    ]
                                                },
                                                "type": "firstValid"
                                            }
                                        ]
                                    },
                                    "type": "concat"
                                },
                                "negativeCondition": {
                                    "attributes": {
                                        "values": [
                                            "$oldValue",
                                            {
                                                "type": "static",
                                                "attributes": {
                                                    "value": {
                                                        "attributes": {
                                                            "id": "JDBCHRMSSource - FirstValid - LegalName"
                                                        },
                                                        "type": "reference"
                                                    }
                                                }
                                            }
                                        ]
                                    },
                                    "type": "firstValid"
                                },
                                "oldv": {
                                    "attributes": {
                                        "input": {
                                            "attributes": {
                                                "values": [
                                                    "$oldValue",
                                                    {
                                                        "type": "static",
                                                        "attributes": {
                                                            "value": {
                                                                "attributes": {
                                                                    "id": "JDBCHRMSSource - FirstValid - LegalName"
                                                                },
                                                                "type": "reference"
                                                            }
                                                        }
                                                    }
                                                ]
                                            },
                                            "type": "firstValid"
                                        },
                                        "substring": "<<ChangedTo>>"
                                    },
                                    "type": "indexOf"
                                }
                            },
                            "type": "conditional"
                        },
                        "delimiter": "<<ChangedTo>>",
                        "index": 1,
                        "throws": false
                    },
                    "type": "split"
                }
            },
            "type": "trim"
        },
        "defaultoldValue": {
            "attributes": {
                "input": {
                    "attributes": {
                        "values": [
                            "$oldValue",
                            {
                                "type": "static",
                                "attributes": {
                                    "value": {
                                        "attributes": {
                                            "id": "JDBCHRMSSource - FirstValid - LegalName"
                                        },
                                        "type": "reference"
                                    }
                                }
                            }
                        ]
                    },
                    "type": "firstValid"
                }
            },
            "type": "trim"
        },
        "newAValue": {
            "attributes": {
                "input": {
                    "type": "static",
                    "attributes": {
                        "value": {
                            "attributes": {
                                "id": "JDBCHRMSSource - FirstValid - LegalName"
                            },
                            "type": "reference"
                        }
                    }
                }
            },
            "type": "trim"
        },
        "value": "#if(!$newAValue.equalsIgnoreCase($currentAValue))$currentAValue<<ChangedTo>>$newAValue#{else}$defaultoldValue#end"
    },
    "internal": false
}

  1. Also, note that we have “Legal Name” identity attribute as well created in our ISC setup which stores the value from “Name” account attribute of JDBC HRMS Source. The transform which us used against “Legal Name” identity attribute.
{
    "name": "JDBCHRMSSource - FirstValid - LegalName",
    "type": "firstValid",
    "attributes": {
        "values": [
            {
                "type": "decomposeDiacriticalMarks",
                "attributes": {
                    "input": {
                        "attributes": {
                            "sourceName": "JDBC-HRMS",
                            "attributeName": "NAME"
                        },
                        "type": "accountAttribute"
                    }
                }
            }
        ],
        "ignoreErrors": true
    },
    "internal": false
}

Create a Transform for Allow Name Change attribute for storing the flag

1. Create the transform as below.

{
    "name": "JDBCHRMSSource - Static - AllowNameChange",
    "type": "static",
    "attributes": {
        "value": "true"
    },
    "internal": false
}

Map the Legal Name identity attribute to extensionAttribute2 in AD source

1. To trigger the Modify Account task with AD so that Name Change process could be executed in AD, you need to map “Legal Name” identity attribute with “extensionAttribute2” account attribute.

2. Refer the below screenshot.

Create a Non-Indexed Search attribute in SailPoint ISC for SamAccountName which will be used by our Before Provisioning Cloud Rule for Uniqueness Check

1. Create a Non-Indexed Search attribute in SailPoint ISC which will be used by your Before Provisioning Cloud Rule for performing uniqueness check for your SamAccountName so that its unique during Name Change process.

  1. The details of Search attribute is.
{
    "name": "SamAccountName",
    "displayName": "SamAccountName",
    "applicationAttributes": {
        "<GUID of AD Source>": "sAMAccountName",
        "<GUID of JDBC HRMS Source>": "USERID",
    }
}

Create a Non-Indexed Search attribute in SailPoint ISC for AD CN which will be used by our Before Provisioning Cloud Rule for Uniqueness Check

1. Create a Non-Indexed Search attribute in SailPoint ISC which will be used by your Before Provisioning Cloud Rule for performing uniqueness check for your CN so that its unique during Name Change process.

2. The details of Search attribute is.

{
    "name": "ActiveDirectoryCN",
    "displayName": "ActiveDirectoryCN",
    "applicationAttributes": {
        "<GUID of AD Source>": "cn"
    }
}

Develop a Before Provisioning Cloud Rule which handles “Name Change” process.

1. The before provisioning rule is as follows.

import sailpoint.api.SailPointContext;
import sailpoint.object.ProvisioningPlan;
import sailpoint.object.ProvisioningPlan.Operation;
import sailpoint.object.ProvisioningPlan.AccountRequest;
import sailpoint.object.ProvisioningPlan.AttributeRequest;
import sailpoint.object.QueryOptions;
import sailpoint.object.Identity;
import sailpoint.connector.Connector;
import sailpoint.object.Application;
import sailpoint.object.*;
import sailpoint.api.IdentityService;
import sailpoint.tools.Util;
import sailpoint.object.Link;
import org.apache.commons.lang.StringUtils;
  
import sailpoint.tools.GeneralException;
import java.util.Iterator;
import java.util.ArrayList;
import sailpoint.api.*;
import java.util.List;
import java.util.regex.Pattern;	

import java.util.Date;
import java.text.SimpleDateFormat;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
	
import java.util.Arrays;
import java.lang.String;
import java.text.Normalizer;


import sailpoint.object.Identity;
import sailpoint.tools.Util;
import java.text.Normalizer;
import sailpoint.api.SailPointContext;
import org.apache.commons.lang.StringUtils;
import sailpoint.object.Application;
import java.util.*;



	int maxIteration = 1000;
	
// ===== Username Generator for ISC (BeanShell) =====
int MIN_LENGTH = 3;
int MAX_LENGTH = 8;

// ---------- Uniqueness check (your logic as-is) ----------
public boolean isUniqueSamAccountName(String checkSamAccountName) {
  int numberFound = 0;
  List SOURCE_IDS = new ArrayList(Arrays.asList(new String[]{
      "<GUID of AD Source>",
      "<GUID of JDBC HRMS Source>"
  }));
  String PROMOTED_ATTR_NAME = "SamAccountName";
  String SEARCH_OP = "Equals";
  List SEARCH_VALUES = new ArrayList(Arrays.asList(new String[]{checkSamAccountName}));
  numberFound = idn.attrSearchCountAccounts(SOURCE_IDS, PROMOTED_ATTR_NAME, SEARCH_OP, SEARCH_VALUES);
  return (numberFound == 0);
}

// ---------- Helpers ----------
String normalizeName(String in) {
  if (in == null) return "";
  String s = in.toLowerCase();
  try {
    s = java.text.Normalizer.normalize(s, java.text.Normalizer.Form.NFD).replaceAll("\\p{M}+","");
  } catch (Throwable t) { /* ignore if Normalizer unavailable */ }
  s = s.replaceAll("[^a-z0-9]", "");
  return s;
}

// Compose base (prefix + last), ONLY trim to MAX; DO NOT pad here
String composeBase(String prefix, String last) {
  String v = prefix + last;
  if (v.length() > MAX_LENGTH) v = v.substring(0, MAX_LENGTH);
  return v;
}

// Append numeric suffix; ALWAYS reserve space for the suffix within MAX_LENGTH
// Example (MAX=8): "vboddimu" + "1" => trim base to 7 => "vboddim1"
String appendNumberTrim(String base, int n) {
  String suffix = String.valueOf(n);

  // Reserve space for suffix
  int room = MAX_LENGTH - suffix.length();
  if (room < 0) room = 0;

  // Trim base first to make space for suffix
  String trimmedBase = (base != null && base.length() > room) ? base.substring(0, room) : (base == null ? "" : base);

  // Build candidate
  String v = trimmedBase + suffix;

  // Ensure MIN_LENGTH (pad with digits if needed)
  if (v.length() < MIN_LENGTH) {
    StringBuilder sb = new StringBuilder(v);
    int d = 1;
    while (sb.length() < MIN_LENGTH) {
      sb.append(d);
      d = (d == 9) ? 1 : d + 1;
    }
    v = sb.toString();
  }

  // Safety: enforce MAX_LENGTH even after padding
  if (v.length() > MAX_LENGTH) {
    v = v.substring(0, MAX_LENGTH);
  }

  return v;
}

// Build first-name prefixes in order: 1st initial, first 2 chars, ..., full
List buildFirstPrefixes(String first) {
  List prefixes = new ArrayList();
  if (first.length() == 0) { prefixes.add(""); return prefixes; }
  prefixes.add(first.substring(0,1));
  for (int i = 2; i <= first.length(); i++) {
    prefixes.add(first.substring(0,i));
  }
  return prefixes;
}

/**
 * Rules:
 *  A) With last name:
 *     - Pass 0: bases < MIN → immediately try 1..9 (e.g., "ky" -> "ky1").
 *     - Pass 1: try all plain bases ≥ MIN ("jsmith", "josmith", "johsmith", "johnsmith").
 *     - Pass 2: if all plain are taken, try 1..9 for each base ≥ MIN.
 *  B) No last name: base = full first name → try plain, then 1..9.
 *  C) Both names empty → return null.
 */
String generateSamAccountName(String rawFirst, String rawLast) {
  String f = normalizeName(rawFirst);
  String l = normalizeName(rawLast);

  // If both empty, return null
  if (f.length() == 0 && l.length() == 0) {
    return null;
  }

  // ---- Scenario B: No last name ----
  if (l.length() == 0) {
    String base = (f.length() > MAX_LENGTH) ? f.substring(0, MAX_LENGTH) : f;

    if (base.length() >= MIN_LENGTH && isUniqueSamAccountName(base)) return base;

    for (int n = 1; n <= 9; n++) {
      String cand = appendNumberTrim(base, n);
      if (isUniqueSamAccountName(cand)) return cand;
    }

    return base;
  }

  // ---- Scenario A: With last name ----
  List bases = new ArrayList();
  for (Object preObj : buildFirstPrefixes(f)) {
    String pre = (String) preObj;
    bases.add(composeBase(pre, l));
  }

  // Pass 0: handle short bases first ("ky" -> "ky1..9")
  for (Object bObj : bases) {
    String base = (String) bObj;
    if (base.length() < MIN_LENGTH) {
      for (int n = 1; n <= 9; n++) {
        String cand = appendNumberTrim(base, n);
        if (isUniqueSamAccountName(cand)) return cand;
      }
    }
  }

  // Pass 1: try all plain bases (>= MIN)
  for (Object bObj : bases) {
    String cand = (String) bObj;
    if (cand.length() >= MIN_LENGTH) {
      if (isUniqueSamAccountName(cand)) return cand;
    }
  }

  // Pass 2: try numbered variants (>= MIN)
  for (Object bObj : bases) {
    String base = (String) bObj;
    if (base.length() >= MIN_LENGTH) {
      for (int n = 1; n <= 9; n++) {
        String cand = appendNumberTrim(base, n);
        if (isUniqueSamAccountName(cand)) return cand;
      }
    }
  }

  return (String) bases.get(0);
}
	
public AttributeRequest newAttributeRequest(String attributeName, Object attributeValue) {
	AttributeRequest attributeRequest = new ProvisioningPlan.AttributeRequest();
	attributeRequest.setName(attributeName);
	attributeRequest.setOperation(ProvisioningPlan.Operation.Set);
	attributeRequest.setValue(attributeValue);
	        
	return attributeRequest;
}


public String normalizeInput(String inputString) {
    //Decompose and remove diacritical marks, spaces, special characters and symbols
    if(inputString == null || inputString.isEmpty()) return "";
    inputString = Normalizer.normalize(inputString, Normalizer.Form.NFD);
    inputString = inputString.replaceAll("[^a-zA-Z0-9]","");
    return inputString;
  }
  
public boolean isdisplayNameChanged (String displayNameHistory) {

		boolean isdisplayNameChanged = false;

        // Check if the input contains the delimiter
        if (displayNameHistory.contains("<<ChangedTo>>")) {
            // Split the input string using "<<ChangedTo>>" as the delimiter
            String[] values = displayNameHistory.split("<<ChangedTo>>");

            // Ensure that there is exactly one delimiter
            if (values.length == 2) {
                String value1 = values[0];
                String value2 = values[1];

                // Perform the comparison
                if (value1.equalsIgnoreCase(value2)) {
                    isdisplayNameChanged = false;
                } else {
                    isdisplayNameChanged = true;
                }
            } else {
                isdisplayNameChanged = false;
            }
        } else {
            isdisplayNameChanged = false;
        }
		
		return isdisplayNameChanged;
    }


 public boolean isADCNUniqueValue(String attribute)
  {
    int numberFound = 0;
    List SOURCE_IDS = new ArrayList(Arrays.asList(new String[]{"<GUID of AD Source>"})); 
    String PROMOTED_ATTR_NAME = "ActiveDirectoryCN";
    String SEARCH_OP = "Equals"; 
    List SEARCH_VALUES = new ArrayList(Arrays.asList(new String[]{attribute}));
    numberFound = idn.attrSearchCountAccounts(SOURCE_IDS, PROMOTED_ATTR_NAME, SEARCH_OP, SEARCH_VALUES);

    if(numberFound == 0)
    {
      return true;
    }

    else 
    {
      return false;
    }
  }
  
  public String generateAdCn(String displayName)
  {
    String commonName = null;
    int iterator = 0;
    int counter = 1;

    if((displayName == null) && (displayName == null))
    {
      return null;
    }

    if((displayName != null) && (!"".equals(displayName)))
    {
      do
      {
        switch (iterator)
        {
          case 0:
          commonName = displayName;
          break;
          default:
          commonName = displayName + counter;
          counter++;
          break;					
        }
        iterator++;

      } while(!(isADCNUniqueValue(commonName)) && (iterator < 20));
      // Don't let the it loop forever if something goes wrong

      if((iterator >= 20) || (null == commonName) || ("").equals(commonName))
      {
        return null;
      }
    }
    return commonName;
  }

   
  if(plan != null) 
	  { 
	  String requester="Sailpoint";
	  boolean isNameChange = false;
		List accountRequests = plan.getAccountRequests();
		Identity identity = plan.getIdentity();
        List requesters = plan.getRequesters();
			 if (requesters != null && requesters.size() > 0) {
	                Identity req = requesters.get(0);
	                if (req != null)
	                    requester = req.getName();
	            }
		if(accountRequests != null) 
			{
			  for (AccountRequest accountRequest : accountRequests) 
				  {
					AccountRequest.Operation op = accountRequest.getOperation();
					String allownamechange = identity.getAttribute("allowNameChange");
					String nativeIdentity = accountRequest.getNativeIdentity();
					String LCS = identity.getAttribute("cloudLifecycleState");
					String orgUnit = identity.getAttribute("ou");
					String adDomain = identity.getAttribute("adDomain");
					String existingProxyAddresses = identity.getAttribute("secondaryEmail");
					List proxyAddressList = null;
					
					
					if (op != null) 
					{	

						//Name Change Code when either FirstName or LastName or Both of a User changes. This will lead to new CN and new SamAccountName generation					
						
						if(accountRequest.getOperation().equals(AccountRequest.Operation.Modify) && "active".equalsIgnoreCase(LCS))
							 {
									List attributeRequests = accountRequest.getAttributeRequests();
									for (AttributeRequest attributeRequest : attributeRequests) {
											if (Util.isNotNullOrEmpty(attributeRequest.getName()) && attributeRequest.getName().equalsIgnoreCase("extensionAttribute2")) {
												isNameChange = true;
											}
										}
								
								if(isNameChange)
									{		
										if("true".equalsIgnoreCase(allownamechange))		
											{						
										
										if(Util.isNotNullOrEmpty(identity.getAttribute("legalNameHistory")))
												  {
											
											if(isdisplayNameChanged(identity.getAttribute("legalNameHistory")))
													{
							
															String displayName = null;
															
															String firstName = identity.getAttribute("firstname");
															String lastName = identity.getAttribute("lastname");
															String legalName = identity.getAttribute("legalName");
															String oldEmail = identity.getAttribute("email");
															
																																								
															String new_CN = null;
															String finalCommonName = null;
															String new_SamAccountName = null;
															String finalSamAccountName = null;
															String finalUPN = null;
															String finalMail = null;
															
															 
															
															finalSamAccountName = generateSamAccountName(firstName,lastName);
															new_SamAccountName = finalSamAccountName;
															
															
															accountRequest.add(new AttributeRequest("sAMAccountName",ProvisioningPlan.Operation.Set,new_SamAccountName));
															
															finalMail = new_SamAccountName + adDomain;
															
															String removeProxyAddress = null;
															String secondaryProxyAddress = null;
															
															removeProxyAddress = "SMTP:"+oldEmail;
															secondaryProxyAddress = "smtp:"+oldEmail;
															
															proxyAddressList = new ArrayList();
															proxyAddressList.add("SMTP:"+finalMail);
															proxyAddressList.add("smtp:"+oldEmail);
															accountRequest.add(new AttributeRequest("proxyAddresses",ProvisioningPlan.Operation.Add,proxyAddressList));
															accountRequest.add(new AttributeRequest("proxyAddresses",ProvisioningPlan.Operation.Remove,removeProxyAddress));
															accountRequest.add(new AttributeRequest("proxyAddresses",ProvisioningPlan.Operation.Add,secondaryProxyAddress));
															
															
															finalUPN = new_SamAccountName + adDomain;
															accountRequest.add(new AttributeRequest("userPrincipalName",ProvisioningPlan.Operation.Set,finalUPN));
															
															
															accountRequest.add(new AttributeRequest("mail",ProvisioningPlan.Operation.Set,finalMail));
															
													
																	
															displayName = firstName + " " + lastName;
															finalCommonName = generateAdCn(displayName);
															new_CN = "CN=" + finalCommonName;
															accountRequest.add(new AttributeRequest("AC_NewName",ProvisioningPlan.Operation.Set,new_CN));
															
													}
												}
											}								  
									}
							}
						
						
						  
								
				  }
			}
	  }
	  }

  1. Once the Before Provisioning rule is developed, make sure you are validating the before provisioning rule against Rule Validator and then, submitting a case with SailPoint to get it deployed.
  2. Once deployed, you need to attach the before provisioning rule against AD source.

Enable the attribute sync for FirstName, LastName and ExtensionAttribute2

1. In the AD source, make that attribute sync is turned on for “extsnionAttribute2” AD account attribute so that before provisioning cloud rule can intercept that and perform the require name change pre-requisites.

2. Refer the below screenshot.

image

Conclusion.

1. When the Legal Name Change of the user is performed in HRMS source, the data gets aggregated from HRMS source to ISC.

2. The ISC will perform the change in “Legal Name” identity attribute and “Legal Name History” identity attribute.

3. Once change in “Legal Name” identity attribute is performed, the attribute sync will be triggered in AD against extensionAttribute2.

4. Before Provisioning Cloud Rule will intercept the Modify Account request against “extensionAttribute2”. The before provisioning rule will perform the changes for below attributes as per the policy.

thanks a lot rohit but we don’t have AD instead we have Entra id then what will be ther process?

Hello @Rakesh_Singh_1234 ,

It will be same process there as well.

Instead of Before Provisioning Rule for AD, you need to create the Before Provisioning Rule for ENTRA and attach it to ENTRA Source. The logic will remain the same but Before Prov rule has to be attached to ENTRA source instead of AD.

Also, instead of extensionAttribute2 (as its AD specific I think), use any other custom Entra ID attribute in its place. Once you go through my articles, you will know in which place I am talking about.

Rest all will remain the same.