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.

New groovy script

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() and init() 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.StringUtils
import org.apache.commons.collections4.CollectionUtils
import org.openiam.base.AttributeOperationEnum
import org.openiam.base.ws.MatchType
import org.openiam.base.ws.SearchParam
import org.openiam.idm.searchbeans.RoleSearchBean
import org.openiam.idm.searchbeans.UserSearchBean
import org.openiam.idm.srvc.continfo.dto.Address
import org.openiam.idm.srvc.continfo.dto.EmailAddress
import org.openiam.idm.srvc.continfo.dto.Phone
import org.openiam.idm.srvc.role.dto.Role
import org.openiam.idm.srvc.synch.dto.LineObject
import org.openiam.idm.srvc.user.dto.*
import org.openiam.provision.dto.ProvisionUser
import org.openiam.provision.type.Attribute
import org.openiam.sync.service.TransformScript
import org.openiam.sync.service.impl.service.AbstractUserTransformScript
import org.springframework.context.ApplicationContext
class MyEmployeeSourceTransformationScript extends AbstractUserTransformScript {
private ApplicationContext context
private org.openiam.common.beans.mq.UserRabbitMQService userManager
@Override
int 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
}
@Override
void init() {}
}

Save the script. OpenIAM will ask you for a path. Unless you have defined a directory structure for your scripts, consider using /synch/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.StringUtils
import org.apache.commons.collections4.CollectionUtils
import org.openiam.base.AttributeOperationEnum
import org.openiam.base.ws.MatchType
import org.openiam.base.ws.SearchParam
import org.openiam.idm.searchbeans.RoleSearchBean
import org.openiam.idm.srvc.continfo.dto.Address
import org.openiam.idm.srvc.continfo.dto.EmailAddress
import org.openiam.idm.srvc.continfo.dto.Phone
import org.openiam.idm.srvc.role.dto.Role
import org.openiam.idm.srvc.synch.dto.LineObject
import org.openiam.idm.srvc.user.dto.*
import org.openiam.provision.dto.ProvisionUser
import org.openiam.provision.type.Attribute
import org.openiam.sync.service.TransformScript
import org.openiam.sync.service.impl.service.AbstractUserTransformScript
import org.springframework.context.ApplicationContext
class MyEmployeeSourceTransformationScript extends AbstractUserTransformScript {
private ApplicationContext context
private org.openiam.common.beans.mq.UserRabbitMQService userManager
@Override
int 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
}
@Override
void 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 regex
if (attrVal && attrVal?.value) {
pUser.setLastName(attrVal?.value)
}
pUser.title = columnMap.get("TITLE")?.value
pUser.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.ADD
if (!isNewUser) {
for (String name : pUser.userAttributes.keySet()) {
if (name.equalsIgnoreCase(attr.name)) {
pUser.userAttributes.remove(name)
userAttr.operation = AttributeOperationEnum.REPLACE
break
}
}
}
pUser.userAttributes.put(attr.name, userAttr)
}
}

Next, update your populateObject() to map you custom attributes as shown below.

// map custom attributes
attrVal = 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.OrganizationRabbitMQService
import org.openiam.common.beans.mq.RabbitMQSender
import org.openiam.idm.searchbeans.OrganizationSearchBean
import 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 OrganizationRabbitMQService
OrganizationSearchBean 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 information
attrVal = 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 numbers
if (value.length() == 12) {
MetadataType mdType = new MetadataType()
mdType.setId(phoneType)
// Check if the phone already exists
Phone ph = pUser.getPhoneByMetadataType(mdType)
boolean notExist = isNewUser || ph == null
if (notExist) {
// Initial phone object
ph = 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 object
ph.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);
}