Name

sn_entitlement.LicenseRoleDiscoveryService

Description

No description available

Script

var LicenseRoleDiscoveryService = Class.create();
LicenseRoleDiscoveryService.prototype = {
  initialize: function () {
      this.licenseRoleDiscoveredDao = new LicenseRoleDiscoveredDao();
      this.roleAnalyzerService = new RoleAnalyzerService();
  },

  /**
   * Analyze custom roles using role analyzer and insert/update information about role type in license_role_discovered 
   */
  analyzeCustomRoles: function() {
      const analysisResult = this.roleAnalyzerService.analyzeCustomRoles();

      for (const [roleName, appDetail] of Object.entries(analysisResult.app_detail))
          this._processRole(roleName, appDetail);
      for (const roleName of this._findRolesInTableOtherThan(Object.keys(analysisResult.app_detail)))
          this._processRole(roleName, {});
  },

  /**
   * Return role information for the roles which have been changed since last time applyRoleDataChanges was called
   * @return {Map} a map from roleName to an array containing two arrays [ [p1, p2...], [n1, n2, ...]]
   * where first array has previously synched LicenseRoleDiscoveredData objects,
   * and second array has new LicenseRoleDiscoveredData objects for those roles 
   * Note that either of these could be empty, but never both.
   */
  fetchChangedRoleData: function() {
      // first lookup un-synced records
      const changedRoleData = this.licenseRoleDiscoveredDao.lookupByStateOtherThan([this.licenseRoleDiscoveredDao.SYNCED]);

      // collect all unique roles names from them
      const roleNames = Array.from(new Set(changedRoleData.map(roleData => roleData.roleName)));

      // lookup synced records for those roles to place as previous
      const existingRoleData = this.licenseRoleDiscoveredDao.lookupForRolesByState(roleNames, [this.licenseRoleDiscoveredDao.SYNCED]);

      const result = new Map(roleNames.map(name => [name, [[],[]]]));

      // collect all previous first
      existingRoleData.forEach(roleData => {
          const resultArrays = result.get(roleData.roleName);
          resultArrays[0].push(roleData);
      });
  
      // collect changed info next
      changedRoleData.forEach(roleData => {
          const resultArrays = result.get(roleData.roleName);
          // add New/Error but if deleted, keep this empty
          if (roleData.state === this.licenseRoleDiscoveredDao.NEW || roleData.state === this.licenseRoleDiscoveredDao.ERROR)
              resultArrays[1].push(roleData);
      });

      return result;
  },

  /**
   * Find changed role information for specified roleName since last time applyRoleDataChanges* was called and apply
   * the changes by changing state to sync or error or delete records as appropriate.
   * @param {string} roleName name of role to apply changes to
   * @param {boolean} uploadSuccess true indicates that information has been synced successfully and to mark as synced, false otherwise
   */
  applyRoleDataChanges: function(roleName, uploadSuccess) {
      const existingRoleData = this.licenseRoleDiscoveredDao.lookupForRole([roleName]);
      const previous = existingRoleData.filter(obj => obj.state === this.licenseRoleDiscoveredDao.SYNCED);
      const changed = existingRoleData.filter(obj => obj.state !== this.licenseRoleDiscoveredDao.SYNCED && obj.state != this.licenseRoleDiscoveredDao.DELETED);

      // case 0: success=false, mark changed as error
      // case 1: if previous is empty, mark all changed as Synced
      // case 2: if previous is not empty, changed is empty, delete all previous, and delete the DELETED record
      // case 3: if previous is not empty, changed is not empty, delete previous, and mark all changed as Synched
      if (!uploadSuccess)
          this._markError(roleName, changed);
      else if (previous.length === 0)
          this._markSynced(roleName, changed);
      else if (changed.length === 0) {
          const deleted = existingRoleData.filter(obj => obj.state == this.licenseRoleDiscoveredDao.DELETED);
          const previousWithDeleted = [...previous, ...deleted];
          this._deleteRoleData(roleName, previousWithDeleted, 'removed role')
      } else {
          this._deleteRoleData(roleName, previous, 'changed');
          this._markSynced(roleName, changed);
      }
  },

  /**
   * Find changed role information since last time applyRoleDataChanges* was called and apply
   * the changes by changing state to error
   */
  applyRoleDataChangesAsError: function() {
      const changedRoleData = this.fetchChangedRoleData();
      const ids = [...changedRoleData]
                          .map(value => value[1][1])
                          .flat()
                          .filter(roleData => roleData.state != this.licenseRoleDiscoveredDao.DELETED)
                          .map(roleData => roleData.id);
      gs.info(`Marking ${ids.length} records as error`);
      this.licenseRoleDiscoveredDao.markState(ids, this.licenseRoleDiscoveredDao.ERROR);
  },

  _processRole: function(roleName, appDetail) {
      const existingRoleData = this.licenseRoleDiscoveredDao.lookupForRole([roleName]);
      existingRoleData.sort(this._compareRoleData.bind(this));

      const existingSynced = existingRoleData.filter(obj => obj.state === this.licenseRoleDiscoveredDao.SYNCED);
      const existingNonSynced = existingRoleData.filter(obj => obj.state !== this.licenseRoleDiscoveredDao.SYNCED);

      const newRoleData = this._buildRoleData(roleName, appDetail);
      newRoleData.sort(this._compareRoleData.bind(this));

      // case 0: newRoleData is empty -- remove existingNonSynced and add record with deleted state if there are synced records
      // case 1: existingSynced matches newRoleData --- remove existingNonSynced  (COMMON CASE)
      // case 2: existingSynced does not match newRoleData 
      //         case 21: if existingNonSynced matches newRoleData --- do nothing (this can be New records or Error records)
      //         case 22: if existingNonSynced does not match newRoleData, remove existingNonSynced, create newRoleData

      if (newRoleData.length === 0) {
          this._deleteRoleData(roleName, existingNonSynced, 'non-synced');
          if (existingSynced.length > 0)
              this._addDeletedRecord(roleName);
      } else if (this._roleDataArraysEquivalent(existingSynced, newRoleData)) {
          this._deleteRoleData(roleName, existingNonSynced, 'non-synced');
      } else if (!this._roleDataArraysEquivalent(existingNonSynced, newRoleData)) {
          this._deleteRoleData(roleName, existingNonSynced, 'non-synced');
          this._addRoleData(roleName, newRoleData);
      }
  },

  _findRolesInTableOtherThan: function(roleNames) {
      return this.licenseRoleDiscoveredDao.lookupRolesOtherThan(roleNames);
  },

  /**
   * Look at per role output of role analyzer return an array of LicenseRoleDiscoveredData which can be inserted
   * into the license_role_discovered table
   */
  _buildRoleData: function(roleName, appDetail) {
      return Object.values(appDetail)
          .filter(roleInApplication => roleInApplication.appId !== 'platform')
          .map(roleInApplication => this._toLicenseRoleDiscoveredData(roleName, roleInApplication));
  },

  /**
   * Delete records for specified roleName
   * @param {string} roleName name of role to which role data applies
   * @param {array} roleData array of LicenseRoleDiscoveredData where id is taken as the sys_id of the record
   * @param {string} description adjective to apply to deleted records in log
   */
  _deleteRoleData : function(roleName, roleData, description) {
      const ids = roleData.map(obj => obj.id);
      if (ids.length == 0)
          return;
      gs.info(`Deleting ${ids.length} ${description} records for role ${roleName}`);
      this.licenseRoleDiscoveredDao.deleteRecords(ids);
  },

  /**
   * Add a record in license_role_discovered where state=deleted for specified roleName
   * @param {string} roleName 
   */
  _addDeletedRecord: function(roleName) {
      const roleData = new LicenseRoleDiscoveredData(
          null,
          roleName,
          'requestor',
          true,
          this.licenseRoleDiscoveredDao.DELETED,
          'deleted',
          'unknown',
          'global'
      );
      gs.info(`Adding deleted record for role ${roleName}`);
      this.licenseRoleDiscoveredDao.insertOrUpdate(roleData);
  },

  /**
   * Add records for specified roleName
   * @param {string} roleName name of role to which role data applies
   * @param {array} newRoleData array of LicenseRoleDiscoveredData objects
   */
  _addRoleData: function(roleName, newRoleData) {
      gs.info(`Adding ${newRoleData.length} records for role ${roleName}`);
      newRoleData.forEach(roleData => this.licenseRoleDiscoveredDao.insertOrUpdate(roleData));
  },

  /**
   * Mark records for specified roleName as state=Synced in license_role_discovered
   * @param {string} roleName name of the role
   * @param {array} changedRoleData array of LicenseRoleDiscoveredData objects where id is taken as the sys_id of the record
   */
  _markSynced: function(roleName, changedRoleData) {
      const ids = changedRoleData.map(roleData => roleData.id);
      gs.info(`Marking ${ids.length} records as synced for role ${roleName}`);
      this.licenseRoleDiscoveredDao.markState(ids, this.licenseRoleDiscoveredDao.SYNCED);
  },

  /**
   * Mark records for specified roleName as state=Error in license_role_discovered
   * @param {string} roleName name of the role
   * @param {array} changedRoleData array of LicenseRoleDiscoveredData objects where id is taken as the sys_id of the record
   */
  _markError: function(roleName, changedRoleData) {
      const ids = changedRoleData.map(roleData => roleData.id);
      gs.info(`Marking ${ids.length} records as for role ${roleName}`);
      this.licenseRoleDiscoveredDao.markState(ids, this.licenseRoleDiscoveredDao.ERROR);
  },

  _toLicenseRoleDiscoveredData: function(roleName, roleInApplication) {
      const appId = this._inferAppId(roleInApplication);

      const isCustom = true;
      const state = this.licenseRoleDiscoveredDao.NEW;

      return  new LicenseRoleDiscoveredData(
          null,
          roleName,
          roleInApplication.detectedType.roleType,
          isCustom,
          state,
          roleInApplication.detectedType.code,
          appId,
          roleInApplication.scope
      );
  },

  _inferAppId: function(roleInApplication) {
      const source = roleInApplication.source.primary;
      // if appId is
      //  a. special fallthrough value 'unk' (for global applications which can't be guessed to be platform)
      //  b. special fallthrough value 'global' with source='App Family' (for global tables where package is empty)
      //  c. or source is a scoped app
      // take (licensable) application as unknown
      if (roleInApplication.appId === 'unk' || source.toLowerCase() === 'scope' ||
          (roleInApplication.appId === 'global' && source.toLowerCase() === 'app family'))
          return 'unknown';
      return roleInApplication.appId;
  },

  /**
   * Return numeric value which indicates lexicographical ordering between input LicenseRoleDiscoveredData objects
   * Comparison is based on (roleType, application, scope)
   * @param {object} lhs 
   * @param {object} rhs 
   * @returns {integer} -1 if lhs<rhs, 1 if lhs>rhs, 0 otherwise
   */
  _compareRoleData: function(lhs, rhs) {
      // simple lexigraphical ordering based on (roleType, application, scope)
      let diff = this._compare(lhs.roleType, rhs.roleType);
      if (diff != 0)
          return diff;
      diff = this._compare(lhs.application, rhs.application);
      if (diff != 0)
          return diff;
      return this._compare(lhs.scope, rhs.scope);
  },

  /**
   * Return numeric value which indicates lexicographical ordering between string arguments
   */
  _compare: function(lhs, rhs) {
      if (lhs < rhs)
          return -1;
      if (lhs > rhs)
          return 1;
      return 0;
  },
  /**
   * Compare two sorted arrays of LicenseRoleDiscoveredData to see if they hold equivalent data as determined by this._compareRoleData
   * @param {array} lhs sorted array of LicenseRoleDiscoveredData 
   * @param {array} rhs sorted array of LicenseRoleDiscoveredData
   * @return {boolean} 
   */
  _roleDataArraysEquivalent: function(lhs, rhs) {
      if (lhs.length != rhs.length)
          return false;

      for (let i = 0; i < lhs.length; ++i) {
          if (this._compareRoleData(lhs[i], rhs[i]) != 0)
              return false;
      }
      return true;
  },

  type: 'LicenseRoleDiscoveryService'
};

Sys ID

2d6c735ed4a12910f877ce4a4aac695f

Offical Documentation

Official Docs: