Issue with ServiceNow Service Catalog integration, and my proposed solution

I have discovered an issue with the ServiceNow Service Catalog integration… here’s the scenario

  1. Open up the ServiceNow portal page manage_access. This is the main page with the request form.

  2. Select a user you wish to request access for and click next

  3. On the left side of the filter, select “Owner”. The UI will become disabled for a moment while it queries the IDN API

  4. A list of owners will appear on the right side of the filter

Sounds fine, right? The problem arises if you have more than 250 Access Profiles or Roles. Unless all of your individual owners are contained in the first 250 Access Profiles or Roles (ordered by name), you are not going to have all the owners in that drop down.

Why is that? Let me explain

First, when you select “Owner” from the filter drop-down, the client controller calls this server-side function

function getFilterLists(userId, action) {
        var result = {
            ownerList: [],
            sourceList: []
        };
        try {
            var response = idn_getFilterLists.getAdvanceFiltersList(userId, action);

            var getFilterListsResponseParsed = JSON.parse(response);
            var getIdnOwners = getFilterListsResponseParsed.ownerList;
            var getIdnSources = getFilterListsResponseParsed.sourceList;

            if (getIdnOwners.length > 0) {
                for (var k = 0; k < getIdnOwners.length; k++) {
                    result.ownerList.push({
                        owners: getIdnOwners[k].Owner
                    });
                }
            }
            if (getIdnSources.length > 0) {
                for (var k1 = 0; k1 < getIdnSources.length; k1++) {
                    result.sourceList.push({
                        sourceName: getIdnSources[k1].sourcename
                    });
                }
            }
        } catch(ex) {
            gs.error(ex.message);
        }
        return result;
    }

It creates an empty array for the owner list, then calls a script include (a server-side script in ServiceNow) to populate that list.

The script include snippet included here queries both roles and access profiles and pushes their owners into that array

                var ownerList = [];
                var roles = this.getRoles(accessTokenServiceAccount);
                if (roles != null && roles.length > 0) {
                    for (var i = 0; i < roles.length; i++) {
                        ownerList.push({
                            type: this.constants.ROLE,
                            Owner: roles[i] && roles[i].owner && roles[i].owner.name ? roles[i].owner.name : ''
                        });
                    }
                }

                var accessProfiles = this.getAccessProfiles(accessTokenServiceAccount);
                if (accessProfiles != null && accessProfiles.length > 0) {
                    for (var j = 0; j < accessProfiles.length; j++) {
                        ownerList.push({
                            type: this.constants.ACCESS_PROFILE,
                            Owner: accessProfiles[j] && accessProfiles[j].owner && accessProfiles[j].owner.name ? accessProfiles[j].owner.name : ''
                        });
                    }
                }

I’ve included a snippet when it queries access profiles

        getAccessProfiles: function(accessToken) {

            var queryForRequest = "requestable:true AND enabled:true";
            var querySuffix = gs.getProperty('x_sap_intidn.x_sp_spnt_snow_int.search_access_profiles_query', '').trim();
            if (querySuffix != '') {
                queryForRequest += ' ' + querySuffix;
            }

            var request = this.createHttpRequest('POST', 'searchAccessProfiles', { headers: { Authorization: 'Bearer ' + accessToken }});

            request.setRequestBody(JSON.stringify({
                indices: ['accessprofile'],
                sort: ['name'],
                query: {
                    query: queryForRequest
                },
                queryResultFilter: {
                    includes: ['name', 'id', 'description', 'owner', 'source', 'entitlements']
                }
            }));

            var response = request.execute();
            return JSON.parse(response.getBody());
        }

This is then returned back to the client side where it builds a list of distinct owners.

So what’s the problem?

A few more levels down, the server-side function is making api calls to the /v3/search endpoint (actually /v3/search/accesssprofiles and /v3/search/roles that I didn’t know existed). If you’ve read the documentation on that endpoint, you’ll know that the default limit for results is 250. As I’ve shown in the code snippets, it does not paginate through the results to get everything, only the first 250.

We have 6500+ access profiles, so instead of pulling back the 400+ owners we have, we only get 97 in the drop-down.

How can it be fixed?

The server-side API call needs to paginate through the results so all access profiles and roles are pulled back.

A less expensive way might be to search identities and use this query

{
    "indices": [
        "identities"
    ],
    "query": {
        "query": "_exists_:owns"
    },
    "queryResultFilter": {
        "includes": ["displayName","id"]
    }
}

I just realized today the identity object type has an “owns” property on it, so that query only returns an identity if they own something

Thanks for reading, and let me know what you think!