Name

sn_entitlement.RoleInfoService

Description

RoleInfoService

Script

/**
* Provides information about roles reading from the database and caching as appropriate
*/
var RoleInfoService = Class.create();
RoleInfoService.prototype = {
  UNLIMITED_ROLE_NAMES: 'snc_internal,snc_external,public,guest'.split(','),
  EXCLUDED_ROLE_NAMES: 'admin,maint,nobody,security_admin',

  initialize: function() {
      this.ROLE_CACHE = {};
      this.arrayUtil = new global.ArrayUtil();
  },

  /**
   * getRoleInfo
   * Return information about a role
   * @param {string} roleId sys_id of role record
   * @returns {object}
   */
  getRoleInfo: function(roleId) {
      if (Object.prototype.hasOwnProperty.call(this.ROLE_CACHE, roleId))
          return this.ROLE_CACHE[roleId];

      var roleInfo = this._getRoleInfo(roleId);
      if (!roleInfo)
          return null;

      this.ROLE_CACHE[roleInfo.name] = roleInfo;
      this.ROLE_CACHE[roleInfo.id] = roleInfo;

      return this.ROLE_CACHE[roleId];
  },
  /**
   * getRoleInfoByName
   * Return information about a role
   * @param {string} name of a role
   * @returns {object} role info if role with specified name is present, null otherwise
   */
  getRoleInfoByName: function(roleName) {
      if (Object.prototype.hasOwnProperty.call(this.ROLE_CACHE, roleName))
          return this.ROLE_CACHE[roleName];

      var roleIds = this._getRoleSysIds(roleName);
      if (roleIds.length == 0)
          return null;

      var roleInfo = this._getRoleInfo(roleIds[0]);
      this.ROLE_CACHE[roleInfo.name] = roleInfo;
      this.ROLE_CACHE[roleInfo.id] = roleInfo;

      return this.ROLE_CACHE[roleName];
  },

  /**
   * getAllRoles
   * Return all roles in system optionally filtering for roles with specified ACL operations.
   * Certain system roles will be filtered out (EXCLUDED_ROLE_NAMES)
   * @param {array} filterAclOps
   * @returns {array} array of {'id', 'name'}
   */
  getAllRoles: function(filterAclOps) {
      return this._getRoles(null, filterAclOps);
  },

  /**
   * Returns the list of all custom roles which are roles which are not shipped OOB/servicenow owned
   * Because the determination of what is OOB is not precise, some OOB roles can be returned.
   * However there will never be missing custom roles.
   *
   * @returns {array} array of {'id', 'name'}
   */
  getCustomRoles: function() {
      var roles = [];
      var gr = new GlideRecord('sys_user_role');

      // Use license_role.category=OOB for now. It's not maintained for platform roles,
      // however it's the best we have right now.

      // Can't use RLQUERY unfortunately because license_role.sys_user_role uses a reference
      // qualifier to store the name as the key
      //gr.addEncodedQuery('RLQUERYlicense_role.sys_user_role,=0^category=0^ENDRLQUERY');

      // use subquery which is far less efficient

      var oobRoles = this._getOOBRoles();
      gr.addQuery('name', 'NOT IN', oobRoles);

      gr.query();
      while (gr.next()) {
          if (this._isCustomRole(gr))
              roles.push({
                  'id': gr.getUniqueValue(),
                  'name': gr.name + ''
              });
      }
      return roles;
  },

  /**
   * Return roles with specified names and the roles they contain (obtained recursively)
   * optionally filtering for roles with specified ACL operations
   * @param {array} roleNames
   * @param {array} filterAclOps
   * @returns {array} array of {'id', 'name'}
   */
  getRolesByName: function(roleNames, filterAclOps) {
      return this._getRoles(this._getContainedRoleIds(this._getRoleSysIds(roleNames)).ids, filterAclOps);
  },

  _getRoleInfo: function(roleId) {
      var roleRecord = this._getRoleRecord(roleId);
      if (!roleRecord)
          return null;
      var containedRoles = this._getContainedRoleIds(roleId);
      var roleName = roleRecord.name;

      return {
          'id': roleId,
          'name': roleRecord.name,
          'applicationName': roleRecord.sys_scope.getDisplayValue(),
          'packageName': roleRecord.sys_package.getDisplayValue(),
          'packageSource': roleRecord.sys_package.source.getDisplayValue(),
          'contained': {
              'ids': containedRoles.ids,
              'maxDepth': containedRoles.maxDepth,
              'totalRoles': containedRoles.totalRoles
          },
          'unlimitedusers': this._isUnlimitedRole(roleName),
          'ootb': this._isOOTBRole(roleName)
      };
  },

  /**
   * getRoles
   * Get all the roles to analyze
   * @param {string} roleIds List of role sys_ids
   * @param {array} filterAclOps optional list of ACL operations to filter by
   * @returns {array} array of {'id', 'name'}
   */
  _getRoles: function(roleIds, filterAclOps) {
      var roles = [];

      var roleNameField;
      var roleIdField;
      var gr;
      if (filterAclOps) {
          roleNameField = 'sys_user_role.name';
          roleIdField = 'sys_user_role';
          gr = new GlideAggregate('sys_security_acl_role'); // only get roles that are used in ACLs
          gr.addQuery('sys_security_acl.type', '=', 'record');
          gr.addQuery('sys_security_acl.active', '=', true); // only loook at active ACLs
          gr.addQuery('sys_security_acl.operation', 'IN', filterAclOps);
          gr.groupBy(roleIdField);
          gr.groupBy(roleNameField);
      } else {
          roleNameField = 'name';
          roleIdField = 'sys_id';
          gr = new GlideRecord('sys_user_role'); // get all roles
      }

      if (roleIds != null)
          gr.addQuery(roleIdField, 'IN', roleIds);
      else
          gr.addQuery(roleNameField, 'NOT IN', this.EXCLUDED_ROLE_NAMES);

      gr.orderBy(roleNameField);
      gr.query();
      while (gr.next()) {
          roles.push({
              'id': gr.getValue(roleIdField),
              'name': gr.getValue(roleNameField)
          });
      }

      return roles;
  },

  /**
   * _getRoleSysIds
   * To reduce the need for joins in other queries, get the role sys_ids for later use
   * @param {*} roleNames
   * @returns {array} Array of role sys ids
   */
  _getRoleSysIds: function(roleNames) {
      var roleIds = [];
      var r = new GlideRecord('sys_user_role');
      if (!gs.nil(roleNames)) {
          r.addQuery('name', 'IN', roleNames);
      }
      r.query();
      while (r.next()) {
          roleIds.push(r.getUniqueValue());
      }
      return roleIds;
  },

  /**
   * _getContainedRoles
   * Recursive function to get all the roles this role contains.
   * @todo Can we replace the names with sys_ids?
   * @param {*} roleIds Role Sys Ids that we want to get all the contained roles for
   * @param {*} containedRoleIds The roles that we know it contains already (used for recursion)
   * @param {*} level Level we are at for recursion
   * @returns
   */
  _getContainedRoleIds: function(roleIds, containedRoleIds, level) {
      if (gs.nil(containedRoleIds)) {
          containedRoleIds = [];
      }
      var directContainsIds = [];
      if (gs.nil(level)) {
          level = 0;
      }
      var cr = new GlideRecord('sys_user_role_contains');
      cr.addQuery('role', 'IN', roleIds);
      if (containedRoleIds.length > 0) {
          cr.addQuery('role', 'NOT IN', containedRoleIds);
      }
      cr.query();

      while (cr.next()) {
          directContainsIds.push(cr.getValue('contains'));
      }

      // add in seed role on the first pass
      if (level == 0) {
          containedRoleIds = this.arrayUtil.union(containedRoleIds, roleIds);
      }

      var maxDepth = level;
      if (directContainsIds.length > 0) {
          var roleInfo = this._getContainedRoleIds(directContainsIds, Array.from(containedRoleIds), ++level);
          containedRoleIds = this.arrayUtil.union(containedRoleIds, roleInfo.ids);
          maxDepth = roleInfo.maxDepth;
      }

      containedRoleIds = this.arrayUtil.union(containedRoleIds, directContainsIds);

      return {
          'maxDepth': maxDepth,
          'ids': containedRoleIds,
          'totalRoles': containedRoleIds.length,
          'direct': directContainsIds
      };
  },

  /**
   * _getRoleNames
   * Translate role sys_ids to role names
   * @param {*} roleIds
   * @returns
   */
  _getRoleNames: function(roleIds) {
      var roleNames = [];
      if (roleIds) {
          var r = new GlideRecord('sys_user_role');
          r.addQuery('sys_id', 'IN', roleIds);
          r.query();
          while (r.next()) {
              roleNames.push(r.getValue('name'));
          }
      }
      return roleNames;
  },

  /**
   * getRoleRecord
   * @param {*} roleId Sys id of the role to get the detail for
   * @returns {sys_user_role}
   */
  _getRoleRecord: function(roleId) {
      var roleInfo = new GlideRecord('sys_user_role');
      roleInfo.get(roleId);
      return roleInfo;
  },

  /**
   * _getOOBRoles
   * Return list of role names marked as OOB in license_role
   * @returns {array}
   */
  _getOOBRoles: function() {
      var r = new GlideRecord('license_role');
      r.addQuery('category', '=', '0'); // ServiceNow OOB
      r.query();
      r.orderBy('sys_user_role')
      var roles = [];
      while (r.next()) {
          roles.push(r.sys_user_role + '');
      }

      return roles;
  },

  /**
   * _isOOTBRole
   * Check whether specified role is marked as OOB in license_role. Note that return of false does not necessarily mean that
   * specified role is NOT OOB because license_role maintenance for platform roles is manual
   * @param {string} roleName
   * @returns boolean
   */
  _isOOTBRole: function(roleName) {
      var r = new GlideRecord('license_role');
      r.addQuery('category', '=', '0'); // ServiceNow OOB
      r.addQuery('name', '=', roleName);
      r.query();
      return r.hasNext();
  },

  /**
   * Given the GlideRecord for a sys_user_role record, return true if role is a custom role based on the scope
   * and customer updates to the record
   * 1. if in global scope or in servicenow, it's a custom role if its in customer updates, servicenow role otherwise
   * 2. else if it's in a custom app scope, it's a custom role
   * 3. else if it's a store app and owned by vendor, it's a custom role
   * 4. false otherwise
   * @param {GlideRecord} roleGr 
   * @returns boolean true if custom role, false if not
   */
  _isCustomRole: function(roleGr) {
      const scopeName = roleGr.sys_scope.scope.toString();
      if (scopeName == 'global' || new global.LicensingEngineGlobalHelper().isServiceNowScope(scopeName))
          return this._isPresentInCustomerUpdates(roleGr);
      if (this._isCustomApp(roleGr.sys_scope.getRefRecord()))
          return true;
      if (this._isOwnedStoreApp(roleGr.sys_scope.getRefRecord()))
          return true;
      return false;
  },

  /**
   * Given sys_scope record, return true if app with that scope is a custom app being developed on the instance
   * @param {GlideRecord} scopeGr
   * @returns boolean true if custom app, false otherwise
   */
  _isCustomApp: function(scopeGr) {
      // replace this with Scope.isCustomApp()
      return scopeGr.sys_class_name == 'sys_app';
  },

  /**
   * Given sys_scope record, return true if app is a store app which has been authored by the customer who owns
   * current instance.
   * @param {GlideRecord} scopeGr
   * @returns boolean true if own app, false otherwise
   */
  _isOwnedStoreApp: function(scopeGr) {
      // replace this with Scope.isStoreApp()
      if (scopeGr.sys_class_name != 'sys_store_app')
          return false;

      // replace with !Scope.isThirdPartyApp() without global check + AppScopeNamer type check
      const vendorKeys = gs.getProperty('sn_appauthor.all_company_keys', '').split(',');
      const companyCode = gs.getProperty('glide.appcreator.company.code', '');
      if (companyCode != '' && vendorKeys.indexOf(companyCode) < 0)
          vendorKeys.push(companyCode);
      const scopeName = scopeGr.scope.toString();

      return vendorKeys.findIndex(vendorKey => scopeName.startsWith('x_' + vendorKey + '_')) >= 0;
  },

  /**
   * Given the GlideRecord for a sys_user_role record, check with customer updates and return true if found in there.
   * Note that presence in customer updates indicates 
   * 1. a customized OOB role
   * 2. a custom role
   * We are interested only in the second, and this is a reasonable approximation. This will return true if a customer
   * has customized an OOB role record (the record iteself, not where it's used etc), but that is very unlikely
   *
   * @param {GlideRecord} roleGr
   * @returns boolean true if present in customer updates, false if not
   */
  _isPresentInCustomerUpdates: function(roleGr) {
      return !sn_collision.CollisionAPI.willBeReplacedOnUpgrade(roleGr.sys_update_name + '');
  },

  _isUnlimitedRole: function(roleName) {
      return this.arrayUtil.contains(this.UNLIMITED_ROLE_NAMES, roleName);
  },

  type: 'RoleInfoService'
};

Sys ID

c1c8c60e430121102aeb1ca57bb8f254

Offical Documentation

Official Docs: