AttributeGenerator for UserPrincipalName

Hello all,

I’m trying do calculate 3 fields for our AD connector

  • sAMAccountName
  • distinguishedName
  • userPrincipalName

For the sAMAccountName and distinguishedName I have created 2 AttributeGenerator cloud rules.
This is working fine.

Now my need is to calculate the userPrincipalName. Our business need is to calculate it with 3 inputs

  • upnAlignedWithEmail (a field retrieved from the identity)
  • email (the email field from the identity)
  • samAccountName

Globally the need is: if upnAlignedWithEmail = true then the UPN must be equals tu the email address, if not it must be equals to the [email protected]

Do someone know how can I achieve that?
I don’t want to duplicate the code to generate the sAMAccountName in the userPrincipalName rule.

Thanks

Do you have any code for this rule yet? Are you able to share it?

Sure,

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<Rule name="DistinguishedNameGenerator" type="AttributeGenerator">
    <Description>
        The "DistinguishedNameGenerator" rule generates a unique Distinguished Name (DN) for a user based on their first name, last name, and organizational unit (OU) path. It retrieves the base DN from application settings, considering regional variations. The rule ensures uniqueness by checking for existing records and retrying if necessary. If an account is disabled, it places the user in the "DISABLED ACCOUNTS" OU. The DN is constructed dynamically, with retries using a numerical suffix if conflicts occur. The rule also checks for DN uniqueness and retries if a match is found, up to a maximum number of attempts, before returning the final unique DN.
    </Description>

    <Source><![CDATA[
        import sailpoint.object.Application;
        import sailpoint.object.Field;
        import sailpoint.object.Identity;
        import sailpoint.server.IdnRuleUtil;
        import sailpoint.tools.GeneralException;

        import javax.naming.NamingException;
        import java.util.Collections;
        import java.util.HashMap;
        import java.util.List;
        import java.util.Map;

            private int MAX_ATTEMPTS = 10;

            /**
             * Retrieves the "forestName" from the application settings.
             * This method looks for the "forestSettings" attribute within the application object.
             * If the attribute is found and contains a list of maps, it extracts the "forestName"
             * from the first map in the list. If no such attribute or value is found, it returns null.
             * @return the forest name if found, or null if not found or the attribute is in an unexpected format
             */
            private String getBaseDNFromSource() {
                // Retrieve the attribute value for "forestSettings"
                Object attributeValue = application.getAttributeValue("forestSettings");

                // Check if the attribute is a List
                if (attributeValue instanceof List) {
                    List forestSettingsList = (List) attributeValue;

                    // Ensure the list is not empty
                    if (!forestSettingsList.isEmpty()) {
                        Object firstElement = forestSettingsList.get(0);

                        // Check if the first element is a Map and contains "forestName"
                        if (firstElement instanceof Map) {
                            Map forestSettingsMap = (Map) firstElement;
                            Object forestName = forestSettingsMap.get("forestName");

                            // Return the "forestName" if found
                            if (forestName != null) {
                                return forestName.toString();
                            }
                        }
                    }
                }

                // Return null if "forestName" is not found
                return null;
            }

            /**
             * Retrieves the base DN based on the given organizational unit path.
             * This method determines the base DN by checking the organizational unit path for known regions.
             * It supports regions like Europe, America, Africa, Asia, and Oceania. If no region is matched,
             * the method returns the default base DN.
             * @param adOrganizationalUnitPath the target OU path
             * @return the appropriate base DN
             * @throws IllegalArgumentException if the organizational unit path is null or empty
             */
            public String getLdapBaseDnFromOrganizationalUnitPath(String adOrganizationalUnitPath) {
                if (adOrganizationalUnitPath == null || adOrganizationalUnitPath.isEmpty()) {
                    throw new IllegalArgumentException("getLdapBaseDnFromOrganizationalUnitPath - adOrganizationalUnitPath cannot be null or empty");
                }

                // Normalize the input to uppercase to ensure case-insensitive comparison
                String normalizedDn = adOrganizationalUnitPath.toUpperCase();

                // Map region to their respective base DN prefix
                HashMap regionPrefixMap = new HashMap();
                regionPrefixMap.put("DC=EUROPE", "DC=EUROPE");
                regionPrefixMap.put("DC=AMERICA", "DC=AMERICA");
                regionPrefixMap.put("DC=AFRICA", "DC=AFRICA");
                regionPrefixMap.put("DC=ASIA", "DC=ASIA");
                regionPrefixMap.put("DC=OCEANIA", "DC=OCEANIA");

                // Loop through the map to find a matching region
                for (Object entryObj : regionPrefixMap.entrySet()) {
                    Map.Entry entry = (Map.Entry) entryObj;  // Cast to Map.Entry
                    String key = (String) entry.getKey();
                    String value = (String) entry.getValue();

                    if (normalizedDn.contains(key)) {
                        return value + "," + getBaseDNFromSource();
                    }
                }

                // Return the default base DN if no region is matched
                return getBaseDNFromSource();
            }

            /**
             * Calculates a unique distinguishedName (DN) for a user, potentially placing them in a "DISABLED ACCOUNTS" OU if needed.
             * @param givenName               the user's given name
             * @param sn                      the user's surname
             * @param adOrganizationalUnitPath the target OU path
             * @param isDisabled              whether the account should be placed in a disabled OU
             * @return the calculated distinguishedName
             * @throws NamingException if a naming error occurs during the process
             */
            public String calculateDistinguishedName(String givenName, String sn, String adOrganizationalUnitPath, String isDisabled) throws NamingException, GeneralException {
                // Validate input
                validateInput(givenName, sn, adOrganizationalUnitPath);

                // Convert isDisabled to boolean
                boolean disabled = Boolean.parseBoolean(isDisabled);

                // Calculate basePath
                String basePath = buildBasePath(adOrganizationalUnitPath, disabled);
                String distinguishedName = buildBaseDistinguishedName(givenName, sn, basePath);
                int attempt = 0;

                // Retry loop for uniqueness
                while (attempt < MAX_ATTEMPTS) {
                    if (attempt > 0) {
                        distinguishedName = appendSuffixToGivenName(distinguishedName, attempt);
                        log.error("calculateDistinguishedName - Attempt " + attempt + ": Trying distinguishedName: " + distinguishedName);
                    }

                    if (isUnique(distinguishedName)) {
                        log.error("calculateDistinguishedName - distinguishedName " + distinguishedName + " is unique.");
                        return distinguishedName;
                    } else {
                        attempt++;
                        log.error("calculateDistinguishedName - distinguishedName " + distinguishedName + " already exists. Retrying...");
                    }
                }

                throw new NamingException("calculateDistinguishedName - Cannot generate unique distinguishedName for " + givenName + " " + sn + " after " + MAX_ATTEMPTS + " attempts");
            }

            /**
             * Validates the input parameters.
             * @param givenName               the user's given name
             * @param sn                      the user's surname
             * @param adOrganizationalUnitPath the target OU path
             * @throws IllegalArgumentException if any of the input parameters are null or empty
             */
            private void validateInput(String givenName, String sn, String adOrganizationalUnitPath) {
                if (givenName == null || givenName.isEmpty() || sn == null || sn.isEmpty() || adOrganizationalUnitPath == null || adOrganizationalUnitPath.isEmpty()) {
                    throw new IllegalArgumentException("validateInput - None of the required parameters can be null or empty");
                }
            }

            /**
             * Builds the base distinguishedName based on the user's given name, surname, and OU path.
             * @param givenName               the user's given name
             * @param sn                      the user's surname
             * @param basePath                the organizational unit path
             * @return the base distinguishedName
             */
            private String buildBaseDistinguishedName(String givenName, String sn, String basePath) {
                return "CN=" + sn + " " + givenName + "," + basePath;
            }

            /**
             * Builds the full path to the target organizational unit.
             * @param adOrganizationalUnitPath the target OU path
             * @param isDisabled               whether the account should be placed in a disabled OU
             * @return the full base path for DN construction
             */
            private String buildBasePath(String adOrganizationalUnitPath, boolean isDisabled) {
                if (isDisabled) {
                    return "OU=DISABLED ACCOUNTS," + getLdapBaseDnFromOrganizationalUnitPath(adOrganizationalUnitPath);
                } else {
                    return adOrganizationalUnitPath;
                }
            }

            /**
             * Appends a numerical suffix to the distinguishedName.
             * @param distinguishedName the current distinguishedName
             * @param attempt           the retry attempt number
             * @return the distinguishedName with the appended suffix
             */
            private String appendSuffixToGivenName(String distinguishedName, int attempt) {
                String incrementSuffix = String.valueOf(attempt);

                // Find the position of the first comma (which separates CN part from the rest of the DN)
                int firstCommaIndex = distinguishedName.indexOf(',');

                // Extract the CN part and append the suffix
                String cnPart = distinguishedName.substring(0, firstCommaIndex);  // Get CN=NAPOLEONI Nicolas
                String restOfDn = distinguishedName.substring(firstCommaIndex);  // Get the rest of the DN after the first comma

                // Append the suffix to the CN part
                cnPart = cnPart + incrementSuffix;  // CN=NAPOLEONI Nicolas1

                // Return the full distinguishedName with the modified CN part
                return cnPart + restOfDn;  // CN=NAPOLEONI Nicolas1,ou=USERS,ou=ACG,ou=MRS,...
            }

            /**
             * Checks if the generated distinguishedName is unique.
             * @param distinguishedName The distinguishedName to check.
             * @return true if the distinguishedName is unique, false otherwise.
             */
            public boolean isUnique(String distinguishedName) {
                List sourcesId = Collections.singletonList(application.getId());
                List searchValues = Collections.singletonList(distinguishedName); // Use typed List

                int count = idn.attrSearchCountAccounts(sourcesId, "distinguishedName", "Equals", searchValues);
                return count == 0;  // Return true if no existing accounts found
            }

            return calculateDistinguishedName(identity.getFirstname(), identity.getLastname(), identity.getAttribute("adOrganizationalUnitPath"), identity.getAttribute("cloudLifecycleState"));
        ]]></Source>
</Rule>
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<Rule name="SamAccountNameGenerator" type="AttributeGenerator">
    <Description>
        This rule generates a unique SAM account name using input parameters like AD trigram, first name, and last name, ensuring it’s no longer than 20 characters. If a forced SAM account name is provided, it uses that directly. The rule checks for existing accounts and attempts up to 10 retries with an incrementing number if a conflict occurs. For contractors, it sets the AD trigram to “EXT”; for employees, it uses the provided trigram. The last name is sanitized by removing spaces and non-alphanumeric characters. If a unique name can’t be generated within the retry attempts, a NamingException is thrown.    </Description>
    <Source><![CDATA[
        import sailpoint.object.Application;
        import sailpoint.object.Field;
        import sailpoint.object.Identity;
        import sailpoint.server.IdnRuleUtil;
        import sailpoint.tools.GeneralException;
        import javax.naming.NamingException;

            private int MAX_USERNAME_LENGTH = 20;
            private int MAX_ATTEMPTS = 10;

            /**
             * Generates a sAMAccountName based on the input parameters, ensuring uniqueness and proper formatting.
             *
             * @param adTrigram         The AD trigram to use.
             * @param personType        The type of person (employee or contractor).
             * @param firstName         The first name of the person.
             * @param lastName          The last name of the person.
             * @param forcedSamAccountName Forced sAMAccountName if provided.

             * @return The generated sAMAccountName.
             * @throws GeneralException If there are errors checking uniqueness.
             * @throws NamingException If a unique name cannot be generated after multiple attempts.
             */
            public String calculateSamAccountName(String adTrigram, String personType, String firstName, String lastName, String forcedSamAccountName) throws GeneralException, NamingException {
                // Validate input
                if (personType == null || personType.isEmpty() || lastName == null || lastName.isEmpty() || firstName == null || firstName.isEmpty() || adTrigram == null || adTrigram.isEmpty()) {
                    log.error("calculateSamAccountName - One or more required parameters are null or empty");
                    throw new IllegalArgumentException("calculateSamAccountName - None of the required parameters can be null or empty");
                }

                // Return forced sAMAccountName if provided
                if (forcedSamAccountName != null && !forcedSamAccountName.isEmpty()) {
                    log.error("calculateSamAccountName - Using forced sAMAccountName: " + forcedSamAccountName);
                    return forcedSamAccountName;
                }

                // Determine trigram
                String trigram = personType.equalsIgnoreCase("contractor") ? "EXT" : adTrigram;
                log.error("calculateSamAccountName - Trigram determined as: " + trigram);

                // Sanitize last name
                String sanitizedLastName = sanitizeName(lastName);
                log.error("calculateSamAccountName - Sanitized last name: " + sanitizedLastName);

                // Process first name parts
                String[] firstNameParts = firstName.trim().split(" ");
                String firstNamePart = firstNameParts[0].substring(0, 1).toUpperCase() + (firstNameParts.length > 1 ? firstNameParts[1].substring(0, 1).toUpperCase() : "");
                log.error("calculateSamAccountName - Processed first name part: " + firstNamePart);

                // Construct base sAMAccountName
                String baseSamAccountName = (trigram + "." + firstNamePart + sanitizedLastName).toUpperCase();
                log.error("calculateSamAccountName - Base sAMAccountName generated: " + baseSamAccountName);

                // Truncate to 20 characters if needed
                if (baseSamAccountName.length() > MAX_USERNAME_LENGTH) {
                    baseSamAccountName = baseSamAccountName.substring(0, MAX_USERNAME_LENGTH);
                    log.error("calculateSamAccountName - Base sAMAccountName truncated to: " + baseSamAccountName);
                }

                // Try generating a unique sAMAccountName with retry mechanism
                String sAMAccountName = baseSamAccountName;
                boolean isNameConflict = true;
                int attempt = 0;

                while (isNameConflict && attempt < MAX_ATTEMPTS) {
                    if (attempt > 0) {
                        String incrementSuffix = String.valueOf(attempt);
                        sAMAccountName = baseSamAccountName.substring(0, Math.min(MAX_USERNAME_LENGTH - incrementSuffix.length(), baseSamAccountName.length())) + incrementSuffix;
                        log.error("calculateSamAccountName - Attempt " + attempt + ": Trying sAMAccountName: " + sAMAccountName);
                    }

                    // Check if the username is unique
                    boolean isUnique = isUnique(sAMAccountName);

                    if (isUnique) {
                        isNameConflict = false;
                        log.error("calculateSamAccountName - sAMAccountName " + sAMAccountName + " is unique.");
                    } else {
                        attempt++;
                        log.error("calculateSamAccountName - sAMAccountName " + sAMAccountName + " already exists. Retrying...");
                    }
                }

                // If the name is still not unique after MAX_ATTEMPTS, throw an exception
                if (isNameConflict) {
                    throw new NamingException("calculateSamAccountName - Cannot generate unique sAMAccountName for " + firstName + " " + lastName + " after " + MAX_ATTEMPTS + " attempts");
                }

                log.error("calculateSamAccountName - Successfully generated sAMAccountName: " + sAMAccountName);
                return sAMAccountName;
            }

            /**
             * Sanitizes a name by removing unwanted characters.
             *
             * @param name The name to be sanitized.
             * @return The sanitized name.
             */
            protected String sanitizeName(String name) {
                String sanitized = name.replaceAll("\\s+", "").replaceAll("[^a-zA-Z0-9]", "");
                log.error("sanitizeName - Sanitized name: " + sanitized);
                return sanitized;
            }

            /**
             * Checks if the generated distinguished name is unique.
             * @param sAMAccountName The sAMAccountName to check.
             * @return true if the distinguished name is unique, false otherwise.
             */
            public boolean isUnique(String sAMAccountName) {
                List sourcesId = Collections.singletonList(application.getId());
                List searchValues = Collections.singletonList(sAMAccountName); // Use typed List

                int count = idn.attrSearchCountAccounts(sourcesId, "sAMAccountName", "Equals", searchValues);
                return count == 0;  // Return true if no existing accounts found
            }

            return calculateSamAccountName(identity.getAttribute("adTrigram"), identity.getAttribute("personType"), identity.getFirstname(), identity.getLastname(), identity.getAttribute("forcedSamaccountname"));
        ]]></Source>
</Rule>
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<Rule name="UserPrincipalNameGenerator" type="AttributeGenerator">
    <Description>
        This rule generates a unique SAM account name using input parameters like AD trigram, first name, and last name, ensuring it’s no longer than 20 characters. If a forced SAM account name is provided, it uses that directly. The rule checks for existing accounts and attempts up to 10 retries with an incrementing number if a conflict occurs. For contractors, it sets the AD trigram to “EXT”; for employees, it uses the provided trigram. The last name is sanitized by removing spaces and non-alphanumeric characters. If a unique name can’t be generated within the retry attempts, a NamingException is thrown.    </Description>
    <Source><![CDATA[
        import sailpoint.object.Application;
        import sailpoint.object.Field;
        import sailpoint.object.Identity;
        import sailpoint.server.IdnRuleUtil;
        import sailpoint.tools.GeneralException;
        import javax.naming.NamingException;

            private int MAX_ATTEMPTS = 10;

            public String calculateUserPrincipalName(String sAMAccountName, String email, String upnAlignedWithMail) throws NamingException {
                // Validate input
                if (sAMAccountName == null || sAMAccountName.isEmpty() || email == null || email.isEmpty() || upnAlignedWithMail == null || upnAlignedWithMail.isEmpty()) {
                    throw new IllegalArgumentException("calculateUserPrincipalName - None of the required parameters can be null or empty");
                }

                String baseUpn = null;

                if (sAMAccountName != null && !sAMAccountName.isEmpty()) {
                    baseUpn = sAMAccountName + "@XXXXX.COM";
                    log.error("calculateUserPrincipalName - Base UPN from sAMAccountName: " + baseUpn);
                }

                if ("yes".equalsIgnoreCase(upnAlignedWithMail)) {
                    if (email == null || email.isEmpty()) {
                        throw new IllegalArgumentException("calculateUserPrincipalName - Can't calculate userPrincipalName for " + identity.getFirstname() + " " + identity.getLastname() + " because it must equal the email, but the email is empty.");
                    }
                    if (!email.equalsIgnoreCase("[email protected]")) {
                        baseUpn = email;
                        log.error("calculateUserPrincipalName - Base userPrincipalName aligned with email: " + baseUpn);
                    }
                }

                if (baseUpn == null) {
                    throw new IllegalArgumentException("calculateUserPrincipalName - Unable to determine baseUpn");
                }

                // Try generating a unique UPN name with retry mechanism
                boolean isNameConflict = true;
                String upn = baseUpn;
                int attempt = 0;

                while (isNameConflict && attempt < MAX_ATTEMPTS) {
                    if (attempt > 0) {
                        String incrementSuffix = String.valueOf(attempt);
                        String[] parts = baseUpn.split("@");
                        String upnPrefix = parts[0];
                        String domain = parts[1];
                        if (upnPrefix.length() + incrementSuffix.length() > 64) {
                            upnPrefix = upnPrefix.substring(0, 64 - incrementSuffix.length());
                        }
                        upn = upnPrefix + incrementSuffix + "@" + domain;
                        log.error("calculateUserPrincipalName - Conflict found, attempting new userPrincipalName: " + upn);
                    }

                    // Check if the username is unique
                    boolean isUnique = isUnique(upn);

                    if (isUnique) {
                        isNameConflict = false;
                        log.error("calculateUserPrincipalName - userPrincipalName " + upn + " is unique.");
                    } else {
                        attempt++;
                        log.error("calculateUserPrincipalName - userPrincipalName " + upn + " already exists. Retrying...");
                    }
                }

                // If the name is still not unique after MAX_ATTEMPTS, throw an exception
                if (isNameConflict) {
                    throw new NamingException("calculateUserPrincipalName - Cannot generate unique userPrincipalName for " + identity.getFirstname() + " " + identity.getLastname() + " after " + MAX_ATTEMPTS + " attempts");
                }

                log.error("calculateUserPrincipalName - Successfully generated userPrincipalName: " + upn);
                return upn;
            }

            /**
             * Checks if the generated userPrincipalName name is unique.
             * @param userPrincipalName The userPrincipalName to check.
             * @return true if the userPrincipalName name is unique, false otherwise.
             */
            public boolean isUnique(String userPrincipalName) {
                List sourcesId = Collections.singletonList(application.getId());
                List searchValues = Collections.singletonList(userPrincipalName); // Use typed List

                int count = idn.attrSearchCountAccounts(sourcesId, "userPrincipalName", "Equals", searchValues);
                return count == 0;  // Return true if no existing accounts found
            }

            return calculateUserPrincipalName(identity.getAttribute("sAMAccountName"), identity.getAttribute("email"), identity.getAttribute("upnAlignedWithMail"));

        ]]></Source>
</Rule>

Hello Nicolos,

Following are the steps.

  1. userPrincipalName attribute should be there in CREATE PROFILE
  2. Update the attribute with transform logic and it should be a static transform.
  3. In the static transform.
"transform": {
            "attributes": {
              "GetUpnAlignedWithEmail ": {
                "attributes": {
                  "values": [
                    {
                      "attributes": {
                        "name": "upnAlignedWithEmai"
                      },
                      "type": "identityAttribute"
                    },
                    "NotPresent"
                  ]
                },
                "type": "firstValid"
              },
"GetEmailAddress ": {
                "attributes": {
                  "values": [
                    {
                      "attributes": {
                        "name": "email"
                      },
                      "type": "identityAttribute"
                    },
                    "no-email"
                  ]
                },
                "type": "firstValid"
              },
              "value": "#if($GetUpnAlignedWithEmail != 'NotPresent' && $GetEmailAddress != 'no-email')$GetEmailAddress#{else}[email protected]#end"
            },
            "type": "static"
          }

Make sure that above transform logic is added to “userPrincipalName” attribute inside the create profile of your AD source.

The sAMAccountName will be picked from Create Profile only and no need to call the code for generating SamAccountName again.

Thanks, with your solution we dont need anymore the rule to calculate the UPN right?

How can I check the unicity of UPN?

For SamAccountName, If you are already performing the uniqueness check, then, you should be good

But, when the UPN will be equals to Email, how the email is getting calculated?

It’s plan to enter the email address automatically in a autorithative source and will be pushed to the email field of the identity

Ok and if its entering from Authz Source, will that be always unique? Will they we taking care of Uniqueness?

Hello,

Sorry for my late reply, I tried few things.

In my create account provisioning policy i saw it’s possible to call a transform type rule.
This transform can call a cloud rule.

{
            "name": "userPrincipalName",
            "transform": {
                "type": "rule",
                "attributes": {
                    "name": "UserPrincipalNameGenerator"
                }
            },
            "attributes": {
                "upnAlignedWithMail": "upnAlignedWithMail",
                "sAMAccountName": "ext.nnapoleoni",
                "email": "[email protected]"
            },
            "isRequired": false,
            "type": "string",
            "isMultiValued": false
        }

I’ve mocked the attributes with fixed values for testing. But when I assign an entitlement, then the rule is well trigerred but it throw an exception cause sAMAccountName is null.

If you check the UserPrincipalNameGenerator rule I think it’s normal cause the input of the rule is

  return calculateUserPrincipalName(identity.getAttribute("sAMAccountName"), identity.getAttribute("email"), identity.getAttribute("upnAlignedWithMail"));

sAMAccountName come from identity and it’s not good cause the attributes is not on the identity but coming dynamicaly from the transform rule.

Do you have any idea how to do that?

Thanks

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.