Name

sn_entitlement.AclAnalyzer

Description

Mechanism to look at an acl record and classifiy it into an enumerated type used for further analysis. Note that this does not return multiple attributes, just a single classification.

Script

/**
* Mechanism to look at an acl record and classifiy it into an enumerated type used for further analysis.
* Note that this does not return multiple attributes, just a single classification.
*/
var AclAnalyzer = Class.create();
AclAnalyzer.prototype = {
  // enumeration of ACL types
  ACL_NOOP: 'n',
  ACL_UNRESTRICTED: 'u',
  ACL_RESTRICTED: 'r',
  ACL_USER_RESTRICTED: 'ur',
  ACL_APPROVER_RESTRICTED: 'a',
  ACL_UNKNOWN: 'ukn',

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

  /**
   * getAclTypes
   * @returns {array} array of all enumerated ACL types
   */
  getAclTypes: function() {
      return [this.ACL_UNRESTRICTED, this.ACL_RESTRICTED, this.ACL_APPROVER_RESTRICTED, this.ACL_USER_RESTRICTED, this.ACL_UNKNOWN, this.ACL_NOOP];
  },

  /**
   * getACLType
   * Determine the ACL type for the ACL record in specified aclRecordIterator based on the condition and script
   * @param {object} aclRecordIterator
   * @returns {string} value from ACL type enumeration
   */
  getACLType: function (aclRecordIterator) {
      var conditionStr = aclRecordIterator.getCondition();
      var scriptStr = this._removeComments(aclRecordIterator.getScript());

      var aclType;
      if (this._isNoOpCondition(scriptStr)) {
          aclType = this.ACL_NOOP;
      } else if (this._isKnownUnknownCondition(scriptStr)) {
          aclType = this.ACL_UNKNOWN;
      } else if (gs.nil(conditionStr) && gs.nil(scriptStr)) {
          aclType = this.ACL_UNRESTRICTED;
      } else if (this._containsCondition(true, conditionStr) || this._containsCondition(true, scriptStr)) {
          aclType = this.ACL_APPROVER_RESTRICTED;
      } else if (this._containsCondition(false, conditionStr) || this._containsCondition(false, scriptStr)) {
          aclType = this.ACL_USER_RESTRICTED;
      } else {
          aclType = this.ACL_RESTRICTED;
      }
      return aclType;
  },

  /**
   * getACLScriptAttributes
   * Return digested attrbutes of the script in ACL record in specified aclRecordIterator which can be used
   * to make finer grained decisions about ACL type
   * @param {object} aclRecordIterator
   * @returns {object}
   */
  getACLScriptAttributes: function (aclRecordIterator) {
      var scriptStr = aclRecordIterator.getScript();
      var hasComment = scriptStr.includes('/*');
      scriptStr = this._removeComments(scriptStr);

      return {
          'hasClass': scriptStr.includes(' new '),
          'hasComment': hasComment,
          'hasHasRole': scriptStr.includes('hasRole'),
          'hasCanRead': scriptStr.includes('.canRead('),
          'hasCanWrite': scriptStr.includes('.canWrite('),
          'hasIf': scriptStr.includes('if(') || scriptStr.includes('if (') ,
          'hasOr': scriptStr.includes('||'),
          'hasIsMemberOf': scriptStr.includes('isMemberOf')
      }
  },

  /**
   * _isNoOpCondition
   * Check the string to identify no op operations.
   * @param {string} str
   * @returns boolean
   */
  _isNoOpCondition: function (str) {
      str = str.replace(/\s/g,"");
      var noop = [];
      noop.push("answer=false;");
      noop.push("false;");
      noop.push("answer=false");
      noop.push("0;");
      return this.arrayUtil.contains(noop, str);
  },


  /**
   * _isKnownUnknownCondition
   * Identify conditions that have common patterns but are not cleary identifable and need to be ignored
   * @param {*} str
   */
  _isKnownUnknownCondition: function (str) {
      return str.match(/current\.\S*can(Write|Read)\(\)/g);
  },

  /**
  * _containsUserCondition
  * Identify if the string (condition or string) contains some user identifying code
  * @param {string} isApprover true if approver, false if user
  * @param {string} scriptString The condition to look at
  * @returns boolean Is the string contains some user identifying code
  */
  _containsCondition: function (isApprover, scriptString) {
      var conditions = [];
      var notIdentifying = [];

      if (isApprover) {
          conditions.push('ApproverUtils()');
      } else {
          // covers several variations of this
          conditions.push('gs.getUser');
          conditions.push('gs.user_id()'); // only global, deprecated by getUserId but still used
          conditions.push('gs.userid()'); // only global, deprecated by getUserId but still used
          // CSM
          conditions.push('sn_queryrules'); // CSM Security
          conditions.push('CSMProjectManagementSecurityUtil'); // CSM PPM

          // SPM
          conditions.push('IMIdeaAccessHelper()'); // ITBM Security

          // Platform
          conditions.push('VTBTaskSecurity'); // VTB security
          conditions.push('sn_cd.cd_ContentDelegationUtils()');

          // Uses User Criteria which may be fulfiller but also can be customized based on user so leaving it as requester
          conditions.push('new STTRMModel(current).evaluateWriteSecurity()');
          conditions.push('new STTRMModel(current).evaluateReadSecurity()');

          // PSM
          conditions.push('new SpendTaskControls(current).canShopperReadAcknowledgementTask();');
          conditions.push('new SpendTaskControls(current).canShopperWriteAcknowledgementTask();');
          conditions.push('SpendCommonUtil.getMyShoppingAs');
          conditions.push('getShopAsUser');
          conditions.push('canShopperReadPurchasingTask');
          conditions.push('canShopperViewApprovalPlan');
          conditions.push('canShopperWritePurchasingTaskState');
          conditions.push('canShopperWriteApprovalPlan');
          conditions.push('canShopperWritePurchasingTaskComments');

          // HR
          conditions.push('er_SecurityUtils().hasReadAccess'); // HR Request

          // Legal

          // Unknown
          conditions.push('AgentScheduleUtil'); //
      }

      // generic
      notIdentifying.push('assigned_to'); // if the script is looking at the assigned to, we are assuming the is fulfilling
      notIdentifying.push('assignment_group'); // if the script is looking at the assignment group, we are assuming the is fulfilling

      // CSM
      notIdentifying.push('QueryRuleGenerator().getEncodedQueryForRoles');
      // SPM
      notIdentifying.push('PPMRoleClassMapper.validateAccess('); // SPM's filter for TeamSpaces
      // Legal
      notIdentifying.push('LegalOperationsSecurity().hasRoleExactly'); // used for legal to block
      // notIdentifying.push('additional_assignee_list'); // if the script is looking at the assigned to, we are assuming the is fulfilling // used by LSD but always is paired with assigned_to so this may not be needed


      for (var nc in notIdentifying) {
          // if the condition has what we are looking for, return early since we don't have to look for more.
          if (scriptString.includes(notIdentifying[nc])) {
              return false;
          }
      }

      for (var c in conditions) {
          // if the condition has what we are looking for, return early since we don't have to look for more.
          if (scriptString.includes(conditions[c])) {
              return true;
          }
      }

      // Special cases
      if (scriptString.includes('current.parent') && scriptString.includes('.canRead()')) {
          // Checking the parent of the task for access.  This won't be a driving record since hte parent record will identify the role type so we'll ignore these.
          return true;
      }

      // check for dynamic filters
      var dynamicFilters = this._findDynamicFilters(scriptString);
      // if we didn't find any we are done looking
      if (dynamicFilters.length == 0)
          return false;

      // check if this filter has a non-identifying query
      if (this._findMatchingDynamicFilter(dynamicFilters, notIdentifying))
          return false;

      // check if this filter does something with the user
      // we may want to add more here in the future
      return this._findMatchingDynamicFilter(dynamicFilters, conditions);
  },

  /**
   * Return list of references to a dynamic filter in specified script string
   * @param {string} scriptString
   */
  _findDynamicFilters: function(scriptString) {
      // check for dynamic filters
      var dynamicFilterOptions = [];
      var regex = /DYNAMIC([a-f0-9]{32})/gm;
      var m;

      // loop through matches to find dynamic filters
      while ((m = regex.exec(scriptString)) !== null) {
          if (m.length == 2)
              dynamicFilterOptions.push(m[1]);
      }

      return this.arrayUtil.unique(dynamicFilterOptions);
  },

  _findMatchingDynamicFilter: function(dynamicFilters, conditions) {
      var filterGr = new GlideRecord('sys_filter_option_dynamic');
      filterGr.addQuery('sys_id', 'IN', dynamicFilters);
      var conditionsCause;
      for (var key in conditions) {
          if (!conditionsCause) {
              conditionsCause = filterGr.addQuery('script', 'CONTAINS', conditions[key]);
          } else {
              conditionsCause.addOrCondition('script', 'CONTAINS', conditions[key]);
          }
      }
      filterGr.query();
      return filterGr.hasNext()
  },

  /**
   * removeComments
   * Remove comments from a string
   * @param {string} str 
   * @returns sanitized input
   */
  _removeComments: function (str) {
      return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g,'');
  },    

  type: 'AclAnalyzer'
};

Sys ID

63e78eca430121102aeb1ca57bb8f299

Offical Documentation

Official Docs: