qik.access.js

/**
 * Creates a new Access Module instance.
 * This module provides a number of helper functions for managing and understanding permissions of the current
 * application or user
 * @alias access
 * @constructor
 * @hideconstructor
 * @param {QikCore} qik A reference to the parent instance of the QikCore module. This module is usually created by a QikCore instance that passes itself in as the first argument.
 */

var QikAccess = function (QikCore) {
  if (!QikCore.auth) {
    throw new Error(
      `Can't Instantiate QikAccess before QikAccess has been initialized`,
    );
  }

  if (!QikCore.utils) {
    throw new Error(
      `Can't Instantiate QikAccess before QikCore.utils has been initialized`,
    );
  }

  //////////////////////////////////

  var service = {};

  ///////////////////////////////////////////////

  /**
   * @alias access.hasFeature
   * @description Checks if a user has a specific feature enabled for their organisation
   * @param  {Object} user The user to check
   * @param  {String} feature The feature to check is enabled/disabled
   * @example
   * const user = sdk.auth.getCurrentUser();
   * const formsFeatureIsEnabled =  sdk.access.hasFeature(user, 'forms');
   */
  service.hasFeature = function (user, feature) {
    const features = user?.organisation?.features || {};
    if (features.all) {
      return true;
    }
    return Boolean(features[feature]);
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.isAdministrator
   * @description Check if a user object is an administrator
   * @param  {Object} user The user session to check
   * @example
   * const userIsAnAdministrator = sdk.access.isAdministrator({firstName:'Jeffrey', lastName:'Winger', ...});
   */
  service.isAdministrator = function (user) {
    return user && user?.userType == "administrator";
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.getAllDescendants
   * @description Get all children of a specified scope
   * @param  {Object} scope A scope object with child scopes
   * @example
   * const childScopes = sdk.access.getAllDescendants({title:'Global', children:[{title:'Australia', ...}, {title:'New Zealand', ...}]});
   */
  service.getAllDescendants = function (scope) {
    //Start by including the scope id
    var results = [];

    addScope(scope);

    function addScope(entry) {
      //Add this scope to the results
      var id = QikCore.utils.id(entry);
      results.push(id);

      (entry.children || []).forEach(function (child) {
        addScope(child);
      });
    }
    return results;
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.hashPermissions
   * @description Get a lookup of all permissions a user has been granted
   * @param  {Object} user A user session object
   * @example
   * const permissions = sdk.access.hashPermissions({firstName:'Mighty', lastName:'Mouse', permissionSets:[...], ...});
   */
  service.hashPermissions = function (user) {
    var results = {};

    for (var key in user?.permissions || {}) {
      //Get the permission set
      var permissionSet = user?.permissions[key];

      //Get all Scope ids from the permission set
      var allIDs = service.getAllDescendants(permissionSet);

      //For each permission defined in the set
      (permissionSet.permissions || []).forEach(function (permissionString) {
        //Include the top scope
        if (!results[permissionString]) {
          results[permissionString] = [];
        }
        results[permissionString] = [
          ...new Set(results[permissionString].concat(allIDs)),
        ];
      });
    }

    ///////////////////////////////////////////////

    return results;
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.allUserScopes
   * @description Return an array of ids of all scopes that a given user has been granted a permission in
   * @param  {Object} user A user session object
   * @example
   * const permissions = sdk.access.allUserScopes({firstName:'Mighty', lastName:'Mouse', permissionSets:[...], ...});
   *
   * // Would result in:
   * ['61eca4746971e75c1fc670cf', '61eca4746971e75c1fc670ca', '77eca4746971e75c1fc670cf'],
   */
  service.allUserScopes = function (user) {
    var collected = {};
    var permissions = user?.permissions || {};

    ////////////////////////

    function pluckScope(scopeObject) {
      if (!scopeObject) {
        return;
      }

      var scopeID = QikCore.utils.id(scopeObject);
      if (!scopeID) {
        return;
      }

      //Add to the collection
      collected[scopeID] = 1;

      //If there are children, then recursively pluck the ids
      if (scopeObject.children && scopeObject.children.length) {
        scopeObject.children.forEach(function (scope) {
          pluckScope(scope);
        });
      }
    }

    ////////////////////////

    for (var scopeID in permissions) {
      pluckScope(permissions[scopeID]);
    }

    ////////////////////////

    return Object.keys(collected);
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.actionableScopes
   * @description Return an array of all scopes that a given user can perform a specified action in
   * @param  {Object} user A user session object
   * @param  {String} action The action
   * @param  {String} definition The defined type of item the action will be performed on
   * @param  {String} type The basic type of item the action will be performed on
   * @param  {Object} options Additional options and parameters
   * @example
   * const user = sdk.auth.getCurrentUser();
   * const canCreateInScopes = sdk.access.actionableScopes(user, 'create', 'car', 'article');
   * const canDeleteInScopes = sdk.access.actionableScopes(user, 'delete', 'profile');
   *
   * // Would result in something like:
   * ['61eca4746971e75c1fc670cf', '61eca4746971e75c1fc670ca', '77eca4746971e75c1fc670cf'],
   */
  service.actionableScopes = function (
    user,
    action,
    definition,
    type,
    options,
  ) {
    options = options || {};

    /////////////////////////

    var validScopes = [];

    if (user?.userType == "administrator") {
      validScopes = service.allUserScopes(user, options);
    } else {
      var permissionsLookup = service.hashPermissions(user);

      var checkTypeName = definition || type;
      if (!checkTypeName) {
        return validScopes;
      }

      let definitionScopes = !definition
        ? []
        : permissionsLookup[`${definition}.${action}`] || [];
      let typeScopes = !type
        ? []
        : permissionsLookup[`${type}.${action}`] || [];

      validScopes = [...definitionScopes, ...typeScopes];
    }

    /////////////////////////

    return validScopes || [];
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.isOwner
   * @description Return whether or not a user is considered an Owner of a specified item
   * @param  {Object} user The user to check
   * @param  {Object} item The item
   * @example
   * const user = sdk.auth.getCurrentUser();
   * const item = {title:'A piece of content', meta:{personaAuthor:'61eca4746971e75c1fc670ca'}, ...}
   *
   * // Returns true if the user owns the item
   * const isTheOwner = sdk.access.isOwner(user, item);
   */
  service.isOwner = function (user, item) {
    var userID = QikCore.utils.id(user);
    var personaID = QikCore.utils.id(user?.persona);

    var metaObject = item.meta;
    if (!metaObject) {
      return false;
    }

    /////////////////////////////////////////////

    var userOwners = metaObject.userOwners || [];

    var users = QikCore.utils.ids([...userOwners, metaObject.userAuthor]);
    var userHash = QikCore.utils.hash(users);

    if (userHash[userID]) {
      return true;
    }

    /////////////////////////////////////////////

    var personaOwners = metaObject.personaOwners || [];

    var personas = QikCore.utils.ids([
      ...personaOwners,
      metaObject.personaAuthor,
    ]);
    var personaHash = QikCore.utils.hash(personas);

    if (personaHash[personaID]) {
      return true;
    }

    const isAssignee = service.isAssignee(user, item);

    return isAssignee;
  };

  /**
   * @alias access.isAssignee
   * @description Return whether or not a user is an assignee of a specified item
   * @param  {Object} user The user to check
   * @param  {Object} item The item
   * @example
   * const user = sdk.auth.getCurrentUser();
   * const item = {title:'A workflow card', assignees:['61eca4746971e75c1fc670ca'], ...}
   *
   * // Returns true if the user owns the item
   * const isAssignee = sdk.access.isAssignee(user, item);
   */
  service.isAssignee = function (user, contentItem) {
    // Check the user profile ids
    const profileIDs = QikCore.utils.ids(user?.profiles);
    if (!profileIDs.length) {
      return false;
    }

    // Check if the content item has assignees set
    const assigneeIDs = QikCore.utils.ids(contentItem.assignees);

    if (!assigneeIDs.length) {
      return false;
    }

    // Return the intersection of assignees and profile ids
    const set = new Set(assigneeIDs);
    const matches = profileIDs.filter((item) => set.has(item));
    const isAssignee = !!matches.length;

    return isAssignee;
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.allPermissions
   * @description Returns an array of all permissions a given user has access to
   * @param  {Object} user The user to check
   * @example
   * const user = sdk.auth.getCurrentUser();
   * const permissions =  sdk.access.allPermissions(user);
   *
   * // Returns
   * ['image.viewany', 'testimonial.viewany', 'testimonial.viewfield.title', ...]
   */
  service.allPermissions = function (user) {
    var allPermissions = Object.values(user?.permissions || {})
      .map(function (permissionScope) {
        return permissionScope.permissions;
      })
      .flat();

    return allPermissions;
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.allPermissionTypes
   * @description Returns an array of content types a given user has permission to interact with
   * @param  {Object} user The user to check
   * @param  {Boolean} asHash Whether to retrieve the result as a keyed object hash, by default
   * the response will be an array
   * @example
   * const user = sdk.auth.getCurrentUser();
   *
   * // Returns ['image', 'testimonial']
   * const types = sdk.access.allPermissionTypes(user);
   *
   * // Returns {image:true, testimonial:true}
   * const types = sdk.access.allPermissionTypes(user, true);
   */
  service.allPermissionTypes = function (user, asHash) {
    if (asHash) {
      return service.allPermissions(user).reduce(function (set, string) {
        var type = string.split(".")[0];
        set[type] = true;
        return set;
      }, {});
    } else {
      return service.allPermissions(user).map(function (string) {
        return string.split(".")[0];
      });
    }
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.canKnowOf
   * @description Checks if a user has any permissions relating to a specified type of content
   * @param  {Object} user The user to check
   * @param  {String} typeKey The type or definition to check
   * @param  {Object} options Additional options and parameters
   * @param  {Object} options.cache Provide an existing object to use as an in memory cache, can be useful to increase performance if running this function in a large for/while loop
   * @example
   * const user = sdk.auth.getCurrentUser();
   * const canAccessImageSection =  sdk.access.canKnowOf(user, 'image');
   * const canAccessCustomerUploadSection =  sdk.access.canKnowOf(user, 'customerUpload');
   */
  service.canKnowOf = function (user, typeKey, options) {
    options = options || {};
    options.cache = options.cache || {};

    //Let us know if there are any permissions for this user
    return service.allPermissionTypes(user, true)[typeKey];
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.checkActionAccess
   * @description Helpful function for checking whether a given user has access to do a given action on a specified item
   * @param  {Object} user The user to check
   * @param  {Object} item The item to check
   * @param  {String} ANY_ACTION The 'any' action to check
   * @param  {String} OWN_ACTION The 'owned' action to check
   * @param  {Object} options Additional options and parameters
   * @param  {Object} options.cache Provide an existing object to use as an in memory cache, can be useful to increase performance if running this function in a large for/while loop
   * @example
   * const user = sdk.auth.getCurrentUser();
   * const canEditItem =  sdk.access.checkActionAccess(user, {_id:'61eca4746971e75c1fc670ca', meta:{...}}, 'editany', 'editown');
   * const canViewItem =  sdk.access.checkActionAccess(user, {_id:'61eca4746971e75c1fc670ca', meta:{...}}, 'viewany', 'viewown');
   *
   * // Example of looping through a large array of items
   * const largeArrayOfItems = [{_id:'...', title...}, {_id:'...', title...}...]
   *
   * // Create an in memory cache object outside of the loop
   * const cache = {}
   *
   * const filtered = largeArrayOfItems.filter(function(item) {
   *    return sdk.access.checkActionAccess(user, item, 'editany', 'editown', {cache});
   * })
   */
  service.checkActionAccess = function (
    user,
    item,
    ANY_ACTION,
    OWN_ACTION,
    options,
  ) {
    options = options || {};
    options.cache = options.cache || {};

    //Allow everything if we're a super user
    if (service.isAdministrator(user)) {
      return true;
    }

    //////////////////////////////////

    var type = item.meta?.type;
    var definition = item.meta?.definition || type;

    if (!definition) {
      return false;
    }

    //////////////////////////////////

    //The user should be able to list any scopes that they have
    //permission to do things in.
    if (ANY_ACTION == "listany" || OWN_ACTION == "listown") {
      if (type === "scope") {
        var itemID = QikCore.utils.id(item);
        return !!user.permissions[itemID];
      }
    }

    //////////////////////////////////

    var itemScopes = QikCore.utils.ids(item.meta.scopes || []);

    //Scopes don't have meta.scopes, instead use the scope's
    //path (ancestors) plus the scope's own ID
    if (type === "scope") {
      itemScopes = [
        ...QikCore.utils.ids(item.path || []),
        QikCore.utils.id(item),
      ];
    }

    //////////////////////////////////

    var actionAnyScopesHash;
    var anyCacheKey = `access-${user?.cacheKey}-${ANY_ACTION}-${definition}-${type}`;

    if (options.cache[anyCacheKey]) {
      actionAnyScopesHash = options.cache[anyCacheKey];
    } else {
      //Check if we can action this type of thing in any scopes
      var actionAnyScopes = QikCore.utils.ids(
        service.actionableScopes(user, ANY_ACTION, definition, type, options),
      );

      //Get a fash hash of the scopes
      actionAnyScopesHash = QikCore.utils.hash(actionAnyScopes);
      options.cache[anyCacheKey] = actionAnyScopesHash;
    }

    //////////////////////////////////

    //If we have data about what they can do
    if (actionAnyScopesHash) {
      //Check if the item is in any of the scopes the user can access
      var isInAnyScopes = itemScopes.some(function (scopeID) {
        return actionAnyScopesHash[scopeID];
      });

      if (isInAnyScopes) {
        return true;
      }
    }

    //////////////////////////////////

    //Check if the user owns this content
    var isOwner = service.isOwner(user, item);

    //If the user is the owner
    if (isOwner) {
      var actionOwnScopesHash;
      var ownCacheKey = `access-${user?.cacheKey}-${OWN_ACTION}-${definition}-${type}`;

      if (options.cache[ownCacheKey]) {
        actionOwnScopesHash = options.cache[ownCacheKey];
      } else {
        //Check if we can action this type of thing in any scopes
        var actionOwnScopes = QikCore.utils.ids(
          service.actionableScopes(user, OWN_ACTION, definition, type, options),
        );

        //Get a fash hash of the scopes
        actionOwnScopesHash = QikCore.utils.hash(actionOwnScopes);
        options.cache[ownCacheKey] = actionOwnScopesHash;
      }

      //If we have data about what they can do
      if (actionOwnScopesHash) {
        //Check if the item is in any of the scopes the user can access
        var isInOwnScopes = itemScopes.some(function (scopeID) {
          return actionOwnScopesHash[scopeID];
        });

        if (isInOwnScopes) {
          return true;
        }
      }
    }

    //////////////////////////////////

    //No access by default
    return false;
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.has
   * @description Helpful function for checking whether a given user has a specified permission
   * @param  {Object} user The user to check
   * @param  {String} permission The permission the user may have been granted
   * @example
   * const user = sdk.auth.getCurrentUser();
   * const isAllowedToCreateItems = sdk.access.has(user, 'video.create');
   */
  service.has = function (user, permission) {
    //Allow everything if we're a super user
    if (service.isAdministrator(user)) {
      return true;
    }

    var permissionsLookup = service.hashPermissions(user);
    return permissionsLookup[permission];
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.canCreate
   * @description Helpful function for checking whether a given user can create a specified type of item
   * @param  {Object} user The user to check
   * @param  {String} definition The defined type key
   * @param  {String} type The basic type key
   * @param  {Object} options Additional options and parameters
   * @param  {Object} options.cache Provide an existing object to use as an in memory cache, can be useful to increase performance if running this function in a large for/while loop
   * @example
   * const user = sdk.auth.getCurrentUser();
   * const userCanCreateShortFilms = sdk.access.canCreate(user, 'shortFilm', 'video');
   */
  service.canCreate = function (user, definition, type, options) {
    options = options || {};
    options.cache = options.cache || {};

    //Allow everything if we're a super user
    if (service.isAdministrator(user)) {
      return true;
    }

    var createableScopes = service.actionableScopes(
      user,
      "create",
      definition,
      type,
    );
    return createableScopes.length;
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.canListItem
   * @description Helpful function for checking whether a given user can list a specified item
   * @param  {Object} user The user to check
   * @param  {Object} item The item to check
   * @param  {Object} options Additional options and parameters
   * @param  {Object} options.cache Provide an existing object to use as an in memory cache, can be useful to increase performance if running this function in a large for/while loop
   * @example
   * const user = sdk.auth.getCurrentUser();
   *
   * // Example of looping through a large array of items
   * const largeArrayOfItems = [{_id:'...', title...}, {_id:'...', title...}...]
   *
   * // Create an in memory cache object outside of the loop
   * const cache = {}
   *
   * const filtered = largeArrayOfItems.filter(function(item) {
   *    return sdk.access.canListItem(user, item, {cache});
   * })
   */
  service.canListItem = function (user, item, options) {
    options = options || {};
    options.cache = options.cache || {};

    //Check if we can list the item
    var canList = service.checkActionAccess(
      user,
      item,
      "listany",
      "listown",
      options,
    );

    if (canList) {
      return true;
    }

    //If we can view the item then we should be able to list it also
    var canView = service.canViewItem(user, item, options);
    if (canView) {
      return true;
    }

    return false;
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.canViewItem
   * @description Helpful function for checking whether a given user can view a specified item
   * @param  {Object} user The user to check
   * @param  {Object} item The item to check
   * @param  {Object} options Additional options and parameters
   * @param  {Object} options.cache Provide an existing object to use as an in memory cache, can be useful to increase performance if running this function in a large for/while loop
   * @example
   * const user = sdk.auth.getCurrentUser();
   *
   * // Example of looping through a large array of items
   * const largeArrayOfItems = [{_id:'...', title...}, {_id:'...', title...}...]
   *
   * // Create an in memory cache object outside of the loop
   * const cache = {}
   *
   * const filtered = largeArrayOfItems.filter(function(item) {
   *    return sdk.access.canViewItem(user, item, {cache});
   * })
   */
  service.canViewItem = function (user, item, options) {
    options = options || {};
    options.cache = options.cache || {};

    //Check if we can view the item
    var canView = service.checkActionAccess(
      user,
      item,
      "viewany",
      "viewOwn",
      options,
    );
    if (canView) {
      return true;
    }

    //If we can edit the item then we should be able to view it also
    var canEdit = service.canEditItem(user, item, options);
    if (canEdit) {
      return true;
    }

    return false;
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.canEditItem
   * @description Helpful function for checking whether a given user can edit a specified item
   * @param  {Object} user The user to check
   * @param  {Object} item The item to check
   * @param  {Object} options Additional options and parameters
   * @param  {Object} options.cache Provide an existing object to use as an in memory cache, can be useful to increase performance if running this function in a large for/while loop
   * @example
   * const user = sdk.auth.getCurrentUser();
   *
   * // Example of looping through a large array of items
   * const largeArrayOfItems = [{_id:'...', title...}, {_id:'...', title...}...]
   *
   * // Create an in memory cache object outside of the loop
   * const cache = {}
   *
   * const filtered = largeArrayOfItems.filter(function(item) {
   *    return sdk.access.canEditItem(user, item, {cache});
   * })
   */
  service.canEditItem = function (user, item, options) {
    options = options || {};
    options.cache = options.cache || {};

    var allowed = service.checkActionAccess(
      user,
      item,
      "editany",
      "editown",
      options,
    );

    return allowed;
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.canDeleteItem
   * @description Helpful function for checking whether a given user can delete a specified item
   * @param  {Object} user The user to check
   * @param  {Object} item The item to check
   * @param  {Object} options Additional options and parameters
   * @param  {Object} options.cache Provide an existing object to use as an in memory cache, can be useful to increase performance if running this function in a large for/while loop
   * @example
   * const user = sdk.auth.getCurrentUser();
   *
   * // Example of looping through a large array of items
   * const largeArrayOfItems = [{_id:'...', title...}, {_id:'...', title...}...]
   *
   * // Create an in memory cache object outside of the loop
   * const cache = {}
   *
   * const filtered = largeArrayOfItems.filter(function(item) {
   *    return sdk.access.canDeleteItem(user, item, {cache});
   * })
   */
  service.canDeleteItem = function (user, item, options) {
    options = options || {};
    options.cache = options.cache || {};

    return service.checkActionAccess(
      user,
      item,
      "deleteany",
      "deleteown",
      options,
    );
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.canRestoreItem
   * @description Helpful function for checking whether a given user can restore a specified item
   * @param  {Object} user The user to check
   * @param  {Object} item The item to check
   * @param  {Object} options Additional options and parameters
   * @param  {Object} options.cache Provide an existing object to use as an in memory cache, can be useful to increase performance if running this function in a large for/while loop
   * @example
   * const user = sdk.auth.getCurrentUser();
   *
   * // Example of looping through a large array of items
   * const largeArrayOfItems = [{_id:'...', title...}, {_id:'...', title...}...]
   *
   * // Create an in memory cache object outside of the loop
   * const cache = {}
   *
   * const filtered = largeArrayOfItems.filter(function(item) {
   *    return sdk.access.canRestoreItem(user, item, {cache});
   * })
   */
  service.canRestoreItem = function (user, item, options) {
    options = options || {};
    options.cache = options.cache || {};

    return service.checkActionAccess(
      user,
      item,
      "restoreany",
      "restoreown",
      options,
    );
  };

  ///////////////////////////////////////////////

  /**
   * @alias access.canEraseItem
   * @description Helpful function for checking whether a given user can erase a specified item
   * @param  {Object} user The user to check
   * @param  {Object} item The item to check
   * @param  {Object} options Additional options and parameters
   * @param  {Object} options.cache Provide an existing object to use as an in memory cache, can be useful to increase performance if running this function in a large for/while loop
   * @example
   * const user = sdk.auth.getCurrentUser();
   *
   * // Example of looping through a large array of items
   * const largeArrayOfItems = [{_id:'...', title...}, {_id:'...', title...}...]
   *
   * // Create an in memory cache object outside of the loop
   * const cache = {}
   *
   * const filtered = largeArrayOfItems.filter(function(item) {
   *    return sdk.access.canEraseItem(user, item, {cache});
   * })
   */
  service.canEraseItem = function (user, item, options) {
    options = options || {};
    options.cache = options.cache || {};

    return service.checkActionAccess(
      user,
      item,
      "eraseany",
      "eraseown",
      options,
    );
  };

  return service;
};

///////////////////////////////////////////////////////////////////////////////

export default QikAccess;