@qikdev/sdk
Version:
Promise based Javascript SDK
836 lines (707 loc) • 26.7 kB
JavaScript
/**
* 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;