Cyrille Riviere
ServiceNow Employee
ServiceNow Employee

Purpose

The article describes the Azure AD integration with ServiceNow DevOps Config. The goals are

  • Create the user and assign their DevOps Config permissions,
  • Create their groups from Azure AD and assign the DevOps Config roles.

The user logs in via SSO and uses Azure AD to authenticate to ServiceNow. The SAML response received by ServiceNow will contain all assertions, such as user and group information. The objective is to proceed with this SAML assertion to dynamically create the user and groups.

 

Install and configure

The following steps are required:

  1. Enable and install the Multi-Provider SSO plugin.
  2. Configure the Entity Provider and enable the User Provisioning capability.
  3. To enable the User Group Provisioning, further actions are required to store the list of groups. By default, table columns have a max length of 40 chars, which is inappropriate for a long string that contains a list of user groups.
    • In the Users table (sys_user), create a new column named Groups List with a max length value of 2048,
    • In the created Transform Map table, u_imp_saml_user_<some_string> table (u_imp_saml_user_<some_string>.list), update the column name http_schemas_m__claims_groups (column label is http://schemas.microsoft.com/ws/2008/06/identity/claims/groups) with a max length value of 2048.

 

Context on MultiSSO versions

From the New York release onwards, version 2 of scripts are applicable, and the OOTB scripts are read-only.

 

Feature

Type

Version 1 - Customize

Version 2 - OOTB

Version 2 - Customize

MultiSSO

Script Include

SAML2_update1

SAML2_internal

SAML2_custom

MultiSSO

Script Include

MultiSSO_SAML2_Update1

MultiSSOv2_SAML2_internal

MultiSSOv2_SAML2_custom

 

In version 2 of the MultiSSO plugin, the GlideSAML2 java class stores the current HttpRequest as attribute for SAML login/logout path. The existing limitation in our context is that only the first AttributeValue is returned for each Attribute of the AttributeStatement SAML response.

 

Customize Scripts

A best practice to modify an existing script include, in particular when it is protected or read-only, is to customize it as described in the documentation. Below are the scripts to modify or customize.

 

Script Include: MultiSSOv2_SAML2_custom

 

 

gs.include("PrototypeServer");

var MultiSSOv2_SAML2_custom = Class.create();
MultiSSOv2_SAML2_custom.prototype = Object.extend(new MultiSSOv2_SAML2_internal(), {
    initialize: function() {
		MultiSSOv2_SAML2_internal.prototype.initialize.call(this);
    },

	getAttributesMap: function() {
        try {
			var ResponseElement = Packages.org.w3c.dom.Element;
			var attributeMap = Packages.java.util.HashMap();
			var attrADgroupclaim = 'http://schemas.microsoft.com/ws/2008/06/identity/claims/groups';
			ResponseElement = this.SAML2.getResponseElement();
			if (ResponseElement != null && ResponseElement != "") {
				var attrStats = ResponseElement.getElementsByTagName("AttributeStatement"); //for each AttributeStatement			
				for(var i=0; i<attrStats.length; i++) {
					var attrs = attrStats.item(i).getChildNodes(); //for each Attribute					
					if(attrs && attrs.length>0) {
						for(var j=0; j< attrs.length; j++) {
							if (attrs.item(j).getAttribute("Name") == attrADgroupclaim) {
								var attrValues = attrs.item(j).getChildNodes(); //for each AttributeValue								
								var value = "";
								if (attrValues && attrValues.length>0) {
									for(var k=0; k< attrValues.length; k++) {
										if (value != "")
											value += '|' + attrValues.item(k).getFirstChild().nodeValue;
										else
											value = attrValues.item(k).getFirstChild().nodeValue;										
									}
								}
							}
						}
					}
				var ResponseAssertionElementADgroupclaim = value;
				}
			} else {
				ResponseAssertionElementADgroupclaim = "SAML Response Assertion is null or empty";
			}
			this.logDebug("MultiSSOv2_SAML2_custom - Assertion Element AD Group Claim: " + ResponseAssertionElementADgroupclaim);
		} catch(error) {
			this.logDebug("MultiSSOv2_SAML2_custom - Error while parsing XML Document of the SAML Response Assertion: "+error);
		} finally {
			attributeMap = this.SAML2.getGlideSaml2Api().calculateResponseAttributes();
			attributeMap.put(attrADgroupclaim,ResponseAssertionElementADgroupclaim);
			this.logDebug("MultiSSOv2_SAML2_custom - Attributes Map: " + attributeMap);
			return attributeMap;
		}
    },

	type: 'MultiSSOv2_SAML2_custom'
});

 

 

 

Transform Script: onBefore (for Transform Map: u_imp_saml_user_<some_string>)

 

 

//create the DevOps Config groups against the group name pattern
var prefix_editor = "fr-group1*";
var pattern_editor = "^fr-group1";
var prefix_admin = "fr-admin*";
var pattern_admin = "^fr-admin";
var groupname = source.http_schemas_m__claims_groups.toString();
var groups = groupname.split(",");
var regexp_editor = new RegExp(pattern_editor);
var regexp_admin = new RegExp(pattern_admin);

if (groups && groups.length>0) {
	for(var i=0; i<groups.length; i++) {
		if (action == 'insert') {
			if (regexp_editor.test(groups[i]) || regexp_admin.test(groups[i])) {
				var gr = new GlideRecord('sys_user_group');
				gr.initialize();
				gr.setValue('name',groups[i]);
				gr.insert();
				if (gr.hasNext()){
					log.info("sys_transform_map_user_onBefore - The group: "+groups[i]+" is created");
				} else {
					log.error("sys_transform_map_user_onBefore - The group: "+groups[i]+" is NOT created");
				}
			} else {
				log.error("sys_transform_map_user_onBefore - Group name is not matching DevOps Config patterns: "+groups[i]);
			}
		}
	}
}

 

 

 

Transform Script: onAfter (for Transform Map: u_imp_saml_user_<some_string>)

 

 

//assign the DevOps Config roles against the group name pattern
// then add the login user as member of the group
var prefix_editor = "fr-group1*";
var pattern_editor = "^fr-group1";
var prefix_admin = "fr-admin*";
var pattern_admin = "^fr-admin";
var groupname = source.http_schemas_m__claims_groups.toString();
var groups = groupname.split("|");
var username = source.http_schemas_x_ims_externalid.toString();
var userid = resturnSysId('sys_user',username,'user_name');
var regexp_editor = new RegExp(pattern_editor);
var regexp_admin = new RegExp(pattern_admin);
var roleid_editor = resturnSysId('sys_user_role','sn_devops_config.editor');
var roleid_admin = resturnSysId('sys_user_role','sn_devops_config.admin');
var roleid_secrets = resturnSysId('sys_user_role','sn_devops_config.secrets');

if (groups && groups.length>0) {
	for(var i=0; i<groups.length; i++) {

		if (action == 'insert') {
			var newgroupid = false;
			var groupid = resturnSysId('sys_user_group',groups[i]);
			//create the DevOps Config groups against the group name pattern
			if (!groupid) {
				if (regexp_editor.test(groups[i]) || regexp_admin.test(groups[i])) {
					newgroupid = createGroup(groups[i]);
				} else {
					log.info("sys_transform_map_user_onAfter - Group name is not matching DevOps Config patterns: "+groups[i]);
				}
			} else {
				log.error("sys_transform_map_user_onAfter - Group name: "+groups[i]+" exists.");
			}			

			//assign the DevOps Config roles against the group name pattern
			if (newgroupid) {
				assignMemberofGroup(username,groups[i],newgroupid,userid)
				if (regexp_editor.test(groups[i])) {
					assignRoleGroup('sn_devops_config.editor',groups[i],newgroupid,roleid_editor);
					assignRoleGroup('sn_devops_config.secrets',groups[i],newgroupid,roleid_secrets);
				}
				else if (regexp_admin.test(groups[i])) {
					assignRoleGroup('sn_devops_config.admin',groups[i],newgroupid,roleid_admin);
					assignRoleGroup('sn_devops_config.secrets',groups[i],newgroupid,roleid_secrets);
				}
				else {
					log.error("sys_transform_map_user_onAfter - No DevOps Config roles assigned to the SAML group: "+groups[i]);
				}
			} else if (groupid) {
				assignMemberofGroup(username,groups[i],groupid,userid);
			} else {
				log.error("sys_transform_map_user_onAfter - Group name: "+groups[i]+" does not exist.");
			}
		}

		else if (action == 'update') {
			var newgroupid = false;
			var groupid = resturnSysId('sys_user_group',groups[i]);
			if (groupid) {
				assignMemberofGroup(username,groups[i],groupid,userid);
			} else {
				log.info("sys_transform_map_user_onAfter - Group name: "+groups[i]+" is creating.");
				if (regexp_editor.test(groups[i]) || regexp_admin.test(groups[i])) {
					newgroupid = createGroup(groups[i]);
				} else {
					log.error("sys_transform_map_user_onAfter - Group name is not matching DevOps Config patterns: "+groups[i]);
				}
				if (newgroupid) {
					assignMemberofGroup(username,groups[i],newgroupid,userid);
					if (regexp_editor.test(groups[i])) {
						assignRoleGroup('sn_devops_config.editor',groups[i],newgroupid,roleid_editor);
						assignRoleGroup('sn_devops_config.secrets',groups[i],newgroupid,roleid_secrets);
					}
					else if (regexp_admin.test(groups[i])) {
						assignRoleGroup('sn_devops_config.admin',groups[i],newgroupid,roleid_admin);
						assignRoleGroup('sn_devops_config.secrets',groups[i],newgroupid,roleid_secrets);
					}
					else {
						log.error("sys_transform_map_user_onAfter - No DevOps Config roles assigned to the SAML group: "+groups[i]);
					}
				}
			}
		}
	}
}	


function createGroup(group){
	var gr = new GlideRecord('sys_user_group');
	gr.initialize();
	gr.setValue('name',group);
	gr.insert();
	if (isRecordInserted('sys_user_group','name='+group)){
		log.info("sys_transform_map_user_onAfter - The group: "+group+" is created");
		return gr.getUniqueValue();
	} else {
		log.error("sys_transform_map_user_onAfter - The group: "+group+" is NOT created");
		return false;
	}
}

function assignMemberofGroup(username,group,groupid,userid) {
	var gr = new GlideRecord('sys_user_grmember');
	gr.initialize();
	gr.setValue('group',groupid);
	gr.setValue('user',userid);
	gr.insert();
	if (isRecordInserted('sys_user_grmember','group='+groupid+'^user='+userid)){
		log.info("sys_transform_map_user_onAfter - User: "+username+" is member of group: "+group);
		return gr.getUniqueValue();
	} else {
		log.error("sys_transform_map_user_onAfter - User: "+username+" is NOT member of group: "+group);
		return false;
	}
}

function assignRoleGroup(role,group,groupid,roleid) {
	var gr = new GlideRecord('sys_group_has_role');
	gr.initialize();
	gr.setValue('group',groupid);
	gr.setValue('role',roleid);
	gr.insert();
	if (isRecordInserted('sys_group_has_role','group='+groupid+'^role='+roleid)) {
		log.info("sys_transform_map_user_onAfter - The DevOps Config role: "+role+" is assigned to the SAML group: "+group);
		return gr.getUniqueValue();
	} else {
		log.error("sys_transform_map_user_onAfter - The DevOps Config role: "+role+" is NOT assigned to the SAML group: "+group);
		return false;
	}

}

function resturnSysId(tableName,itemName,fieldName){
	var defaultField = 'name';
	var fldName = (typeof fieldName === 'undefined')?defaultField:fieldName;
	gr = new GlideRecord(tableName);
	gr.addQuery(fldName, '=', itemName);
	gr.query();
	if (gr.next()) {
		return gr.getUniqueValue();
	} else {
		return false;
	}
}

function isRecordInserted(tableName,queryString) {
	var gr = new GlideRecord(tableName);
	gr.addEncodedQuery(queryString);
	gr.query();
	gr.setLimit(1);
	if (gr.next())
		return true;
	else 
		return false;
}

 

 

 

Outcomes

  • When the user is logged in, his/her groups are created, he/she becomes a group member, and DevOps Config roles are assigned against the defined patterns.
  • This works at the first connection but applies at every attempt to update the group assignment coming from Azure AD.

 

ServiceNow Resources

Below is a list of useful links that helped me:

Version history
Last update:
‎10-20-2023 09:19 AM
Updated by:
Contributors