Granting Temporary Roles/Groups in ServiceNow

In today's article, we're going to build some new functionality around granting temporary permissions. This will give us the ability to grant users temporary permissions by either directly granting a role, or by adding them to a group temporarily from a user's profile. 

ServiceNow Grant Temporary Roles or Groups

We're also going to create the ability to bulk-add users, temporarily, to a given group. This could be an assignment group, or a security/permissions group. It could also be a group created specifically for temporarily granting a specific subset of roles. 

We're also going to allow an Administrator to set a specific expiration date for these temporary permissions. 

Finally, for security purposes, we're going to make it so that users who have temporary versions of the higher permissions roles (admin/security_admin) cannot add the admin or security_admin roles (temporary or permanent) to themselves or other users. 

So now that our project requirements are laid out before us, let's begin! 

Note: If you're feeling super lazy and just want to download an update set containing this functionality so you can deploy it into your instance already, scroll to the bottom of the article for a link to the Tools page, or find the Grant Temporary Permissions tool under the Tools section in the nav-bar at the top of this page! 

Defining the Fields

So the first thing we'll need to do, is add some fields to the sys_user_has_role and sys_user_grmember tables. These are the m2m (many-to-many) tables where the associations between users and roles, and between users and groups are stored, respectively. 

We're going to need to track two key pieces of information: Whether or not the permissions are temporary, and when the expiration date is. So to both of the above table, let's add those fields. 

The Temporary field should be a Boolean (True/False) field, with a default value of False. Here's how mine is configured: 

The Expiration Date field should be a Date field type. Not Date/Time, just Date. Here's how I've configured that one: 

Next, just add the same fields to the sys_user_grmember table, and add the new fields to the Roles and Groups related lists on the sys_user table (by right-clicking the header on the groups/roles related lists on a user's profile and going to Configure -> List Design).

Validating the Data

Since we can assume that all temporary roles/groups must have an expiration date, let's create a UI Policy that'll ensure that if the Temporary field is ticked (true) then the Expiration Date field will be both visible and mandatory; but if it is false, then the expiration date should be neither visible, n'or mandatory. And just for good measure (to ensure that no scripts or DOM-manipulation attempts to bypass our UI Policy, we'll create a Data Policy as well.

Data Policies are simpler than UI Policies, and can easily be converted, so let's begin by creating our data policy. On the sys_user_has_role table, right-click the header and go to Configure -> Data Policies. On Helsinki, click the hamburger menu at the top-left of the list, and click on Configure and then Data Policies. On the corresponding list of Data Policies, click New

Give the new Data Policy a friendly short description like Set Expiration Mandatory When Temporary is True. Then, set the Conditions to Temporaryistrue. Tick all the boxes at the top except Inherit: Reverse if false, Active, Apply to import sets, Apply to SOAP, and Use as UI Policy on client. 

Save the record, and then create a new Data Policy Rule from the related list at the bottom. Set the Field name to Expiration and set Mandatory to True. It should look something like this: 

Click Update to return to the Data Policy record. 
Once there, click on the Convert this to UI Policy UI Action. You'll be taken to a UI Policy record, created with similar conditions and actions to the Data Policy. Now you just need to add one thing piece to the existing UI Policy Action: Set Visible to True. This way (since Reverse if false is turned on), if Temporary is false, then the expiration date will not be shown. 
Finally, repeat these steps for the sys_user_has_role table. 

Now that we've ensured that only acceptable data can be entered into the table, we're ready to start building our functionality. 

Building the Script Include

The first part of this functionality that we're going to create, is the ability to directly add temporary roles to a given user, or add that user to a group temporarily.
The way we're going to accomplish that, is by eventually having a UI Action on the sys_user table that only Admins can see. This UI action is going to launch a GlideDialogWindow containing a UI Page that will present the Admin with a list of roles or groups depending on their selection.
Then, once they've filled in some roles or groups and an expiration date in the client window, we'll need to submit those selections to a server-side script that'll do the work of actually creating the associations between the user, and the roles/groups selected. To accomplish this, we'll be using GlideAjax. If you haven't read our article on GlideAjax and callback functions, I strongly recommend that you check it out over here

Start off by navigating to System Definition -> Script Includes, and creating a new Script Include. I've named mine TemporaryPermissions. Make sure you check the Client callable checkbox, and ServiceNow will provide you with a script stub that extends AbstractAjaxProcessor, which is what we need in order to call it using GlideAjax, from a client script (which you may remember from our article on GlideAjax and asynchronous callback functions). 

Give the Script Include a nice description, and let's get started! 
Here is my code... It's a bit of a beast, but I've tried to document it thoroughly so you can see what's going on in there. You'll also see some notes in the script, that talk about pieces (such as the UI page) that we haven't created yet. Don't worry about those. ;-) 

You might also notice that some methods/functions begin with an underscore, whereas others do not. Simply put, functions beginning with an underscore are meant to be called only by other, internal code; not from an external API. These functions are "helper" functions that I can call from other parts of my code, to do little bits of work for me over and over. 

Finally, you might notice my use of JSDoc notation in the method descriptions. Terms like "@returns {string}". For more information on JSDoc (which I highly recommend using in any multifunctional code), see here

var TemporaryPermissions = Class.create();
TemporaryPermissions.prototype = Object.extendsObject(AbstractAjaxProcessor, {

    /**
     * This method is called via GlideAjax, from the _createTemporaryGroups function inside the grant_temporary_permissions UI page, after the user clicks OK.
     * Its' function is to create temporary group memberships for one user, into one or more groups.
     * This function relies on several parameters:
     * sysparm_groups: An array or comma-separated string list of the sys_IDs of the group(s) to which the given user (from sysparm_userID) should be added. NOTE: An array will automatically be converted to a comma-delimited string in a GlideAjax call.
     * sysparm_userID: The sys_id of the user that should be added to the selected group(s).
     * sysparm_expiration: A string containing the system date-formatted date on which the user's group membership should expire. The user's membership to the selected group(s) will expire at midnight following this date. For example, if the date is 7/11/17, the user will have access for all of 7/11/16, but will cease to have access at 12:01AM on the morning of 7/12/16 (give or take a few minutes).
     * @returns {string} a message indicating whether the groups have been added.
     */
    createTemporaryGroups: function ()
    {
        var successfulGroupAdditions = []; //Declare an array to hold the groups we've successfully added the user to, for logging purposes.
        var selectedGroups = this.getParameter('sysparm_groups'); //get comma-separated string of groups to be added, by sys_id
        selectedGroups = selectedGroups.split(','); //split the string by comma, into an array in the same variable (thanks, javascript)
        var userID = this.getParameter('sysparm_userID'); //get the sys_id of the user to be added to the group
        var expirationDate = this.getParameter('sysparm_expiration'); //get the expiration date of the requested permissions

        //Validate that all parameters were found
        if (!selectedGroups || !userID || !expirationDate) {
            gs.logError('Could not find one of the following parameters: selectedGroups (' + selectedGroups + '), userID (' + userID + '), expirationDate (' + expirationDate + ')', 'createTemporaryGroups method of TemporaryPermissions client callable script include');
            return 'Missing some data. Please be sure to provide the selected groups, as well as the expiration date.';
        }

        var groupInsertGR = new GlideRecord('sys_user_grmember'); //Declare the gliderecord for inserting members into the sys_user_grmember table.
        var workingGroup; //instantiate a variable for the "working group" to make the for loop below a little prettier.

        //Loop over each group, and add the selected user to it by inserting a record into the sys_user_grmember table.
        for (var i = 0; i < selectedGroups.length; i++) {
            workingGroup = selectedGroups[i];

            groupInsertGR.initialize();
            groupInsertGR.setValue('user', userID);
            groupInsertGR.setValue('group', workingGroup);
            groupInsertGR.setValue('u_temporary', 'true');
            groupInsertGR.setValue('u_expiration', expirationDate);
            groupInsertGR.insert();

            successfulGroupAdditions.push(groupInsertGR.group.name);
        }

        gs.log('Added user with sys_id ' + userID + ' to the following groups: [' + successfulGroupAdditions + ']. Group membership expires on ' + expirationDate + '.');
        return 'Temporary groups granted.';
    },

    /**
     * This method is called via GlideAjax, from the _createTemporaryGroupMemberships function inside the grant_temporary_group UI page, after the user clicks OK.
     * Its' function is to create temporary group memberships for one or more users into a single group.
     * This function relies on several parameters:
     * sysparm_groupID: The sys_id of the group to which the user(s) should be added.
     * sysparm_users: A comma-delimited string, containing the USER IDs (from the user_name field on the sys_user record) of the user(s) who should be temporarily added to the selected group. User IDs generally look like: ABC1234 (three letters followed by four numbers), but can vary.
     * sysparm_expiration: A string containing the system date-formatted date on which the user's group membership should expire. The user's membership to the selected group(s) will expire at midnight following this date. For example, if the date is 7/11/17, the user will have access for all of 7/11/16, but will cease to have access at 12:01AM on the morning of 7/12/16 (give or take a few minutes).
     * @returns {string} a message indicating which users (by user ID) were successfully added to the group.
     */
    createTemporaryGroupMemberships: function ()
    {
        var successInsertedUsers = []; //Declare a blank array to hold the list of successfully inserted users, for reporting and messaging back to the user.
        var groupToAdd = this.getParameter('sysparm_groupID'); //get the sys_id of the (one) group to add the user to.
        var expirationDate = this.getParameter('sysparm_expiration'); //get the system date-formatted expiration date for the group memberships.
        var selectedUsers = this.getParameter('sysparm_users'); //A comma-separated string of user IDs (that is, user IDs from the user_name field in the sys_user table, NOT sys_ids).

        //Validate that all parameters were found
        if (!groupToAdd || !selectedUsers || !expirationDate) {
            gs.logError('Could not find one of the following parameters: groupToAdd (' + groupToAdd + '), selectedUsers (' + selectedUsers + '), expirationDate (' + expirationDate + ')', 'createTemporaryGroups method of TemporaryPermissions client callable script include');
            return 'Missing some data. Please be sure to provide the user IDs, as well as the expiration date.';
        }

        selectedUsers = this._trimAndSplit(selectedUsers.toString()); //Call a custom helper function to split the comma-separated string into an array, and then trim each element of the array to ensure no fuzzy spaces mess up our function.
        selectedUsers = this._getUserSysIDsFromUserIDs(selectedUsers); //Call another custom helper function to get the sys_IDs of the users specified by their user ID. NOTE: If multiple users with the same user_ID exist, this function will get the sys_ID for each such user specified. For example, if you specify one user: ABC1234, and there are two users with that exact same user ID, then you'll get an array of two sys_IDs back from this function.
        //selectedUsers now contains sys_ids for processing

        var groupMembershipInsertGR = new GlideRecord('sys_user_grmember'); //Declare a GlideRecord for inserting into the sys_user_grmember table.
        var workingUser; //instantiate workingUser just to make our loop below a little prettier.

        //Iterate over the selected users and add each one to the specified group.
        for (var i = 0; i < selectedUsers.length; i++) {
            workingUser = selectedUsers[i];

            groupMembershipInsertGR.initialize();
            groupMembershipInsertGR.setValue('user', workingUser);
            groupMembershipInsertGR.setValue('group', groupToAdd);
            groupMembershipInsertGR.setValue('u_temporary', 'true');
            groupMembershipInsertGR.setValue('u_expiration', expirationDate);
            groupMembershipInsertGR.insert();

            successInsertedUsers.push(groupMembershipInsertGR.user.user_name);
        }

        gs.log('Added the following users to group with sys_id ' + groupToAdd + '. These are the users: [' + successInsertedUsers + ']. Group memberships expire on ' + expirationDate + '.');
        return 'Temporary group memberships added for the following users: ' + successInsertedUsers;
    },

    /**
     * This method is called via GlideAjax, from the _createTemporaryRoles function on the grant_temporary_permissions UI page, after the user clicks OK.
     * Its' function is to temporarily grant a given user one or more specified roles.
     * This function relies on the following parameters:
     * sysparm_roles: a comma-separated string (or array, which will be converted via AJAX to a string) of the roles which the user should be temporarily granted.
     * sysparm_userID: The sys_id of the selected user, who should be temporarily granted the selected roles.
     * sysparm_expiration: The system date-formatted string representing the last day on which the user should retain these roles. Note that if the expiration day is in the past, the user's access will be terminated the following midnight, just as if the expiration date was today.
     * @returns {string}
     */
    createTemporaryRoles: function ()
    {
        var successInsertedRoles = []; //Declare a blank array to hold the list of successfully inserted roles.

        var selectedRoles = this.getParameter('sysparm_roles');
        selectedRoles = this._trimAndSplit(selectedRoles, ','); //Call a custom function that splits a string based on the token in the second argument (a comma), but which also trims each element as it is pushed to the new array.
        var userID = this.getParameter('sysparm_userID');
        var expirationDate = this.getParameter('sysparm_expiration');

        //Validate that all parameters were found
        if (!selectedRoles || !userID || !expirationDate) {
            gs.logError('Could not find one of the following parameters: selectedRoles (' + selectedRoles + '), userID (' + userID + '), expirationDate (' + expirationDate + ')', 'createTemporaryGroups method of TemporaryPermissions client callable script include');
            return 'Missing some data. Please be sure to provide the roles, as well as the expiration date.';
        }

        var roleInsertGR = new GlideRecord('sys_user_has_role');
        var workingRole;
        for (var i = 0; i < selectedRoles.length; i++) {
            workingRole = selectedRoles[i];

            roleInsertGR.initialize();
            roleInsertGR.setValue('user', userID);
            roleInsertGR.setValue('role', workingRole);
            roleInsertGR.setValue('u_temporary', 'true');
            roleInsertGR.setValue('u_expiration', expirationDate);
            roleInsertGR.insert();

            successInsertedRoles.push(roleInsertGR.role.name + '');
        }

        gs.log('Added user with sys_id ' + userID + ' to the following roles: [' + successInsertedRoles + ']. Role access expires on ' + expirationDate + '.');
        return 'Temporary roles successfully granted.';
    },


    _getUserSysIDsFromUserIDs: function (userIDs)
    {
        var foundUsers = [];
        var userSysIDs = [];

        var userGR = new GlideRecord('sys_user');
        userGR.addEncodedQuery('user_nameIN' + userIDs);
        userGR.query();

        while (userGR.next()) {
            userSysIDs.push(userGR.getValue('sys_id'));
            foundUsers.push(userGR.getValue('user_name'));
        }


        return userSysIDs;
    },

    _trimAndSplit: function (str, tok)
    {
        if (!str) {
            return;
        }
        if (!tok || tok.length != 1) {
            tok = ',';
        }
        if (str.indexOf(tok) < 0) {
            return [str]; //Return the original string, because no instances of the token were found.
        }
        //declare return array
        var ret = [];
        //convert the input string to an array, splitting on the given token (usually ',')
        var ara = str.split(tok);
        var ele;
        for (var i = 0; i < ara.length; i++) {
            ele = ara[i];
            //Trim each element in the split string array, then push it to the return array.
            ret.push(ele.trim());
        }
        //return the trimmed and split array.
        return ret;
    },

    type: 'TemporaryPermissions'
});

Okay, so that's something of a beast. If you had trouble following what that does, don't worry. I'll explain what it expects and returns, when we get around to using it. Speaking of which...

The UI Page

Now that we have our master script built, it's time to clench up and write some Jelly. 
Don't ask me why I have such an irrational hatred of Jelly. My wife loves it. Not as much as Angular, but it's up there. I just can't stand it. 

Anyway, let's start by navigating to System UI -> UI Pages from the Application Navigator, and then clicking New to create a new UI page. Give it a name (I chose grant_temporary_permissions), and a description. 

You'll notice that your new UI page contains 3 primary sections: HTMLClient script, and Processor. Today, we're just going to be using HTML, and Client script

Here, I'll paste the HTML I used, followed by an explanation of the important bits: 

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
    <style>
        
        #grant_permissions td {
            padding-bottom: 10px;
            text-align: center;
        }
        #upDownButtons {
            display: none;
        }
        #roles_or_groups {
            margin-left: 10px;
        }
        #expiration_date {
            margin-left: 10px;
        }
        table {
            padding-left: 10px;
            padding-right: 10px;
        }
        
        
    </style>
    <table cellpadding="50" id="grant_permissions">
      <TR>
            <TD colspan="2" align="center">
                Grant user temporary roles, or groups: <select id="roles_or_groups" autofocus="true" required="true" onchange="selectBoxChanged()">
                    <option value="" selected="true">--Select One--</option>
                    <option value="roles">Roles</option>
                    <option value="groups">Groups</option>
                </select>
                <hr />
          </TD>
       </TR>
       <TR>
         <TD id="roles_label" style="display:none;" align="center">
            Select the roles you'd like to temporarily grant the user.
         </TD>
         <TD id="groups_label" style="display:none;" align="center">
            Select the groups you'd like to temporarily add the user to.
         </TD>
      </TR>
      <TR>
         <TD id="roles_slush_td" style="display:none;">
            <g:ui_slushbucket name="role_slush" />
         </TD>
      <!--</TR>
      <TR>-->
         <TD id="groups_slush_td" style="display:none;">
            <g:ui_slushbucket name="group_slush" />
         </TD>
      </TR>
        <TR>
            <TD id="date_field_td" colspan="2" style="display:none;">
                <hr /><div title="The last day that the user should have the selected group/role. To expire tomorrow, select today's date.">Expires on: <g:ui_date name="expiration_date" id="expiration_date" /></div><br />
                <strong>NOTE:</strong> The selected groups/roles will expire at 12:00AM on the morning FOLLOWING the above selected date. 
            </TD>
        </TR>
      <TR>
         <TD id="buttons_td" style="display:none;" align="center" colspan="2">
            <!-- Include the 'dialog_buttons_ok_cancel' UI macro -->
            <g:dialog_buttons_ok_cancel ok="return mKay()" cancel="return nope()" ok_type="button" cancel_type="button"/>
         </TD>
      </TR>
   </table>
</j:jelly>

At the top, inside the style tags, is just some basic formatting and prettying up of the page, except for line 10 which hides some buttons that show up in the "slush bucket" further down. 

Next, we set up a table to contain most of the elements we're going to be working with, because this is the easiest way I could think of for someone like me who knows little about design, to get things to line up properly. 😎

In the first row, we have an element which asks the admin whether he or she would like to grant temporary roles, or temporary groups to the selected user. This also happens to be the only element which is visible when the UI page initially loads. 
I'm using the autofocus attribute to ensure that this is the first thing that the admin has selected when the UI page loads, and the onchange attribute to call a function in my client script (selectBoxChanged())when the value changes. As you'll see later on, this function basically just displays the appropriate elements below the select box, depending on what was selected. 

The next rows in the table contain two columns: One for if the admin selects Roles (meaning they'd like to grant the selected user temporary roles), and one for if they select Groups. These elements both contain a UI Macro called ui_slushbucket

Next is a date field, which uses the ui_date UI Macro, and finally, we've got the buttons (UI Macro: dialog_buttons_ok_cancel). When OK is clicked, it runs the function mKay(). When cancel is clicked, it runs nope(). Both of these functions are in the client script. 

Speaking of the client script, here it is:

//Called when the form loads
addLoadEvent(function ()
{
    //Load the groups when the form loads
    role_slush.clear();
    group_slush.clear();
    _getRolesForSlush();
    _getGroupsForSlush();
    return false;
});

//Called when the 'OK' button gets clicked
function mKay()
{
    document.getElementById('ok_button').innerHTML = "Working...";
    var permissionType = document.getElementById('roles_or_groups').value;

    if (permissionType == 'roles') {
        _createTemporaryRoles();
    }
    else if (permissionType == 'groups') {
        _createTemporaryGroups();
    }
    else {
        //how did you even get here?
        alert('Something went wrong. Did you select what type of permissions you want to grant the user? (groups/roles)');
    }

    //Close the window
    GlideDialogWindow.get().destroy();
}

//Called when the 'Cancel' button gets clicked
function nope()
{
    //Close the window
    GlideDialogWindow.get().destroy();
    return false;
}

//Called when the select box changes
function selectBoxChanged()
{
    var permissionType = document.getElementById('roles_or_groups').value;
    if (permissionType == 'roles') {
        //show roles TDs (display="table-cell")
        document.getElementById('roles_label').style.display = 'table-cell';
        document.getElementById('roles_slush_td').style.display = 'table-cell';
        document.getElementById('groups_label').style.display = 'none';
        document.getElementById('groups_slush_td').style.display = 'none';

        document.getElementById('buttons_td').style.display = 'table-cell';
        document.getElementById('date_field_td').style.display = 'table-cell';
    }
    else if (permissionType == 'groups') {
        //show groups TDs
        document.getElementById('groups_label').style.display = 'table-cell';
        document.getElementById('groups_slush_td').style.display = 'table-cell';
        document.getElementById('roles_label').style.display = 'none';
        document.getElementById('roles_slush_td').style.display = 'none';

        document.getElementById('buttons_td').style.display = 'table-cell';
        document.getElementById('date_field_td').style.display = 'table-cell';

    }
    else {
        //hide both!
        document.getElementById('roles_label').style.display = 'none';
        document.getElementById('roles_slush_td').style.display = 'none';
        document.getElementById('groups_label').style.display = 'none';
        document.getElementById('groups_slush_td').style.display = 'none';
        document.getElementById('date_field_td').style.display = 'none';

        document.getElementById('buttons_td').style.display = 'none';
    }
}

function _createTemporaryRoles()
{
    var selectedRoles = role_slush.getValues(role_slush.getRightSelect());
    if (!selectedRoles) {
        alert('You must select at least one role!');
        return;
    }
    var ga = new GlideAjax('TemporaryPermissions');
    ga.addParam('sysparm_name', 'createTemporaryRoles');
    ga.addParam('sysparm_roles', selectedRoles);
    ga.addParam('sysparm_userID', g_form.getUniqueValue());
    ga.addParam('sysparm_expiration', expiration_date.value);
    ga.getXML(_showResultsCB);
}

function _createTemporaryGroups()
{
    var selectedGroups = group_slush.getValues(group_slush.getRightSelect());
    if (!selectedGroups) {
        alert('You must select at least one group!');
        return;
    }
    var ga = new GlideAjax('TemporaryPermissions');
    ga.addParam('sysparm_name', 'createTemporaryGroups');
    ga.addParam('sysparm_groups', selectedGroups);
    ga.addParam('sysparm_userID', g_form.getUniqueValue());
    ga.addParam('sysparm_expiration', expiration_date.value);
    ga.getXML(_showResultsCB);
}

function _showResultsCB(response)
{
    var answer = response.responseXML.documentElement.getAttribute('answer');
    g_form.addInfoMessage(answer);
}

function _getRolesForSlush()
{
    var roleGR = new GlideRecord('sys_user_role');
    roleGR.addQuery('active', 'true');
    roleGR.orderBy('name');
    roleGR.query(_rolesCallBack);
}

function _rolesCallBack(roleGR)
{
    //role_slush.addLeftChoice('value', 'name');
    while (roleGR.next()) {
        var roleName = roleGR.getValue('name');
        var roleID = roleGR.getValue('sys_id');
        role_slush.addLeftChoice(roleID, roleName);
    }
}

function _getGroupsForSlush()
{
    var groupGR = new GlideRecord('sys_user_group');
    groupGR.addQuery('active', 'true');
    groupGR.addQuery('user', '!=', g_form.getUniqueValue());
    groupGR.orderBy('name');
    groupGR.query(_groupsCallBack);
}

function _groupsCallBack(groupGR)
{
    while (groupGR.next()) {
        var groupName = groupGR.getValue('name');
        var groupID = groupGR.getValue('sys_id');
        group_slush.addLeftChoice(groupID, groupName);
    }
}

As you can see, the first thing we do is clear both "slush buckets". They shouldn't contain any data, but we do this just to be safe. Next, we call the internal _getRolesForSlush() and _getGroupsForSlush() functions. Since they're both very similar, so I'll just go over _getRolesForSlush(). 

Pro Tip: Though we couldn't find documentation on this anywhere, client-side queries seem to have a default maximum of 500 records. Changing the "glide.db.max_view_records" property doesn't seem to affect this until you hit the 10,000 default limit of that property. Instead, to return more records (if you have more than 500 roles/groups), we could use ".setLimit(n)" before our query.

Essentially, what the _getRolesForSlush() function is doing, is making an asynchronous GlideRecord query to get a list of all roles, in alphabetical order. It then passes the result _rolesCallBack(), which adds each role as an option on the left side of the roles "slush bucket". This populates the list of roles for the admin to select from. 

Pro Tip: If you wanted to be extra clever here, you could make another function that got a list of all the roles the user currently has, and filter them out from the returned roles!

At this point, imagine an admin visits a user's profile, clicks the "Grant temporary access" UI action, and this page loads. The admin selects "Roles" from the drop-down list, double-clicks a few roles from the list, and sets an expiration date of today (which is the default). Finally, the admin clicks the OK button. -- 

What happens next, is the client script function mKay() is run. This function checks what type of permissions the admin requested (roles or groups), and calls the appropriate helper function. The helper function gets the selected roles/groups, and makes a GlideAjax call  to our script include to create those roles using the createTemporaryRoles method. 

Finally, the helper function asynchronously (for performance reasons) passes the results of creating the temporary roles to another function (_showResultsCB()), which shows the results of the GlideAjax call on the admin's screen. 

NoteI'm using a modified version of the ClientDateTimeUtils script include, which you can find on the community site, here. You can see my modified version in the update set at the bottom of this article. 

The UI Action

Now we've got to create the button that launches the UI page. Since we've already built all of the logic to do all the work, that's pretty much all that this little button has to do. So navigate to a user record on the sys_user table, right-click the header and go to Configure -> UI Actions.

Create a new UI action with a name like Grant Temporary Permissions, or whatever you like. Give it an action name like temporary_roles, and tick the Form link box. Set Onclick to showTemporaryPermissionsDialog(), and make sure the Condition says gs.hasRole('admin');, so non-admins can't see the UI action. Finally, fill in the script:

function showTemporaryPermissionsDialog(){
    //Open a dialog window to select Approval Groups
    var dialog = new GlideDialogWindow('grant_temporary_permissions');
    dialog.setTitle('Grant Temporary Permissions');
    dialog.setSize('auto', 'auto');
    dialog.render();
    //Make sure to not submit the form when button gets clicked
    return false;
}

All that this script has to do, is set the size and title of the GlideDialogWindow, and render it. The scripts we built before, will do the rest. 

Preventing Abuse

It might immediately occur to the more security-conscious of you, that a user with a temporary admin role, could easily grant themselves or others, permanent admin. In order to limit that, I've also created a business rule on the sys_user_has_role table. The BR runs before insert or update, and the conditions are Role is admin or Role is security_admin, and the script (below) just ensures that the user has some non-temporary version of the Admin role:

(function executeRule(current, previous /*null when async*/)
{
    var allowAction = true;
    var isPermanentAdmin = doesUserHavePermanentRole('admin');
    var isPermanentSecurityAdmin = doesUserHavePermanentRole('security_admin');

    if (current.role.name == 'admin' && (!isPermanentAdmin)) {
        allowAction = false;
    } else if (current.role.name == 'security_admin' && (!isPermanentSecurityAdmin)) {
        allowAction = false;
    }

    if (!allowAction) {
        gs.addErrorMessage('Since your admin or security_admin role is temporary, you are not able to add or modify the admin or security_admin role for yourself or other users.');
        current.setAbortAction(true);
    }
        

})(current, previous);

function doesUserHavePermanentRole(role) {
    var currentUser = gs.getUserID();
    var roleGR = new GlideRecord('sys_user_has_role');
    roleGR.addQuery('u_temporary', 'false');
    roleGR.addQuery('role.name', role);
    roleGR.addQuery('user', currentUser);
    roleGR.query();
    if (roleGR.next()) {
        return true;
    } else {
        return false;
    }
}

This could've been written a little more succinctly, but I think it would've made it less clear. Basically, we're just checking that the user has a non-temporary version of the admin/security_admin role, before allowing them to grant or update higher-level permissions for themselves or others. 

Note: This protection is really designed to prevent people from accidentally adding permanent or temporary admin roles. It can be bypassed by users with the temporary admin/security_admin role. Whenever you grant an Admin role - temporary or not - there is an implied level of trust. Do not grant high-permissions roles to users that you cannot trust to use them wisely! 

Although in the update set at the end of this article, you'll find additional functionality (such as the ability to bulk-add a list of users to a group by User ID), this article has become quite long already, so the last piece of the puzzle that we'll go over is the lynchpin to this whole scheme: The scheduled job that revokes temporary roles once they've expired! 

Pro TipDon't forget that scheduled jobs don't get captured in update sets by default, so be sure to force this into an update set or export/import if you plan to move this from Dev to Prod!

In the Application Navigator, head over to System Definition -> Scheduled Jobs, and click New. On the Interceptor page, click Automatically run a script of your choosing

Give it a good name, then make sure that Run is set to Daily, and that Time is set to 1 minute after midnight (00:01:00). Though scheduled jobs don't always execute at exactly the same time, this ensures that it will at least run after midnight each morning. 

Finally, here's the script: 

revokeGroups();
revokeRoles();


function revokeGroups()
{
    var groupGR = new GlideRecord('sys_user_grmember');
    groupGR.addQuery('u_temporary', 'true');
    groupGR.addQuery('u_expiration', '<', gs.now());
    groupGR.query();
    groupGR.deleteMultiple();
}

function revokeRoles()
{
    var roleGR = new GlideRecord('sys_user_has_role');
    roleGR.addQuery('u_temporary', 'true');
    roleGR.addQuery('u_expiration', '<', gs.now());
    roleGR.query();
    roleGR.deleteMultiple();
}

This script checks for temporary permissive records in the sys_user_grmember, and sys_user_has_role tables, where the expiration date is earlier than now. So if the expiration date is today, then today is the last day that I'll be able to use these permissions. Some time very shortly after midnight tonight, these permissions will be revoked. 

And that's the last piece! Don't hesitate to download the update set linked at the bottom of this article if you'd like to try this functionality out on your own instance, but I hope my explanations have clarified how this sort of thing can work! 

SN Pro Tip: As with all of our free tools (available from the Tools heading in the nav-bar at the top of this page), there are no advertisements, no nags, no "pro versions", and no credit card or other info required.
Just a simple download link for an un-obfuscated update set that you can inspect and deploy into your own instance, for free

Why? Well, just because we like you, and we want to thank you for having a look at our humble website. We do hope you'll consider us for any ServiceNow development/admin/architect work that your company needs done, but that's not a requirement. No strings attached. Really

 

Click here to go to this tool's page, and download the latest version!