New hires
This section describes how you can develop your own transformation script where you take incoming data from your authoritative source and provision new users in OpenIAM and relevant downstream applications. While there are several out of OOTB examples, there will be times when its necessary to create your own.
The steps below assume that you have already completed the Synchronization configuration described in the Administrators guide. The scripts below are based on the dataset described here.
While the dataset appears to be trivial, the resulting Groovy script to process this information will demonstrate the following:
- Map attributes to the OpenIAM objects including: Map primary attributes to the Role object
- Add custom attributes
- Add Role or group
- Add supervisor
- Add organizational hierarchy
Each of above topics will be covered in order they are shown above.
Create new groovy script
The first step in developing a transformation script is to create the body of the script. You can do this by going to Webconsole -> Administration -> Groovy Manager. From the Groovy manager, select the New
button has shown below.
Next create the body of the script as show in example below (you may paste the example below to help get started). There are a few things to do when doing this:
- Class name below and the Groovy script name must be the same; Change the name to suite your needs. It should be something meaningful to aid in maintainability.
- Class must extend
AbstractUserTransformScript
as shown below - Class must override
execute()
andinit()
shown below.
The execute() and init() will be called by the synchronization framework when synchronization starts. The execute() method is where all the work will need to happen.
import org.apache.commons.lang3.StringUtilsimport org.apache.commons.collections4.CollectionUtilsimport org.openiam.base.AttributeOperationEnumimport org.openiam.base.ws.MatchTypeimport org.openiam.base.ws.SearchParamimport org.openiam.idm.searchbeans.RoleSearchBeanimport org.openiam.idm.searchbeans.UserSearchBeanimport org.openiam.idm.srvc.continfo.dto.Addressimport org.openiam.idm.srvc.continfo.dto.EmailAddressimport org.openiam.idm.srvc.continfo.dto.Phoneimport org.openiam.idm.srvc.role.dto.Roleimport org.openiam.idm.srvc.synch.dto.LineObjectimport org.openiam.idm.srvc.user.dto.*import org.openiam.provision.dto.ProvisionUserimport org.openiam.provision.type.Attributeimport org.openiam.sync.service.TransformScriptimport org.openiam.sync.service.impl.service.AbstractUserTransformScriptimport org.springframework.context.ApplicationContextclass MyEmployeeSourceTransformationScript extends AbstractUserTransformScript {private ApplicationContext contextprivate org.openiam.common.beans.mq.UserRabbitMQService userManager@Overrideint execute(LineObject rowObj, ProvisionUser pUser) {if (userManager == null) {userManager = context.getBean(org.openiam.common.beans.mq.UserRabbitMQService.class);}println "** - Transformation script called."pUser.setSkipPreprocessor(false)pUser.setSkipPostProcessor(false)return TransformScript.NO_DELETE}@Overridevoid init() {}}
Save the script. OpenIAM will ask you for a path. Unless you have defined a directory structure for your scripts, consider using /sync/user/[name of your source system]
.
OpenIAM will compile your script. Only if its free of compilation errors, will the file save successfully. Otherwise, the compilation error will be shown.
Map primary fields
The next step is to map the simple fields, like name and title, in the CSV to objects which OpenIAM can process. This process separated into two steps:
- Creating a method called populateObject()
- Add logic for the mapping.
Update script with populate object
Update the shell th at you created so that it resembles the one below. See that addition of the:
populateObject()
method- Call the populateObject() from the
execute()
method.
import org.apache.commons.lang3.StringUtilsimport org.apache.commons.collections4.CollectionUtilsimport org.openiam.base.AttributeOperationEnumimport org.openiam.base.ws.MatchTypeimport org.openiam.base.ws.SearchParamimport org.openiam.idm.searchbeans.RoleSearchBeanimport org.openiam.idm.srvc.continfo.dto.Addressimport org.openiam.idm.srvc.continfo.dto.EmailAddressimport org.openiam.idm.srvc.continfo.dto.Phoneimport org.openiam.idm.srvc.role.dto.Roleimport org.openiam.idm.srvc.synch.dto.LineObjectimport org.openiam.idm.srvc.user.dto.*import org.openiam.provision.dto.ProvisionUserimport org.openiam.provision.type.Attributeimport org.openiam.sync.service.TransformScriptimport org.openiam.sync.service.impl.service.AbstractUserTransformScriptimport org.springframework.context.ApplicationContextclass MyEmployeeSourceTransformationScript extends AbstractUserTransformScript {private ApplicationContext contextprivate org.openiam.common.beans.mq.UserRabbitMQService userManager@Overrideint execute(LineObject rowObj, ProvisionUser pUser) {if (userManager == null) {userManager = context.getBean(org.openiam.common.beans.mq.UserRabbitMQService.class);}println "** - Transformation script called."if (isNewUser) {pUser.id = null}try {populateObject(rowObj, pUser)}catch(Exception ex) {ex.printStackTrace();println "** - Transformation script error."return -1;}pUser.setSkipPreprocessor(false)pUser.setSkipPostProcessor(false)return TransformScript.NO_DELETE}private void populateObject(LineObject rowObj, ProvisionUser pUser) {// add logic to map attributes}@Overridevoid init() {}}
Map source attributes
Update the populateObject() such that it contains logic to map attributes from the source system to OpenIAM objects. There a few essential attributes that must be set:
- Metadata Type - This is the type of user and we can set it by updating the
pUser.mdTypeId
attribute on the user object - Status - This determines if the user is active, inactive or otherwise. All user objects must have a status. Statuses have a critical role in user life cycle management and this concept will be developed further in subsequent sections.
- The column headers are directly from the source. If you are using a CSV file, then these are the column headers.
private void populateObject(LineObject rowObj, ProvisionUser pUser) {def attrVal;Map<String, Attribute> columnMap = rowObj.columnMap;// user object must have a metadata type or it will not save.pUser.mdTypeId = 'DEFAULT_USER';attrVal = columnMap.get("FIRST_NAME");if (attrVal && attrVal?.value) {pUser.setFirstName(attrVal?.value)}attrVal = columnMap.get("LAST_NAME")//some last name include spaces. Need to add add spaces to last name regexif (attrVal && attrVal?.value) {pUser.setLastName(attrVal?.value)}pUser.title = columnMap.get("TITLE")?.valuepUser.employeeId = columnMap.get("EMPLOYEE_ID")?.value// user status must be set or the the object will not save.pUser.status = (columnMap.get("STATUS") != null && StringUtils.isNotEmpty(columnMap.get("STATUS").value)) ? UserStatusEnum.getFromString(columnMap.get("STATUS").value) : UserStatusEnum.ACTIVE;println "user status = " + pUser.status;}
Add custom attributes
The User object in OpenIAM can be extended to support custom attributes. Custom attributes are those attributes which are not predefined in the OpenIAM schema.
To process custom attributes, first add an addAttribute()
as shown below. In the method below is doing the following:
- Creating a UserAttribute object and setting the status to ADD.
- Next, it checks if this attribute already exists. If it does, then we change the status to REPLACE so that if the value has changed, it will be updated.
def addAttribute(ProvisionUser pUser, Attribute attr) {if (attr?.name) {def userAttr = new UserAttribute(attr.name, attr.value)userAttr.operation = AttributeOperationEnum.ADDif (!isNewUser) {for (String name : pUser.userAttributes.keySet()) {if (name.equalsIgnoreCase(attr.name)) {pUser.userAttributes.remove(name)userAttr.operation = AttributeOperationEnum.REPLACEbreak}}}pUser.userAttributes.put(attr.name, userAttr)}}
Next, update your populateObject()
to map you custom attributes as shown below.
// map custom attributesattrVal = columnMap.get("BADGE_NUMBER")if (attrVal && attrVal?.value) {addAttribute(pUser, attrVal)}attrVal = columnMap.get("PREFERRED_NAME")if (attrVal && attrVal?.value) {addAttribute(pUser, attrVal)}
Add Organization information
Our dataset has two organization level attributes: Company and Department.
First add the following import
statements to beginning of your script along with the other import statements.
import org.openiam.common.beans.mq.OrganizationRabbitMQServiceimport org.openiam.common.beans.mq.RabbitMQSenderimport org.openiam.idm.searchbeans.OrganizationSearchBeanimport org.openiam.idm.srvc.org.dto.Organization
Next add the following method which will use the Organization service to find the organization object that you are looking. In this example you will notice the call to RabbitMQ. This is because all the major services in OpenIAM are loosely coupled and communicate with each other using the message bus
def addOrganization(ProvisionUser pUser, String organizationName, String organizationType) {OrganizationRabbitMQService organizationRabbitMQService = context.getBean(OrganizationRabbitMQService.class) as OrganizationRabbitMQServiceOrganizationSearchBean osb = new OrganizationSearchBean();SearchParam searchParam = new SearchParam();searchParam.setValue(organizationName);osb.setNameToken(searchParam);osb.setOrganizationTypeId(organizationType)List<Organization> organizationList = organizationRabbitMQService.findBeans(osb, 0, 1);if (CollectionUtils.isNotEmpty(organizationList)) {println("add ${organizationList.get(0).getName()}")pUser.addAffiliation(organizationList.get(0), new HashSet<>(), null, null);} else {println("can't find organization ${organizationName}")}}
Next, map the organization information in your populateObject method as shown below.
// organization informationattrVal = columnMap.get("COMPANY")if (attrVal && attrVal?.value) {addOrganization(pUser, attrVal.value, "ORGANIZATION");}attrVal = columnMap.get("DEPARTMENT")if (attrVal && attrVal?.value) {addOrganization(pUser, attrVal.value, "DEPARTMENT");}
Manage dates
Users may have various dates linked to profiles including:
- Start date - Date they join a company
- Date a position ends
- Last date - Terminate date
The user profile objects has pre-defined fields for start date and last date. To manage these critical dates, we need to update our synchronization script.
First add the following import statement along with the other import statements. import java.text.SimpleDateFormat
Next, create a helper method which will parse the date string and create a valid date object.
Date parseDate(String date) {if (date.length() == 7) {date = "0" + date}SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MMddyyyy")return simpleDateFormat.parse(date)}
Next update, the populateObject() method call the parseDate()
method and then assign the resulting value to the ProvisionUser object.
attrVal = columnMap.get("START_DATE")if (attrVal && attrVal?.value) {try {pUser.setStartDate(parseDate(attrVal?.value))} catch (NumberFormatException ex) {log.error(ex)}}attrVal = columnMap.get("LAST_DATE")if (attrVal && attrVal?.value) {try {pUser.setLastDate(parseDate(attrVal?.value))} catch (NumberFormatException ex) {log.error(ex)}}
Add manager / supervisor
While managing the user life cycle its common to manage the employee / supervisor relationship. This relationship will be used for a variety of use cases ranging from request / approval, sending notifications, and reviewing access.
The example below shows how we can take take the manager's employee ID, find the manager and link them to the new user. The same could be done with a different attribute.
def addSupervisor(ProvisionUser pUser, String managerEmployeeId) {// ensure that a manager employee Id has been passed in.if (!managerEmployeeId) {return;}UserSearchBean usb = new UserSearchBean();usb.addEmployeeIdMatchToken(new SearchParam(managerEmployeeId, MatchType.EXACT));def userCollection = [UserCollection.ATTRIBUTES, UserCollection.PRINCIPALS] as UserCollection[];List<User> userList = userManager.findBeans(usb, userCollection, 0, 1);if (CollectionUtils.isNotEmpty(userList)) {User manager = userList.get(0);if (manager != null) {pUser.addSupervisor(manager, null);} else {println("can't find supervisor")}} else {println("can't find supervisors")}}
Invoke the above method with the code below.
attrVal = columnMap.get("SUPERVISOR")if (attrVal && attrVal?.value) {addSupervisor(pUser,attrVal?.value);}
Add phone numbers
Add the following import statement import org.openiam.idm.srvc.meta.dto.MetadataType
Create a helper method to parse the phone number and add associate it with the user object.
void addPhone(ProvisionUser pUser, String phoneType, String value, boolean isPrimary) {// works for US Phone numbersif (value.length() == 12) {MetadataType mdType = new MetadataType()mdType.setId(phoneType)// Check if the phone already existsPhone ph = pUser.getPhoneByMetadataType(mdType)boolean notExist = isNewUser || ph == nullif (notExist) {// Initial phone objectph = new Phone()ph.setMdTypeId(phoneType)ph.setActive(true)ph.setActive(true)if (isPrimary){ph.setDefault(true)ph.setUsedForSMS(true)}ph.setOperation(AttributeOperationEnum.ADD)} else {// set replace attribute to true to indicate that this object will// replace the existing objectph.setOperation(AttributeOperationEnum.REPLACE)}ph.setCountryCd("+1")ph.setAreaCd(value.substring(0, 3))ph.setPhoneNbr(value.substring(4,12))if (notExist) {pUser.addPhone(ph)}} else {println("Cannot parse phone: ${value}")}println("processed phone " + phoneType + " " + value)}
Update the populateObject() method to call the phone method.
attrVal = columnMap.get("MOBILE_PHONE")if (attrVal && attrVal?.value) {addPhone(pUser,"CELL_PHONE", attrVal?.value, true);}attrVal = columnMap.get("OFFICE_PHONE")if (attrVal && attrVal?.value) {addPhone(pUser,"OFFICE_PHONE", attrVal?.value, false);}
Add Groups
def addGroup(ProvisionUser pUser, String groupName) {if (!isNewUser) {def foundGroup = pUser.groups.find { r -> r.name == groupName }if (foundGroup) {return}}Group group = getGroupByName(groupName);if (group) {UserToGroupMembershipXref groupMembershipXref = new UserToGroupMembershipXref()groupMembershipXref.setEntityId(group.getId());groupMembershipXref.setMemberEntityId(pUser.getId());groupMembershipXref.operation = AttributeOperationEnum.ADD;pUser.groups.add(groupMembershipXref);}}
Add Roles
def addRole(ProvisionUser pUser, String roleName) {if (!isNewUser) {def foundRole = pUser.roles.find { r -> r.name == roleName }if (foundRole) {return}}Role role = getRoleByName(roleName);if (role) {UserToRoleMembershipXref roleMembershipXref = new UserToRoleMembershipXref()roleMembershipXref.setEntityId(role.getId());roleMembershipXref.setMemberEntityId(pUser.getId());roleMembershipXref.operation = AttributeOperationEnum.ADD;pUser.roles.add(roleMembershipXref);}}
Sending email notifications
Adhoc notifications
sendEmail(mailQueue,String.format("User with WorkdayId: %s already exists in OpenIAM. Please pay attention on this use case", pUser.getEmployeeId()),"User already exists");
Add the following to your list of import statements: import org.openiam.common.beans.mq.MailRabbitMQService
Declare a mailQueue variable. You can do this in your execute() method.
MailRabbitMQService mailQueue = null;
public void sendEmail(MailRabbitMQService mailQueue, String body, String caption, String toEmailAddress) {mailQueue.sendEmailAsynchronously(toEmailAddress, caption, body, false);mailQueue.sendEmailAsynchronously(toEmailAddress, caption, body, false);}