Leveraging User Criteria in your custom applications

User Criteria (UC) have been around on the ServiceNow platform for quite some time now. They were a welcome extension to providing a more flexible way for controlling the set of users who should or shouldn’t have access to specific records. With UCs you can define a grouping of users either by name, groups they belong to, roles they have, or through a script.

Standard User Criteria definition form.

You can then take that UC and add/configure it on a record as having the ability to ‘can read’ or ‘cannot read’ a specific record or group of records. Probably the best known examples of this are in the OOB knowledge base and service catalog applications.

KB article can read/cannot read attributes.

Available For/Not Available For related lists on catalog items.

But what if you wanted to be able to use UCs in your custom applications? The following will provide a roadmap on how you can do this!

Lets review how UCs are used for security by using the concrete example from the knowledge base. For the Knowledge Base application there are can/cannot read attributes (KB article image shown above) on the knowledge base record as well as the individual article record.  On the knowledge base record the UCs are associated as related records and shown in the ‘Can Read’/’Cannot Read’ related lists.  On the knowledge base articles there are ‘Can Read’/’Cannot Read’ glidelist attributes directly on the record. The way the OOB security has been implemented to use those UC lists for determining if a user can read a specific knowledge article is that:

  • A user can NOT be in either the knowledge base or knowledge articles cannot read UCs to be able to read the article.
  • If there is a can read UC on the knowledge base, the user must be in the UC to read articles in the knowledge base.
  • If there is a can read UC on the knowledge article, the user must be in the UC to read the article.
  • If there are no UCs defined on either the knowledge base or article, the article is readable by any user.

This is generally the way the service catalog works as well with the catalog and catalog item.

There are other OOB applications using UCs as well….but what if you wanted to be able to use UCs in other places to control access that aren’t part of OOB applications? To do this you’d need some way of evaluating a user’s inclusion in a UC. Thankfully there is a scriptable object that does just that: UserCriteriaLoader.

UserCriteriaLoader was a utility Mike Sherman (also a long-time ServiceNow employee) and I found while trying to develop the security model for a project we were working on. It’s relatively easy to use and just needs some fairly straightforward wrappers to make it reusable for our needs.

UserCriteriaLoader has several functions on it but the main one we are going to make use of is userMatches(). The userMatches() function takes the sys_id of a user you’re checking and an array of UC sys_id’s to check against. The function then evaluates the user against those UCs and returns true/false indicating if the user is in at least one of the UCs. Something like the following:

var user = gs.getUserId();
var uc_array = [“<uc_sys_id_1>”, “<uc_sys_id_2>”, “<uc_sys_id_3>”];
var ret = sn_uc.UserCriteriaLoader.userMatches( user, uc_array);


The above is a great start as it gives you the base capability to evaluate a user’s inclusion in a set of UCs, but what we really want is a utility that wrappers the above to give us a generic way of evaluating users against UCs in the way that UCs tend to be applied to records.

The script include below gives us a reusable utility to do this in the two primary ways we found UCs to be applied to a record:

  • A glidelist directly on the record
  • A related list with references to the record and to UC’s


UserCriteriaLoader.userMatches() requires the user’s sys_user record sys_id and an array of UC record sys_ids. The ‘trick’ to the utility is converting either the glidelist or a list of related records into that array of UC record sys_ids. Let’s look at some specific examples of how to use each function.

//'UserCriteriaLoader.userMatches()' Sample Usage
var gr = new GlideRecord("my_custom_table"); //Table name of record for which are evaluating access
gr.get(""); //sys_id of record for which you are evaluating access
var user_can_read = new UserCriteria(gs.getUserID(), gr).evalUCGlideList("my_can_read", "my_cannot_read");

The above would return true/false depending on the user’s inclusion in the can/cannot read UCs. A couple of things to note:

  • The cannot read parameter on the function is optional.  If not included it is assumed that the user is NOT part of any cannot read UCs.
  • If given a cannot read parameter, the function will take it into account by determining if the given user is part of the cannot read UC(s) and if they are returning false as the ‘answer’ as being included in any can read UCs would no longer matter for functionality to be consistent with the way OOB applications us UCs.
  • The above is somewhat contrived as one of the biggest use cases is where you’d already HAVE a glide record to work with as part of the platform (BRs & ACLs mainly).

Usage when the UCs are associated via a related list is a bit more complicated….but just because data model is a more complicated. The overall approach is the same in that we want to build an array of UCs for the can read and cannot read related lists and feed them to base function. 

var gr = new GlideRecord("my_custom_table");
gr.get("");
var user_can_read = new UserCriteria(gs.getUserID(), gr).evalUCRelatedList("m2mCanTable", "canGrRefFieldName", "canUCRefFieldName", "m2mCannotTable", "cannotGrRefFieldName", "cannotUCRefFieldName");

//m2mCanTable: name of the table that contains the list of can read UCs related to the record in my_custom_table.
//canGrRefFieldName: reference to a record on my_custom_table.
//canUCRefFieldName: reference to a UC record
//m2mCannotTable: name of the table that contains the list of cannot read UCs related to the record in my_custom_table.
//cannotGrRefFieldName: reference to a record on my_custom_table
//cannotUCRefFieldName: reference to a UC record

Hopefully, this is helpful and gives the ServiceNow community an extra tool in their toolbox to solve their problems with the power of the ServiceNow platform!

NOTE:

  • UserCriteriaLoader is NOT documented on docs.servicenow.com or in developer.servicenow.com. However, if you search in OOB script includes you will see it being used. So a bit of ‘buyer beware’ but we believe it’s relatively safe to use….i.e. while not formally documented it seems unlikely that ServiceNow would deprecate the object/functions.
  • The script include given was factored for the use case/need we had at the time, it can certainly be reworked to fit your needs…but the building blocks are there for most use cases.

var UserCriteria = Class.create();
UserCriteria.prototype = {
    /**
    name: initialize
    description: Initializes the UserCriteria object by setting the user sys_id and the GlideRecord.
    param: {String} [userSysId] - sys_id of the user we want to check.
    param: {GlideRecord} [grToCheck] - GlideRecord of the record we want to check against.
    example:
    var matches = new UserCriteria(userSysID, grToCheck)
    returns: {UserCriteria} returns a initialized UserCriteria Object.
    */
    initialize: function(userSysId, grToCheck) {
        this.userSysId = userSysId;
        this.grToCheck = grToCheck;
    },

    /**
    name: evalUCGlideList
    description: Takes the field names on the table/GlideRecord that are GlideList(s) pointing too User Criteria for "Can" & "Cannot".
    param: {String} [ucCanField] - The field name of the GlideList for the "Can" User Criteria.
    param: {String} [ucCannotField] - The field name of the GlideList for the "Cannot" User Criteria.
    example:
    var matches = new UserCriteria(userSysID, grToCheck).evalUCGlideList("<can_read_uc_glidelist_field>", "<cannot_read_uc_glidelist_field>");
    returns: {boolean} Returns true if user passed in is part of any of the user criteria in the GlideList. 
    */
    evalUCGlideList: function(ucCanField, ucCannotField) {
        var ret = false;
        if (ucCannotField)
            ret = (!this._matchesUCListField(ucCannotField, false) && this._matchesUCListField(ucCanField, true));
        else
            ret = (this._matchesUCListField(ucCanField, true));

        return ret;

    },

    /**
    name: evalUCRelatedList
    description: Takes m2m tables for can and can't mappings, with ref field names of "connectors", that connects a record to User Criteria and returns true/false indicating a users access depending on related lists.
    param: {String} [m2mCanTable] - Name of the m2m table connecting the record with User Criteria 'can' access.
    param: {String} [canGrRefFieldName] - Name of the field on the m2m table that points to the GlideRecord (can access).
    param: {String} [canUCRefFieldName] - Name of the field on the m2m table that points to the User Criteria (can access).
    param: {String} [m2mCannotTable] - Name of the m2m table connecting the record with User Criteria for can't access
    param: {String} [cannotGrRefFieldName] - Name of the field on the m2m table that points to the GlideRecord (can't access).
    param: {String} [cannotUCRefFieldName] - Name of the field on the m2m table that points to the User Criteria (can't access).
    example:
    var matches = new UserCriteria(userSysID, grToCheck).evalUCRelatedList(m2mCanTable, canGrRefFieldName, canUCRefFieldName, m2mCannotTable, cannotGrRefFieldName, cannotUCRefFieldName)
    returns: {boolean} Returns true if user passed in is part of any of the user criteria in the GlideList. 
    */
    evalUCRelatedList: function(m2mCanTable, canGrRefFieldName, canUCRefFieldName, m2mCannotTable, cannotGrRefFieldName, cannotUCRefFieldName) {

        var ret = false;
        if (m2mCannotTable)
            ret = (!this._matchesUCListTable(m2mCannotTable, cannotGrRefFieldName, cannotUCRefFieldName, false) && this._matchesUCListTable(m2mCanTable, canGrRefFieldName, canUCRefFieldName, true));
        else
            ret = this._matchesUCListTable(m2mCanTable, canGrRefFieldName, canUCRefFieldName, true);
        return ret;

    },

    /**
    name: _matchesUCListField
    description: Takes the field name on the table/GlideRecord that is a GlideList pointing too User Criteria.
    param: {String} [ucField] - The field name of the GlideList for the User Criteria.
    param: {boolean} [emptyListReturn] - What to return in case the list of criteria is empty (false: for cannot read; true: for can read).

    returns: {boolean} Returns true if user passed in is part of any of the user criteria in the GlideList. 
    */
    _matchesUCListField: function(ucField, emptyListReturn) {

        var ucArray = [];
        var ucFieldValue = this.grToCheck.getValue(ucField);
        if (ucFieldValue != null)
            ucArray = ucFieldValue.split(",");

        return (ucFieldValue && !gs.isLoggedIn()) ? !emptyListReturn : this._matchesUCList(ucArray, emptyListReturn);

    },

    /**
    name: _matchesUCListTable
    description: Takes a m2m table, with ref field names of "connectors", that connects a record to User Criteria and returns true/false indicating if the user is in any of the User Criteria.
    param: {String} [m2mTable] - Name of the m2m table connecting the record with User Criteria
    param: {String} [grRefFieldName] - Name of the field on the m2m table that points to the GlideRecord.
    param: {String} [ucRefFieldName] - Name of the field on the m2m table that points to the User Criteria.
    param: {boolean} [emptyListReturn] - What to return in case the list of criteria is empty.
    
    returns: {boolean} Returns true if user passed in is part of any of the user criteria in the GlideList. 
    */
    _matchesUCListTable: function(m2mTable, grRefFieldName, ucRefFieldName, emptyListReturn) {

        //var ret = false;
        var ucArray = this._glideQuerytoArray(m2mTable, grRefFieldName, ucRefFieldName);
        return (ucArray.length > 0 && !gs.isLoggedIn()) ? !emptyListReturn : this._matchesUCList(ucArray, emptyListReturn);

    },


    /**
    name: individualUserCriteriaCheck
    description: Used to check the includance of a user in a single User Criteria. 
    param: {String} [uc] - Sys_id of a single User Criteria
    example:
    var matches = new UserCriteria(userSysID, grToCheck).individualUserCriteriaCheck("<uc_sys_id>")
    returns: {boolean} Returns true if user passed in is part of any of the user criteria. 
    */
    individualUserCriteriaCheck: function(uc) {

        var ucArrayofOne = [uc];
        return this._matchesUCList(ucArrayofOne, false);

    },

    /**
    name: _matchesUCList
    description: Makes call to OOB UserCriteriaLoader for evaluation of user/criteria and takes in to account empty user criteria.
    param: {Array} [ucArray] - Array of relevent User Criteria sys_ids.
    param: {boolean} [emptyListReturn] - what to return if User Criteria array is empty.

    returns: {boolean} boolean if user is in User Criteria passed.
    */
    _matchesUCList: function(ucArray, emptyListReturn) {

        var ret = false;
        if (this.isEmpty(ucArray))
            ret = emptyListReturn;
        else
            ret = sn_uc.UserCriteriaLoader.userMatches(this.userSysId, ucArray);
        return ret;

    },

    /**
    name: isEmpty
    description: Checks if the inpur array is empty or contains only inactive records.
    param: {Array} [ucArray] - Array of relevent User Criteria sys_ids.
    example:
    this.isEmpty(ucArray)
    returns: {boolean} True if array has no elements or if all associated records are inactive.
    */
    isEmpty: function(ucArray) {
        if (ucArray.length == 0) {
            return true;
        }
        var ucGr = new GlideRecord("user_criteria");
        ucGr.addActiveQuery();
        ucGr.addQuery("sys_id", "IN", ucArray);
        ucGr.setLimit(1);
        ucGr.query();
        return !ucGr.hasNext();
    },

    /**
    name: getAllUserCriteria
    description: Given a user sys_id returns the list of User Criteria that user is part of.
    example:
    new UserCriteria(userSysID, grToCheck).getAllUserCriteria()
    returns: {String} - List of User Criteria sys_id's that the user is part of.
    */
    getAllUserCriteria: function() {

        return sn_uc.UserCriteriaLoader.getAllUserCriteria(this.userSysId);

    },

    /**
    name: _glideQuerytoArray
    description: An internal/hidden function used to look up the related User Criteria records and build them in to an array.
    param: {String} [m2mTable] - Name of the m2m table connecting the record with User Criteria
    param: {String} [grRefFieldName] - Name of the field on the m2m table that points to the GlideRecord.
    param: {String} [ucRefFieldName] - Name of the field on the m2m table that points to the User Criteria.
    
    returns: {Array} An array of User Criteria that are related to the record via the m2m table.
    */
    _glideQuerytoArray: function(m2mTable, grRefFieldName, ucRefFieldName) {

        var ucArray = [];
        var grUC = new GlideRecord(m2mTable);
        grUC.addQuery(grRefFieldName, this.grToCheck.sys_id);
        grUC.query();
        while (grUC.next()) {
            ucArray.push(grUC.getValue(ucRefFieldName).toString());
        }

        return ucArray;
    },

    type: 'UserCriteria'
};

 

Date Posted:

June 3, 2024

Share This:

4 Comments

  1. Dorian June 11, 2024 at 10:46 am - Reply

    I think you have a small typo:

    On line 94 “this._matchesList(ucArray, emptyListReturn);”
    On line 110: “return this.matchesList(ucArrayofOne, false);”,

    I believe that you renamed the function to “_matchesUCList” so those function calls need to be renamed.

    • Jacob Kimball June 11, 2024 at 11:36 am - Reply

      Thanks for catching this Dorian! I believe I’ve fixed them, but let me know if you see anything else askew!

  2. Dorian June 11, 2024 at 10:52 am - Reply

    Great article! Love the knowledge sharing.

    Question: So this wrapper currently makes an assumption that you need to have another custom table that holds the m2m data right? I wonder if you can skip the idea of having an extra table to hold that and just pass in an existing User Criteria.

    So for example:
    In KB you have the “Can Read” where you add all the UCs that “Can Read”.

    What if you just had a function that takes in a given UC record, evaluates if the user matches thes the criteria. That would save you from having to create m2m tables right? In cases where you need more than 1 UC to be evaluated, you just loop through the function you created and return true if any match.

    • Jacob Kimball June 11, 2024 at 11:42 am - Reply

      Absolutley! We had the m2m use case for what we implemented so we built that wrapper to create the needed array of UC’s from that data model. If you just need to pass in a UC you could use _matchesUCList() directly. The “_” prepending the function name is typically used to indicate a function intended for internal use only (by other functions in that object)…but that is just a convention, so you could use it directly as-is or rename it to remove the “_” (and make the updates throughout the rest of the code) and use it that way. Thanks for your comments!

Leave A Comment

Categories

Tags

Loading

Related Posts

Fresh Content
Direct to Your Inbox

Just add your email and hit subscribe to stay informed.