diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index 54a3d379..ffce9b1b 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -36,9 +36,8 @@ jobs: strategy: matrix: meteorRelease: - - '2.3' - - '2.7.3' - - '2.12' + - '2.8.0' + - '2.13.3' # Latest version steps: - name: Checkout code diff --git a/.gitignore b/.gitignore index 497cb990..936ee1e8 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ npm-debug.log smart.lock .idea +.DS_Store docs/ node_modules/ .npm/ diff --git a/package.js b/package.js index 5cd7213e..eef959fd 100644 --- a/package.js +++ b/package.js @@ -8,7 +8,7 @@ Package.describe({ }) Package.onUse(function (api) { - api.versionsFrom(['1.12', '2.3', '2.8.0']) + api.versionsFrom(['2.8.0']) const both = ['client', 'server'] @@ -27,6 +27,7 @@ Package.onUse(function (api) { api.export('Roles') api.addFiles('roles/roles_common.js', both) + api.addFiles('roles/roles_common_async.js', both) api.addFiles('roles/roles_server.js', 'server') api.addFiles([ 'roles/client/debug.js', @@ -43,7 +44,7 @@ Package.onTest(function (api) { 'meteortesting:mocha@2.1.0' ]) - api.versionsFrom('2.3') + api.versionsFrom(['2.3', '2.8.1']) const both = ['client', 'server'] @@ -56,5 +57,7 @@ Package.onTest(function (api) { ], both) api.addFiles('roles/tests/server.js', 'server') + api.addFiles('roles/tests/serverAsync.js', 'server') api.addFiles('roles/tests/client.js', 'client') + api.addFiles('roles/tests/clientAsync.js', 'client') }) diff --git a/roles/.gitignore b/roles/.gitignore index 677a6fc2..964bc064 100644 --- a/roles/.gitignore +++ b/roles/.gitignore @@ -1 +1,2 @@ .build* +.DS_Store diff --git a/roles/roles_common.js b/roles/roles_common.js index f05d6206..3dfcefe3 100644 --- a/roles/roles_common.js +++ b/roles/roles_common.js @@ -706,7 +706,7 @@ Object.assign(Roles, { * - `onlyAssigned`: return only assigned roles and not automatically inferred (like subroles) * - `fullObjects`: return full roles objects (`true`) or just names (`false`) (`onlyAssigned` option is ignored) (default `false`) * If you have a use-case for this option, please file a feature-request. You shouldn't need to use it as it's - * result strongly dependant on the internal data structure of this plugin. + * result strongly dependent on the internal data structure of this plugin. * * Alternatively, it can be a scope name string. * @return {Array} Array of user's roles, unsorted. diff --git a/roles/roles_common_async.js b/roles/roles_common_async.js new file mode 100644 index 00000000..7d3369ca --- /dev/null +++ b/roles/roles_common_async.js @@ -0,0 +1,1324 @@ +/* global Roles */ +import { Meteor } from 'meteor/meteor' +import { Mongo } from 'meteor/mongo' + +/** + * Provides functions related to user authorization. Compatible with built-in Meteor accounts packages. + * + * Roles are accessible throgh `Meteor.roles` collection and documents consist of: + * - `_id`: role name + * - `children`: list of subdocuments: + * - `_id` + * + * Children list elements are subdocuments so that they can be easier extended in the future or by plugins. + * + * Roles can have multiple parents and can be children (subroles) of multiple roles. + * + * Example: `{_id: 'admin', children: [{_id: 'editor'}]}` + * + * The assignment of a role to a user is stored in a collection, accessible through `Meteor.roleAssignment`. + * It's documents consist of + * - `_id`: Internal MongoDB id + * - `role`: A role object which got assigned. Usually only contains the `_id` property + * - `user`: A user object, usually only contains the `_id` property + * - `scope`: scope name + * - `inheritedRoles`: A list of all the roles objects inherited by the assigned role. + * + * @module Roles + */ +if (!Meteor.roles) { + Meteor.roles = new Mongo.Collection('roles') +} + +if (!Meteor.roleAssignment) { + Meteor.roleAssignment = new Mongo.Collection('role-assignment') +} + +/** + * @class Roles + */ +if (typeof Roles === 'undefined') { + Roles = {} // eslint-disable-line no-global-assign +} + +let getGroupsForUserDeprecationWarning = false + +/** + * Helper, resolves async some + * @param {*} arr + * @param {*} predicate + * @returns {Promise} + */ +const asyncSome = async (arr, predicate) => { + for (const e of arr) { + if (await predicate(e)) return true + } + return false +} + +Object.assign(Roles, { + /** + * Used as a global group (now scope) name. Not used anymore. + * + * @property GLOBAL_GROUP + * @static + * @deprecated + */ + GLOBAL_GROUP: null, + + /** + * Create a new role. + * + * @method createRole + * @param {String} roleName Name of role. + * @param {Object} [options] Options: + * - `unlessExists`: if `true`, exception will not be thrown in the role already exists + * @return {Promise} ID of the new role or null. + * @static + */ + createRoleAsync: async function (roleName, options) { + Roles._checkRoleName(roleName) + + options = Object.assign( + { + unlessExists: false + }, + options + ) + + let insertedId = null + + const existingRole = await Meteor.roles.findOneAsync({ _id: roleName }) + + if (existingRole) { + await Meteor.roles.updateAsync( + { _id: roleName }, + { $setOnInsert: { children: [] } } + ) + return null + } else { + insertedId = await Meteor.roles.insertAsync({ + _id: roleName, + children: [] + }) + } + + if (!insertedId) { + if (options.unlessExists) return null + throw new Error("Role '" + roleName + "' already exists.") + } + + return insertedId + }, + + /** + * Delete an existing role. + * + * If the role is set for any user, it is automatically unset. + * + * @method deleteRole + * @param {String} roleName Name of role. + * @returns {Promise} + * @static + */ + deleteRoleAsync: async function (roleName) { + let roles + let inheritedRoles + + Roles._checkRoleName(roleName) + + // Remove all assignments + await Meteor.roleAssignment.removeAsync({ + 'role._id': roleName + }) + + do { + // For all roles who have it as a dependency ... + roles = Roles._getParentRoleNames( + await Meteor.roles.findOneAsync({ _id: roleName }) + ) + + for (const r of await Meteor.roles + .find({ _id: { $in: roles } }) + .fetchAsync()) { + await Meteor.roles.updateAsync( + { + _id: r._id + }, + { + $pull: { + children: { + _id: roleName + } + } + } + ) + + inheritedRoles = await Roles._getInheritedRoleNamesAsync( + await Meteor.roles.findOneAsync({ _id: r._id }) + ) + await Meteor.roleAssignment.updateAsync( + { + 'role._id': r._id + }, + { + $set: { + inheritedRoles: [r._id, ...inheritedRoles].map((r2) => ({ + _id: r2 + })) + } + }, + { multi: true } + ) + } + } while (roles.length > 0) + + // And finally remove the role itself + await Meteor.roles.removeAsync({ _id: roleName }) + }, + + /** + * Rename an existing role. + * + * @method renameRole + * @param {String} oldName Old name of a role. + * @param {String} newName New name of a role. + * @returns {Promise} + * @static + */ + renameRoleAsync: async function (oldName, newName) { + let count + + Roles._checkRoleName(oldName) + Roles._checkRoleName(newName) + + if (oldName === newName) return + + const role = await Meteor.roles.findOneAsync({ _id: oldName }) + + if (!role) { + throw new Error("Role '" + oldName + "' does not exist.") + } + + role._id = newName + + await Meteor.roles.insertAsync(role) + + do { + count = await Meteor.roleAssignment.updateAsync( + { + 'role._id': oldName + }, + { + $set: { + 'role._id': newName + } + }, + { multi: true } + ) + } while (count > 0) + + do { + count = await Meteor.roleAssignment.updateAsync( + { + 'inheritedRoles._id': oldName + }, + { + $set: { + 'inheritedRoles.$._id': newName + } + }, + { multi: true } + ) + } while (count > 0) + + do { + count = await Meteor.roles.updateAsync( + { + 'children._id': oldName + }, + { + $set: { + 'children.$._id': newName + } + }, + { multi: true } + ) + } while (count > 0) + + await Meteor.roles.removeAsync({ _id: oldName }) + }, + + /** + * Add role parent to roles. + * + * Previous parents are kept (role can have multiple parents). For users which have the + * parent role set, new subroles are added automatically. + * + * @method addRolesToParent + * @param {Array|String} rolesNames Name(s) of role(s). + * @param {String} parentName Name of parent role. + * @returns {Promise} + * @static + */ + addRolesToParentAsync: async function (rolesNames, parentName) { + // ensure arrays + if (!Array.isArray(rolesNames)) rolesNames = [rolesNames] + + for (const roleName of rolesNames) { + await Roles._addRoleToParentAsync(roleName, parentName) + } + }, + + /** + * @method _addRoleToParent + * @param {String} roleName Name of role. + * @param {String} parentName Name of parent role. + * @returns {Promise} + * @private + * @static + */ + _addRoleToParentAsync: async function (roleName, parentName) { + Roles._checkRoleName(roleName) + Roles._checkRoleName(parentName) + + // query to get role's children + const role = await Meteor.roles.findOneAsync({ _id: roleName }) + + if (!role) { + throw new Error("Role '" + roleName + "' does not exist.") + } + + // detect cycles + if ((await Roles._getInheritedRoleNamesAsync(role)).includes(parentName)) { + throw new Error( + "Roles '" + roleName + "' and '" + parentName + "' would form a cycle." + ) + } + + const count = await Meteor.roles.updateAsync( + { + _id: parentName, + 'children._id': { + $ne: role._id + } + }, + { + $push: { + children: { + _id: role._id + } + } + } + ) + + // if there was no change, parent role might not exist, or role is + // already a sub-role; in any case we do not have anything more to do + if (!count) return + + await Meteor.roleAssignment.updateAsync( + { + 'inheritedRoles._id': parentName + }, + { + $push: { + inheritedRoles: { + $each: [ + role._id, + ...(await Roles._getInheritedRoleNamesAsync(role)) + ].map((r) => ({ _id: r })) + } + } + }, + { multi: true } + ) + }, + + /** + * Remove role parent from roles. + * + * Other parents are kept (role can have multiple parents). For users which have the + * parent role set, removed subrole is removed automatically. + * + * @method removeRolesFromParent + * @param {Array|String} rolesNames Name(s) of role(s). + * @param {String} parentName Name of parent role. + * @returns {Promise} + * @static + */ + removeRolesFromParentAsync: async function (rolesNames, parentName) { + // ensure arrays + if (!Array.isArray(rolesNames)) rolesNames = [rolesNames] + + for (const roleName of rolesNames) { + await Roles._removeRoleFromParentAsync(roleName, parentName) + } + }, + + /** + * @method _removeRoleFromParent + * @param {String} roleName Name of role. + * @param {String} parentName Name of parent role. + * @returns {Promise} + * @private + * @static + */ + _removeRoleFromParentAsync: async function (roleName, parentName) { + Roles._checkRoleName(roleName) + Roles._checkRoleName(parentName) + + // check for role existence + // this would not really be needed, but we are trying to match addRolesToParent + const role = await Meteor.roles.findOneAsync( + { _id: roleName }, + { fields: { _id: 1 } } + ) + + if (!role) { + throw new Error("Role '" + roleName + "' does not exist.") + } + + const count = await Meteor.roles.updateAsync( + { + _id: parentName + }, + { + $pull: { + children: { + _id: role._id + } + } + } + ) + + // if there was no change, parent role might not exist, or role was + // already not a subrole; in any case we do not have anything more to do + if (!count) return + + // For all roles who have had it as a dependency ... + const roles = [ + ...(await Roles._getParentRoleNamesAsync( + await Meteor.roles.findOneAsync({ _id: parentName }) + )), + parentName + ] + + for (const r of await Meteor.roles + .find({ _id: { $in: roles } }) + .fetchAsync()) { + const inheritedRoles = await Roles._getInheritedRoleNamesAsync( + await Meteor.roles.findOneAsync({ _id: r._id }) + ) + await Meteor.roleAssignment.updateAsync( + { + 'role._id': r._id, + 'inheritedRoles._id': role._id + }, + { + $set: { + inheritedRoles: [r._id, ...inheritedRoles].map((r2) => ({ + _id: r2 + })) + } + }, + { multi: true } + ) + } + }, + + /** + * Add users to roles. + * + * Adds roles to existing roles for each user. + * + * @example + * Roles.addUsersToRoles(userId, 'admin') + * Roles.addUsersToRoles(userId, ['view-secrets'], 'example.com') + * Roles.addUsersToRoles([user1, user2], ['user','editor']) + * Roles.addUsersToRoles([user1, user2], ['glorious-admin', 'perform-action'], 'example.org') + * + * @method addUsersToRoles + * @param {Array|String} users User ID(s) or object(s) with an `_id` field. + * @param {Array|String} roles Name(s) of roles to add users to. Roles have to exist. + * @param {Object|String} [options] Options: + * - `scope`: name of the scope, or `null` for the global role + * - `ifExists`: if `true`, do not throw an exception if the role does not exist + * @returns {Promise} + * + * Alternatively, it can be a scope name string. + * @static + */ + addUsersToRolesAsync: async function (users, roles, options) { + let id + + if (!users) throw new Error("Missing 'users' param.") + if (!roles) throw new Error("Missing 'roles' param.") + + options = Roles._normalizeOptions(options) + + // ensure arrays + if (!Array.isArray(users)) users = [users] + if (!Array.isArray(roles)) roles = [roles] + + Roles._checkScopeName(options.scope) + + options = Object.assign( + { + ifExists: false + }, + options + ) + + for (const user of users) { + if (typeof user === 'object') { + id = user._id + } else { + id = user + } + + for (const role of roles) { + await Roles._addUserToRoleAsync(id, role, options) + } + } + }, + + /** + * Set users' roles. + * + * Replaces all existing roles with a new set of roles. + * + * @example + * Roles.setUserRoles(userId, 'admin') + * Roles.setUserRoles(userId, ['view-secrets'], 'example.com') + * Roles.setUserRoles([user1, user2], ['user','editor']) + * Roles.setUserRoles([user1, user2], ['glorious-admin', 'perform-action'], 'example.org') + * + * @method setUserRoles + * @param {Array|String} users User ID(s) or object(s) with an `_id` field. + * @param {Array|String} roles Name(s) of roles to add users to. Roles have to exist. + * @param {Object|String} [options] Options: + * - `scope`: name of the scope, or `null` for the global role + * - `anyScope`: if `true`, remove all roles the user has, of any scope, if `false`, only the one in the same scope + * - `ifExists`: if `true`, do not throw an exception if the role does not exist + * @returns {Promise} + * + * Alternatively, it can be a scope name string. + * @static + */ + setUserRolesAsync: async function (users, roles, options) { + let id + + if (!users) throw new Error("Missing 'users' param.") + if (!roles) throw new Error("Missing 'roles' param.") + + options = Roles._normalizeOptions(options) + + // ensure arrays + if (!Array.isArray(users)) users = [users] + if (!Array.isArray(roles)) roles = [roles] + + Roles._checkScopeName(options.scope) + + options = Object.assign( + { + ifExists: false, + anyScope: false + }, + options + ) + + for (const user of users) { + if (typeof user === 'object') { + id = user._id + } else { + id = user + } + // we first clear all roles for the user + const selector = { 'user._id': id } + if (!options.anyScope) { + selector.scope = options.scope + } + + await Meteor.roleAssignment.removeAsync(selector) + + // and then add all + for (const role of roles) { + await Roles._addUserToRole(id, role, options) + } + } + }, + + /** + * Add one user to one role. + * + * @method _addUserToRole + * @param {String} userId The user ID. + * @param {String} roleName Name of the role to add the user to. The role have to exist. + * @param {Object} options Options: + * - `scope`: name of the scope, or `null` for the global role + * - `ifExists`: if `true`, do not throw an exception if the role does not exist + * @returns {Promise} + * @private + * @static + */ + _addUserToRoleAsync: async function (userId, roleName, options) { + Roles._checkRoleName(roleName) + Roles._checkScopeName(options.scope) + + if (!userId) { + return + } + + const role = await Meteor.roles.findOneAsync( + { _id: roleName }, + { fields: { children: 1 } } + ) + + if (!role) { + if (options.ifExists) { + return [] + } else { + throw new Error("Role '" + roleName + "' does not exist.") + } + } + + // This might create duplicates, because we don't have a unique index, but that's all right. In case there are two, withdrawing the role will effectively kill them both. + // TODO revisit this + /* const res = await Meteor.roleAssignment.upsertAsync( + { + "user._id": userId, + "role._id": roleName, + scope: options.scope, + }, + { + $setOnInsert: { + user: { _id: userId }, + role: { _id: roleName }, + scope: options.scope, + }, + } + ); */ + const existingAssignment = await Meteor.roleAssignment.findOneAsync({ + 'user._id': userId, + 'role._id': roleName, + scope: options.scope + }) + + let insertedId + let res + if (existingAssignment) { + await Meteor.roleAssignment.updateAsync(existingAssignment._id, { + $set: { + user: { _id: userId }, + role: { _id: roleName }, + scope: options.scope + } + }) + + res = await Meteor.roleAssignment.findOneAsync(existingAssignment._id) + } else { + insertedId = await Meteor.roleAssignment.insertAsync({ + user: { _id: userId }, + role: { _id: roleName }, + scope: options.scope + }) + } + + if (insertedId) { + await Meteor.roleAssignment.updateAsync( + { _id: insertedId }, + { + $set: { + inheritedRoles: [ + roleName, + ...(await Roles._getInheritedRoleNamesAsync(role)) + ].map((r) => ({ _id: r })) + } + } + ) + + res = await Meteor.roleAssignment.findOneAsync({ _id: insertedId }) + } + res.insertedId = insertedId // For backward compatibility + + return res + }, + + /** + * Returns an array of role names the given role name is a child of. + * + * @example + * Roles._getParentRoleNames({ _id: 'admin', children; [] }) + * + * @method _getParentRoleNames + * @param {object} role The role object + * @returns {Promise} + * @private + * @static + */ + _getParentRoleNamesAsync: async function (role) { + if (!role) { + return [] + } + + const parentRoles = new Set([role._id]) + + for (const roleName of parentRoles) { + for (const parentRole of await Meteor.roles + .find({ 'children._id': roleName }) + .fetchAsync()) { + parentRoles.add(parentRole._id) + } + } + + parentRoles.delete(role._id) + + return [...parentRoles] + }, + + /** + * Returns an array of role names the given role name is a parent of. + * + * @example + * Roles._getInheritedRoleNames({ _id: 'admin', children; [] }) + * + * @method _getInheritedRoleNames + * @param {object} role The role object + * @returns {Promise} + * @private + * @static + */ + _getInheritedRoleNamesAsync: async function (role) { + const inheritedRoles = new Set() + const nestedRoles = new Set([role]) + + for (const r of nestedRoles) { + const roles = await Meteor.roles + .find( + { _id: { $in: r.children.map((r) => r._id) } }, + { fields: { children: 1 } } + ) + .fetchAsync() + + for (const r2 of roles) { + inheritedRoles.add(r2._id) + nestedRoles.add(r2) + } + } + + return [...inheritedRoles] + }, + + /** + * Remove users from assigned roles. + * + * @example + * Roles.removeUsersFromRoles(userId, 'admin') + * Roles.removeUsersFromRoles([userId, user2], ['editor']) + * Roles.removeUsersFromRoles(userId, ['user'], 'group1') + * + * @method removeUsersFromRoles + * @param {Array|String} users User ID(s) or object(s) with an `_id` field. + * @param {Array|String} roles Name(s) of roles to remove users from. Roles have to exist. + * @param {Object|String} [options] Options: + * - `scope`: name of the scope, or `null` for the global role + * - `anyScope`: if set, role can be in any scope (`scope` option is ignored) + * @returns {Promise} + * + * Alternatively, it can be a scope name string. + * @static + */ + removeUsersFromRolesAsync: async function (users, roles, options) { + if (!users) throw new Error("Missing 'users' param.") + if (!roles) throw new Error("Missing 'roles' param.") + + options = Roles._normalizeOptions(options) + + // ensure arrays + if (!Array.isArray(users)) users = [users] + if (!Array.isArray(roles)) roles = [roles] + + Roles._checkScopeName(options.scope) + + for (const user of users) { + if (!user) return + + for (const role of roles) { + let id + if (typeof user === 'object') { + id = user._id + } else { + id = user + } + + await Roles._removeUserFromRoleAsync(id, role, options) + } + } + }, + + /** + * Remove one user from one role. + * + * @method _removeUserFromRole + * @param {String} userId The user ID. + * @param {String} roleName Name of the role to add the user to. The role have to exist. + * @param {Object} options Options: + * - `scope`: name of the scope, or `null` for the global role + * - `anyScope`: if set, role can be in any scope (`scope` option is ignored) + * @returns {Promise} + * @private + * @static + */ + _removeUserFromRoleAsync: async function (userId, roleName, options) { + Roles._checkRoleName(roleName) + Roles._checkScopeName(options.scope) + + if (!userId) return + + const selector = { + 'user._id': userId, + 'role._id': roleName + } + + if (!options.anyScope) { + selector.scope = options.scope + } + + await Meteor.roleAssignment.removeAsync(selector) + }, + + /** + * Check if user has specified roles. + * + * @example + * // global roles + * Roles.userIsInRole(user, 'admin') + * Roles.userIsInRole(user, ['admin','editor']) + * Roles.userIsInRole(userId, 'admin') + * Roles.userIsInRole(userId, ['admin','editor']) + * + * // scope roles (global roles are still checked) + * Roles.userIsInRole(user, 'admin', 'group1') + * Roles.userIsInRole(userId, ['admin','editor'], 'group1') + * Roles.userIsInRole(userId, ['admin','editor'], {scope: 'group1'}) + * + * @method userIsInRole + * @param {String|Object} user User ID or an actual user object. + * @param {Array|String} roles Name of role or an array of roles to check against. If array, + * will return `true` if user is in _any_ role. + * Roles do not have to exist. + * @param {Object|String} [options] Options: + * - `scope`: name of the scope; if supplied, limits check to just that scope + * the user's global roles will always be checked whether scope is specified or not + * - `anyScope`: if set, role can be in any scope (`scope` option is ignored) + * + * Alternatively, it can be a scope name string. + * @return {Promise} `true` if user is in _any_ of the target roles + * @static + */ + userIsInRoleAsync: async function (user, roles, options) { + let id + + options = Roles._normalizeOptions(options) + + // ensure array to simplify code + if (!Array.isArray(roles)) roles = [roles] + + roles = roles.filter((r) => r != null) + + if (!roles.length) return false + + Roles._checkScopeName(options.scope) + + options = Object.assign( + { + anyScope: false + }, + options + ) + + if (user && typeof user === 'object') { + id = user._id + } else { + id = user + } + + if (!id) return false + if (typeof id !== 'string') return false + + const selector = { + 'user._id': id + } + + if (!options.anyScope) { + selector.scope = { $in: [options.scope, null] } + } + + const res = await asyncSome(roles, async (roleName) => { + selector['inheritedRoles._id'] = roleName + const out = + (await Meteor.roleAssignment + .find(selector, { limit: 1 }) + .countAsync()) > 0 + return out + }) + + return res + }, + + /** + * Retrieve user's roles. + * + * @method getRolesForUser + * @param {String|Object} user User ID or an actual user object. + * @param {Object|String} [options] Options: + * - `scope`: name of scope to provide roles for; if not specified, global roles are returned + * - `anyScope`: if set, role can be in any scope (`scope` and `onlyAssigned` options are ignored) + * - `onlyScoped`: if set, only roles in the specified scope are returned + * - `onlyAssigned`: return only assigned roles and not automatically inferred (like subroles) + * - `fullObjects`: return full roles objects (`true`) or just names (`false`) (`onlyAssigned` option is ignored) (default `false`) + * If you have a use-case for this option, please file a feature-request. You shouldn't need to use it as it's + * result strongly dependent on the internal data structure of this plugin. + * + * Alternatively, it can be a scope name string. + * @return {Promise} Array of user's roles, unsorted. + * @static + */ + getRolesForUserAsync: async function (user, options) { + let id + + options = Roles._normalizeOptions(options) + + Roles._checkScopeName(options.scope) + + options = Object.assign({ + fullObjects: false, + onlyAssigned: false, + anyScope: false, + onlyScoped: false + }, options) + + if (user && typeof user === 'object') { + id = user._id + } else { + id = user + } + + if (!id) return [] + + const selector = { + 'user._id': id + } + + const filter = { + fields: { 'inheritedRoles._id': 1 } + } + + if (!options.anyScope) { + selector.scope = { $in: [options.scope] } + + if (!options.onlyScoped) { + selector.scope.$in.push(null) + } + } + + if (options.onlyAssigned) { + delete filter.fields['inheritedRoles._id'] + filter.fields['role._id'] = 1 + } + + if (options.fullObjects) { + delete filter.fields + } + + const roles = await Meteor.roleAssignment.find(selector, filter).fetchAsync() + + if (options.fullObjects) { + return roles + } + + return [ + ...new Set( + roles.reduce((rev, current) => { + if (current.inheritedRoles) { + return rev.concat(current.inheritedRoles.map((r) => r._id)) + } else if (current.role) { + rev.push(current.role._id) + } + return rev + }, []) + ) + ] + }, + + /** + * Retrieve cursor of all existing roles. + * + * @method getAllRoles + * @param {Object} [queryOptions] Options which are passed directly + * through to `Meteor.roles.find(query, options)`. + * @return {Cursor} Cursor of existing roles. + * @static + */ + getAllRoles: function (queryOptions) { + queryOptions = queryOptions || { sort: { _id: 1 } } + + return Meteor.roles.find({}, queryOptions) + }, + + /** + * Retrieve all users who are in target role. + * + * Options: + * + * @method getUsersInRole + * @param {Array|String} roles Name of role or an array of roles. If array, users + * returned will have at least one of the roles + * specified but need not have _all_ roles. + * Roles do not have to exist. + * @param {Object|String} [options] Options: + * - `scope`: name of the scope to restrict roles to; user's global + * roles will also be checked + * - `anyScope`: if set, role can be in any scope (`scope` option is ignored) + * - `onlyScoped`: if set, only roles in the specified scope are returned + * - `queryOptions`: options which are passed directly + * through to `Meteor.users.find(query, options)` + * + * Alternatively, it can be a scope name string. + * @param {Object} [queryOptions] Options which are passed directly + * through to `Meteor.users.find(query, options)` + * @return {Promise} Cursor of users in roles. + * @static + */ + getUsersInRoleAsync: async function (roles, options, queryOptions) { + const ids = ( + await Roles.getUserAssignmentsForRole(roles, options).fetchAsync() + ).map((a) => a.user._id) + + return Meteor.users.find( + { _id: { $in: ids } }, + (options && options.queryOptions) || queryOptions || {} + ) + }, + + /** + * Retrieve all assignments of a user which are for the target role. + * + * Options: + * + * @method getUserAssignmentsForRole + * @param {Array|String} roles Name of role or an array of roles. If array, users + * returned will have at least one of the roles + * specified but need not have _all_ roles. + * Roles do not have to exist. + * @param {Object|String} [options] Options: + * - `scope`: name of the scope to restrict roles to; user's global + * roles will also be checked + * - `anyScope`: if set, role can be in any scope (`scope` option is ignored) + * - `queryOptions`: options which are passed directly + * through to `Meteor.roleAssignment.find(query, options)` + + * Alternatively, it can be a scope name string. + * @return {Cursor} Cursor of user assignments for roles. + * @static + */ + getUserAssignmentsForRole: function (roles, options) { + options = Roles._normalizeOptions(options) + + options = Object.assign( + { + anyScope: false, + queryOptions: {} + }, + options + ) + + return Roles._getUsersInRoleCursor(roles, options, options.queryOptions) + }, + + /** + * @method _getUsersInRoleCursor + * @param {Array|String} roles Name of role or an array of roles. If array, ids of users are + * returned which have at least one of the roles + * assigned but need not have _all_ roles. + * Roles do not have to exist. + * @param {Object|String} [options] Options: + * - `scope`: name of the scope to restrict roles to; user's global + * roles will also be checked + * - `anyScope`: if set, role can be in any scope (`scope` option is ignored) + * + * Alternatively, it can be a scope name string. + * @param {Object} [filter] Options which are passed directly + * through to `Meteor.roleAssignment.find(query, options)` + * @return {Object} Cursor to the assignment documents + * @private + * @static + */ + _getUsersInRoleCursor: function (roles, options, filter) { + options = Roles._normalizeOptions(options) + + options = Object.assign( + { + anyScope: false, + onlyScoped: false + }, + options + ) + + // ensure array to simplify code + if (!Array.isArray(roles)) roles = [roles] + + Roles._checkScopeName(options.scope) + + filter = Object.assign( + { + fields: { 'user._id': 1 } + }, + filter + ) + + const selector = { + 'inheritedRoles._id': { $in: roles } + } + + if (!options.anyScope) { + selector.scope = { $in: [options.scope] } + + if (!options.onlyScoped) { + selector.scope.$in.push(null) + } + } + + return Meteor.roleAssignment.find(selector, filter) + }, + + /** + * Deprecated. Use `getScopesForUser` instead. + * + * @method getGroupsForUser + * @returns {Promise} + * @static + * @deprecated + */ + getGroupsForUserAsync: async function (...args) { + if (!getGroupsForUserDeprecationWarning) { + getGroupsForUserDeprecationWarning = true + console && + console.warn( + 'getGroupsForUser has been deprecated. Use getScopesForUser instead.' + ) + } + + return await Roles.getScopesForUser(...args) + }, + + /** + * Retrieve users scopes, if any. + * + * @method getScopesForUser + * @param {String|Object} user User ID or an actual user object. + * @param {Array|String} [roles] Name of roles to restrict scopes to. + * + * @return {Promise} Array of user's scopes, unsorted. + * @static + */ + getScopesForUserAsync: async function (user, roles) { + let id + + if (roles && !Array.isArray(roles)) roles = [roles] + + if (user && typeof user === 'object') { + id = user._id + } else { + id = user + } + + if (!id) return [] + + const selector = { + 'user._id': id, + scope: { $ne: null } + } + + if (roles) { + selector['inheritedRoles._id'] = { $in: roles } + } + + const scopes = ( + await Meteor.roleAssignment + .find(selector, { fields: { scope: 1 } }) + .fetchAsync() + ).map((obi) => obi.scope) + + return [...new Set(scopes)] + }, + + /** + * Rename a scope. + * + * Roles assigned with a given scope are changed to be under the new scope. + * + * @method renameScope + * @param {String} oldName Old name of a scope. + * @param {String} newName New name of a scope. + * @returns {Promise} + * @static + */ + renameScopeAsync: async function (oldName, newName) { + let count + + Roles._checkScopeName(oldName) + Roles._checkScopeName(newName) + + if (oldName === newName) return + + do { + count = await Meteor.roleAssignment.updateAsync( + { + scope: oldName + }, + { + $set: { + scope: newName + } + }, + { multi: true } + ) + } while (count > 0) + }, + + /** + * Remove a scope. + * + * Roles assigned with a given scope are removed. + * + * @method removeScope + * @param {String} name The name of a scope. + * @returns {Promise} + * @static + */ + removeScopeAsync: async function (name) { + Roles._checkScopeName(name) + + await Meteor.roleAssignment.removeAsync({ scope: name }) + }, + + /** + * Throw an exception if `roleName` is an invalid role name. + * + * @method _checkRoleName + * @param {String} roleName A role name to match against. + * @private + * @static + */ + _checkRoleName: function (roleName) { + if ( + !roleName || + typeof roleName !== 'string' || + roleName.trim() !== roleName + ) { + throw new Error("Invalid role name '" + roleName + "'.") + } + }, + + /** + * Find out if a role is an ancestor of another role. + * + * WARNING: If you check this on the client, please make sure all roles are published. + * + * @method isParentOf + * @param {String} parentRoleName The role you want to research. + * @param {String} childRoleName The role you expect to be among the children of parentRoleName. + * @returns {Promise} + * @static + */ + isParentOfAsync: async function (parentRoleName, childRoleName) { + if (parentRoleName === childRoleName) { + return true + } + + if (parentRoleName == null || childRoleName == null) { + return false + } + + Roles._checkRoleName(parentRoleName) + Roles._checkRoleName(childRoleName) + + let rolesToCheck = [parentRoleName] + while (rolesToCheck.length !== 0) { + const roleName = rolesToCheck.pop() + + if (roleName === childRoleName) { + return true + } + + const role = await Meteor.roles.findOneAsync({ _id: roleName }) + + // This should not happen, but this is a problem to address at some other time. + if (!role) continue + + rolesToCheck = rolesToCheck.concat(role.children.map((r) => r._id)) + } + + return false + }, + + /** + * Normalize options. + * + * @method _normalizeOptions + * @param {Object} options Options to normalize. + * @return {Object} Normalized options. + * @private + * @static + */ + _normalizeOptions: function (options) { + options = options === undefined ? {} : options + + if (options === null || typeof options === 'string') { + options = { scope: options } + } + + options.scope = Roles._normalizeScopeName(options.scope) + + return options + }, + + /** + * Normalize scope name. + * + * @method _normalizeScopeName + * @param {String} scopeName A scope name to normalize. + * @return {String} Normalized scope name. + * @private + * @static + */ + _normalizeScopeName: function (scopeName) { + // map undefined and null to null + if (scopeName == null) { + return null + } else { + return scopeName + } + }, + + /** + * Throw an exception if `scopeName` is an invalid scope name. + * + * @method _checkRoleName + * @param {String} scopeName A scope name to match against. + * @private + * @static + */ + _checkScopeName: function (scopeName) { + if (scopeName === null) return + + if ( + !scopeName || + typeof scopeName !== 'string' || + scopeName.trim() !== scopeName + ) { + throw new Error("Invalid scope name '" + scopeName + "'.") + } + } +}) diff --git a/roles/roles_server.js b/roles/roles_server.js index 78c11c39..fdf03794 100644 --- a/roles/roles_server.js +++ b/roles/roles_server.js @@ -79,7 +79,7 @@ Object.assign(Roles, { * @static */ _isNewField: function (roles) { - return Array.isArray(roles) && (typeof roles[0] === 'object') + return Array.isArray(roles) && typeof roles[0] === 'object' }, /** @@ -92,7 +92,10 @@ Object.assign(Roles, { * @static */ _isOldField: function (roles) { - return (Array.isArray(roles) && (typeof roles[0] === 'string')) || ((typeof roles === 'object') && !Array.isArray(roles)) + return ( + (Array.isArray(roles) && typeof roles[0] === 'string') || + (typeof roles === 'object' && !Array.isArray(roles)) + ) }, /** @@ -104,7 +107,7 @@ Object.assign(Roles, { * @static */ _convertToNewRole: function (oldRole) { - if (!(typeof oldRole.name === 'string')) throw new Error("Role name '" + oldRole.name + "' is not a string.") + if (!(typeof oldRole.name === 'string')) { throw new Error("Role name '" + oldRole.name + "' is not a string.") } return { _id: oldRole.name, @@ -121,7 +124,7 @@ Object.assign(Roles, { * @static */ _convertToOldRole: function (newRole) { - if (!(typeof newRole._id === 'string')) throw new Error("Role name '" + newRole._id + "' is not a string.") + if (!(typeof newRole._id === 'string')) { throw new Error("Role name '" + newRole._id + "' is not a string.") } return { name: newRole._id @@ -141,7 +144,7 @@ Object.assign(Roles, { const roles = [] if (Array.isArray(oldRoles)) { oldRoles.forEach(function (role, index) { - if (!(typeof role === 'string')) throw new Error("Role '" + role + "' is not a string.") + if (!(typeof role === 'string')) { throw new Error("Role '" + role + "' is not a string.") } roles.push({ _id: role, @@ -159,7 +162,7 @@ Object.assign(Roles, { } rolesArray.forEach(function (role) { - if (!(typeof role === 'string')) throw new Error("Role '" + role + "' is not a string.") + if (!(typeof role === 'string')) { throw new Error("Role '" + role + "' is not a string.") } roles.push({ _id: role, @@ -191,18 +194,26 @@ Object.assign(Roles, { } newRoles.forEach(function (userRole) { - if (!(typeof userRole === 'object')) throw new Error("Role '" + userRole + "' is not an object.") + if (!(typeof userRole === 'object')) { throw new Error("Role '" + userRole + "' is not an object.") } // We assume that we are converting back a failed migration, so values can only be // what were valid values in 1.0. So no group names starting with $ and no subroles. if (userRole.scope) { - if (!usingGroups) throw new Error("Role '" + userRole._id + "' with scope '" + userRole.scope + "' without enabled groups.") + if (!usingGroups) { + throw new Error( + "Role '" + + userRole._id + + "' with scope '" + + userRole.scope + + "' without enabled groups." + ) + } // escape const scope = userRole.scope.replace(/\./g, '_') - if (scope[0] === '$') throw new Error("Group name '" + scope + "' start with $.") + if (scope[0] === '$') { throw new Error("Group name '" + scope + "' start with $.") } roles[scope] = roles[scope] || [] roles[scope].push(userRole._id) @@ -227,13 +238,16 @@ Object.assign(Roles, { * @static */ _defaultUpdateUser: function (user, roles) { - Meteor.users.update({ - _id: user._id, - // making sure nothing changed in meantime - roles: user.roles - }, { - $set: { roles } - }) + Meteor.users.update( + { + _id: user._id, + // making sure nothing changed in meantime + roles: user.roles + }, + { + $set: { roles } + } + ) }, /** @@ -294,7 +308,10 @@ Object.assign(Roles, { Meteor.users.find().forEach(function (user, index, cursor) { if (!Roles._isNewField(user.roles)) { - updateUser(user, Roles._convertToNewField(user.roles, convertUnderscoresToDots)) + updateUser( + user, + Roles._convertToNewField(user.roles, convertUnderscoresToDots) + ) } }) }, @@ -313,10 +330,15 @@ Object.assign(Roles, { Object.assign(userSelector, { roles: { $ne: null } }) Meteor.users.find(userSelector).forEach(function (user, index) { - user.roles.filter((r) => r.assigned).forEach(r => { - // Added `ifExists` to make it less error-prone - Roles._addUserToRole(user._id, r._id, { scope: r.scope, ifExists: true }) - }) + user.roles + .filter((r) => r.assigned) + .forEach((r) => { + // Added `ifExists` to make it less error-prone + Roles._addUserToRole(user._id, r._id, { + scope: r.scope, + ifExists: true + }) + }) Meteor.users.update({ _id: user._id }, { $unset: { roles: '' } }) }) @@ -381,10 +403,12 @@ Object.assign(Roles, { Meteor.users._ensureIndex({ 'roles.scope': 1 }) } - Meteor.roleAssignment.find(assignmentSelector).forEach(r => { + Meteor.roleAssignment.find(assignmentSelector).forEach((r) => { const roles = Meteor.users.findOne({ _id: r.user._id }).roles || [] - const currentRole = roles.find(oldRole => oldRole._id === r.role._id && oldRole.scope === r.scope) + const currentRole = roles.find( + (oldRole) => oldRole._id === r.role._id && oldRole.scope === r.scope + ) if (currentRole) { currentRole.assigned = true } else { @@ -394,8 +418,11 @@ Object.assign(Roles, { assigned: true }) - r.inheritedRoles.forEach(inheritedRole => { - const currentInheritedRole = roles.find(oldRole => oldRole._id === inheritedRole._id && oldRole.scope === r.scope) + r.inheritedRoles.forEach((inheritedRole) => { + const currentInheritedRole = roles.find( + (oldRole) => + oldRole._id === inheritedRole._id && oldRole.scope === r.scope + ) if (!currentInheritedRole) { roles.push({ diff --git a/roles/tests/clientAsync.js b/roles/tests/clientAsync.js new file mode 100644 index 00000000..ce1575e4 --- /dev/null +++ b/roles/tests/clientAsync.js @@ -0,0 +1,141 @@ +/* eslint-env mocha */ +/* global Roles */ + +import { Meteor } from 'meteor/meteor' +import chai, { assert } from 'chai' +import chaiAsPromised from 'chai-as-promised' + +// To ensure that the files are loaded for coverage +import '../roles_common' + +chai.use(chaiAsPromised) + +const safeInsert = async (collection, data) => { + try { + await collection.insertAsync(data) + } catch (e) {} +} + +describe('roles async', function () { + const roles = ['admin', 'editor', 'user'] + const users = { + eve: { + _id: 'eve' + }, + bob: { + _id: 'bob' + }, + joe: { + _id: 'joe' + } + } + + async function testUser (username, expectedRoles, scope) { + const user = users[username] + + // test using user object rather than userId to avoid mocking + for (const role of roles) { + const expected = expectedRoles.includes(role) + const msg = username + ' expected to have \'' + role + '\' permission but does not' + const nmsg = username + ' had un-expected permission ' + role + + if (expected) { + assert.isTrue(await Roles.userIsInRoleAsync(user, role, scope), msg) + } else { + assert.isFalse(await Roles.userIsInRoleAsync(user, role, scope), nmsg) + } + } + } + + let meteorUserMethod + before(() => { + meteorUserMethod = Meteor.user + // Mock Meteor.user() for isInRole handlebars helper testing + Meteor.user = function () { + return users.eve + } + }) + + after(() => { + Meteor.user = meteorUserMethod + }) + + beforeEach(async () => { + await safeInsert(Meteor.roleAssignment, { + user: users.eve, + role: { _id: 'admin' }, + inheritedRoles: [{ _id: 'admin' }] + }) + await safeInsert(Meteor.roleAssignment, { + user: users.eve, + role: { _id: 'editor' }, + inheritedRoles: [{ _id: 'editor' }] + }) + + await safeInsert(Meteor.roleAssignment, { + user: users.bob, + role: { _id: 'user' }, + inheritedRoles: [{ _id: 'user' }], + scope: 'group1' + }) + await safeInsert(Meteor.roleAssignment, { + user: users.bob, + role: { _id: 'editor' }, + inheritedRoles: [{ _id: 'editor' }], + scope: 'group2' + }) + + await safeInsert(Meteor.roleAssignment, { + user: users.joe, + role: { _id: 'admin' }, + inheritedRoles: [{ _id: 'admin' }] + }) + await safeInsert(Meteor.roleAssignment, { + user: users.joe, + role: { _id: 'editor' }, + inheritedRoles: [{ _id: 'editor' }], + scope: 'group1' + }) + }) + + it('can check current users roles via template helper', function () { + let expected + let actual + + if (!Roles._handlebarsHelpers) { + // probably running package tests outside of a Meteor app. + // skip this test. + return + } + + const isInRole = Roles._handlebarsHelpers.isInRole + assert.equal(typeof isInRole, 'function', "'isInRole' helper not registered") + + expected = true + actual = isInRole('admin, editor') + assert.equal(actual, expected) + + expected = true + actual = isInRole('admin') + assert.equal(actual, expected) + + expected = false + actual = isInRole('unknown') + assert.equal(actual, expected) + }) + + it('can check if user is in role', async function () { + await testUser('eve', ['admin', 'editor']) + }) + + it('can check if user is in role by group', async function () { + await testUser('bob', ['user'], 'group1') + await testUser('bob', ['editor'], 'group2') + }) + + it('can check if user is in role with Roles.GLOBAL_GROUP', async function () { + await testUser('joe', ['admin']) + await testUser('joe', ['admin'], Roles.GLOBAL_GROUP) + await testUser('joe', ['admin', 'editor'], 'group1') + }) +}) diff --git a/roles/tests/server.js b/roles/tests/server.js index b3fb854d..5ed65b56 100644 --- a/roles/tests/server.js +++ b/roles/tests/server.js @@ -1418,206 +1418,210 @@ describe('roles', function () { assert.isFalse(Roles.userIsInRole(users.eve, 'user', 'scope2')) }) - it('migration without global groups (to v2)', function () { - assert.isOk(Meteor.roles.insert({ name: 'admin' })) - assert.isOk(Meteor.roles.insert({ name: 'editor' })) - assert.isOk(Meteor.roles.insert({ name: 'user' })) - - assert.isOk(Meteor.users.update(users.eve, { $set: { roles: ['admin', 'editor'] } })) - assert.isOk(Meteor.users.update(users.bob, { $set: { roles: [] } })) - assert.isOk(Meteor.users.update(users.joe, { $set: { roles: ['user'] } })) - - Roles._forwardMigrate() - - assert.deepEqual(Meteor.users.findOne(users.eve, { fields: { roles: 1, _id: 0 } }), { - roles: [{ + describe('v2 migration', function () { + it('migration without global groups (to v2)', function () { + assert.isOk(Meteor.roles.insert({ name: 'admin' })) + assert.isOk(Meteor.roles.insert({ name: 'editor' })) + assert.isOk(Meteor.roles.insert({ name: 'user' })) + + assert.isOk(Meteor.users.update(users.eve, { $set: { roles: ['admin', 'editor'] } })) + assert.isOk(Meteor.users.update(users.bob, { $set: { roles: [] } })) + assert.isOk(Meteor.users.update(users.joe, { $set: { roles: ['user'] } })) + + Roles._forwardMigrate() + + assert.deepEqual(Meteor.users.findOne(users.eve, { fields: { roles: 1, _id: 0 } }), { + roles: [{ + _id: 'admin', + scope: null, + assigned: true + }, { + _id: 'editor', + scope: null, + assigned: true + }] + }) + assert.deepEqual(Meteor.users.findOne(users.bob, { fields: { roles: 1, _id: 0 } }), { + roles: [] + }) + assert.deepEqual(Meteor.users.findOne(users.joe, { fields: { roles: 1, _id: 0 } }), { + roles: [{ + _id: 'user', + scope: null, + assigned: true + }] + }) + + assert.deepEqual(Meteor.roles.findOne({ _id: 'admin' }), { _id: 'admin', - scope: null, - assigned: true - }, { + children: [] + }) + assert.deepEqual(Meteor.roles.findOne({ _id: 'editor' }), { _id: 'editor', - scope: null, - assigned: true - }] - }) - assert.deepEqual(Meteor.users.findOne(users.bob, { fields: { roles: 1, _id: 0 } }), { - roles: [] - }) - assert.deepEqual(Meteor.users.findOne(users.joe, { fields: { roles: 1, _id: 0 } }), { - roles: [{ + children: [] + }) + assert.deepEqual(Meteor.roles.findOne({ _id: 'user' }), { _id: 'user', - scope: null, - assigned: true - }] - }) - - assert.deepEqual(Meteor.roles.findOne({ _id: 'admin' }), { - _id: 'admin', - children: [] - }) - assert.deepEqual(Meteor.roles.findOne({ _id: 'editor' }), { - _id: 'editor', - children: [] - }) - assert.deepEqual(Meteor.roles.findOne({ _id: 'user' }), { - _id: 'user', - children: [] - }) - - Roles._backwardMigrate(null, null, false) - - assert.deepEqual(Meteor.users.findOne(users.eve, { fields: { roles: 1, _id: 0 } }), { - roles: ['admin', 'editor'] - }) - assert.deepEqual(Meteor.users.findOne(users.bob, { fields: { roles: 1, _id: 0 } }), { - roles: [] - }) - assert.deepEqual(Meteor.users.findOne(users.joe, { fields: { roles: 1, _id: 0 } }), { - roles: ['user'] + children: [] + }) + + Roles._backwardMigrate(null, null, false) + + assert.deepEqual(Meteor.users.findOne(users.eve, { fields: { roles: 1, _id: 0 } }), { + roles: ['admin', 'editor'] + }) + assert.deepEqual(Meteor.users.findOne(users.bob, { fields: { roles: 1, _id: 0 } }), { + roles: [] + }) + assert.deepEqual(Meteor.users.findOne(users.joe, { fields: { roles: 1, _id: 0 } }), { + roles: ['user'] + }) + + assert.deepEqual(Meteor.roles.findOne({ name: 'admin' }, { fields: { _id: 0 } }), { + name: 'admin' + }) + assert.deepEqual(Meteor.roles.findOne({ name: 'editor' }, { fields: { _id: 0 } }), { + name: 'editor' + }) + assert.deepEqual(Meteor.roles.findOne({ name: 'user' }, { fields: { _id: 0 } }), { + name: 'user' + }) }) - assert.deepEqual(Meteor.roles.findOne({ name: 'admin' }, { fields: { _id: 0 } }), { - name: 'admin' - }) - assert.deepEqual(Meteor.roles.findOne({ name: 'editor' }, { fields: { _id: 0 } }), { - name: 'editor' - }) - assert.deepEqual(Meteor.roles.findOne({ name: 'user' }, { fields: { _id: 0 } }), { - name: 'user' - }) - }) - - it('migration without global groups (to v3)') - - it('migration with global groups (to v2)', function () { - assert.isOk(Meteor.roles.insert({ name: 'admin' })) - assert.isOk(Meteor.roles.insert({ name: 'editor' })) - assert.isOk(Meteor.roles.insert({ name: 'user' })) - - assert.isOk(Meteor.users.update(users.eve, { $set: { roles: { __global_roles__: ['admin', 'editor'], foo_bla: ['user'] } } })) - assert.isOk(Meteor.users.update(users.bob, { $set: { roles: { } } })) - assert.isOk(Meteor.users.update(users.joe, { $set: { roles: { __global_roles__: ['user'], foo_bla: ['user'] } } })) - - Roles._forwardMigrate(null, null, false) - - assert.deepEqual(Meteor.users.findOne(users.eve, { fields: { roles: 1, _id: 0 } }), { - roles: [{ + it('migration with global groups (to v2)', function () { + assert.isOk(Meteor.roles.insert({ name: 'admin' })) + assert.isOk(Meteor.roles.insert({ name: 'editor' })) + assert.isOk(Meteor.roles.insert({ name: 'user' })) + + assert.isOk(Meteor.users.update(users.eve, { $set: { roles: { __global_roles__: ['admin', 'editor'], foo_bla: ['user'] } } })) + assert.isOk(Meteor.users.update(users.bob, { $set: { roles: { } } })) + assert.isOk(Meteor.users.update(users.joe, { $set: { roles: { __global_roles__: ['user'], foo_bla: ['user'] } } })) + + Roles._forwardMigrate(null, null, false) + + assert.deepEqual(Meteor.users.findOne(users.eve, { fields: { roles: 1, _id: 0 } }), { + roles: [{ + _id: 'admin', + scope: null, + assigned: true + }, { + _id: 'editor', + scope: null, + assigned: true + }, { + _id: 'user', + scope: 'foo_bla', + assigned: true + }] + }) + assert.deepEqual(Meteor.users.findOne(users.bob, { fields: { roles: 1, _id: 0 } }), { + roles: [] + }) + assert.deepEqual(Meteor.users.findOne(users.joe, { fields: { roles: 1, _id: 0 } }), { + roles: [{ + _id: 'user', + scope: null, + assigned: true + }, { + _id: 'user', + scope: 'foo_bla', + assigned: true + }] + }) + + assert.deepEqual(Meteor.roles.findOne({ _id: 'admin' }), { _id: 'admin', - scope: null, - assigned: true - }, { + children: [] + }) + assert.deepEqual(Meteor.roles.findOne({ _id: 'editor' }), { _id: 'editor', - scope: null, - assigned: true - }, { + children: [] + }) + assert.deepEqual(Meteor.roles.findOne({ _id: 'user' }), { _id: 'user', - scope: 'foo_bla', - assigned: true - }] - }) - assert.deepEqual(Meteor.users.findOne(users.bob, { fields: { roles: 1, _id: 0 } }), { - roles: [] - }) - assert.deepEqual(Meteor.users.findOne(users.joe, { fields: { roles: 1, _id: 0 } }), { - roles: [{ - _id: 'user', - scope: null, - assigned: true - }, { - _id: 'user', - scope: 'foo_bla', - assigned: true - }] - }) - - assert.deepEqual(Meteor.roles.findOne({ _id: 'admin' }), { - _id: 'admin', - children: [] - }) - assert.deepEqual(Meteor.roles.findOne({ _id: 'editor' }), { - _id: 'editor', - children: [] - }) - assert.deepEqual(Meteor.roles.findOne({ _id: 'user' }), { - _id: 'user', - children: [] - }) - - Roles._backwardMigrate(null, null, true) - - assert.deepEqual(Meteor.users.findOne(users.eve, { fields: { roles: 1, _id: 0 } }), { - roles: { - __global_roles__: ['admin', 'editor'], - foo_bla: ['user'] - } - }) - assert.deepEqual(Meteor.users.findOne(users.bob, { fields: { roles: 1, _id: 0 } }), { - roles: {} - }) - assert.deepEqual(Meteor.users.findOne(users.joe, { fields: { roles: 1, _id: 0 } }), { - roles: { - __global_roles__: ['user'], - foo_bla: ['user'] - } - }) - - assert.deepEqual(Meteor.roles.findOne({ name: 'admin' }, { fields: { _id: 0 } }), { - name: 'admin' - }) - assert.deepEqual(Meteor.roles.findOne({ name: 'editor' }, { fields: { _id: 0 } }), { - name: 'editor' - }) - assert.deepEqual(Meteor.roles.findOne({ name: 'user' }, { fields: { _id: 0 } }), { - name: 'user' - }) - - Roles._forwardMigrate(null, null, true) - - assert.deepEqual(Meteor.users.findOne(users.eve, { fields: { roles: 1, _id: 0 } }), { - roles: [{ + children: [] + }) + + Roles._backwardMigrate(null, null, true) + + assert.deepEqual(Meteor.users.findOne(users.eve, { fields: { roles: 1, _id: 0 } }), { + roles: { + __global_roles__: ['admin', 'editor'], + foo_bla: ['user'] + } + }) + assert.deepEqual(Meteor.users.findOne(users.bob, { fields: { roles: 1, _id: 0 } }), { + roles: {} + }) + assert.deepEqual(Meteor.users.findOne(users.joe, { fields: { roles: 1, _id: 0 } }), { + roles: { + __global_roles__: ['user'], + foo_bla: ['user'] + } + }) + + assert.deepEqual(Meteor.roles.findOne({ name: 'admin' }, { fields: { _id: 0 } }), { + name: 'admin' + }) + assert.deepEqual(Meteor.roles.findOne({ name: 'editor' }, { fields: { _id: 0 } }), { + name: 'editor' + }) + assert.deepEqual(Meteor.roles.findOne({ name: 'user' }, { fields: { _id: 0 } }), { + name: 'user' + }) + + Roles._forwardMigrate(null, null, true) + + assert.deepEqual(Meteor.users.findOne(users.eve, { fields: { roles: 1, _id: 0 } }), { + roles: [{ + _id: 'admin', + scope: null, + assigned: true + }, { + _id: 'editor', + scope: null, + assigned: true + }, { + _id: 'user', + scope: 'foo.bla', + assigned: true + }] + }) + assert.deepEqual(Meteor.users.findOne(users.bob, { fields: { roles: 1, _id: 0 } }), { + roles: [] + }) + assert.deepEqual(Meteor.users.findOne(users.joe, { fields: { roles: 1, _id: 0 } }), { + roles: [{ + _id: 'user', + scope: null, + assigned: true + }, { + _id: 'user', + scope: 'foo.bla', + assigned: true + }] + }) + + assert.deepEqual(Meteor.roles.findOne({ _id: 'admin' }), { _id: 'admin', - scope: null, - assigned: true - }, { + children: [] + }) + assert.deepEqual(Meteor.roles.findOne({ _id: 'editor' }), { _id: 'editor', - scope: null, - assigned: true - }, { - _id: 'user', - scope: 'foo.bla', - assigned: true - }] - }) - assert.deepEqual(Meteor.users.findOne(users.bob, { fields: { roles: 1, _id: 0 } }), { - roles: [] - }) - assert.deepEqual(Meteor.users.findOne(users.joe, { fields: { roles: 1, _id: 0 } }), { - roles: [{ - _id: 'user', - scope: null, - assigned: true - }, { + children: [] + }) + assert.deepEqual(Meteor.roles.findOne({ _id: 'user' }), { _id: 'user', - scope: 'foo.bla', - assigned: true - }] - }) - - assert.deepEqual(Meteor.roles.findOne({ _id: 'admin' }), { - _id: 'admin', - children: [] - }) - assert.deepEqual(Meteor.roles.findOne({ _id: 'editor' }), { - _id: 'editor', - children: [] - }) - assert.deepEqual(Meteor.roles.findOne({ _id: 'user' }), { - _id: 'user', - children: [] + children: [] + }) }) }) - it('migration with global groups (to v3)') + describe('v3 migration', function () { + it('migration without global groups (to v3)') + + it('migration with global groups (to v3)') + }) it('_addUserToRole', function () { Roles.createRole('admin') diff --git a/roles/tests/serverAsync.js b/roles/tests/serverAsync.js new file mode 100644 index 00000000..8d118aef --- /dev/null +++ b/roles/tests/serverAsync.js @@ -0,0 +1,2123 @@ +/* eslint-env mocha */ +/* global Roles */ + +import { Meteor } from 'meteor/meteor' +import chai, { assert } from 'chai' +import chaiAsPromised from 'chai-as-promised' + +// To ensure that the files are loaded for coverage +import '../roles_server' +import '../roles_common' + +chai.use(chaiAsPromised) + +// To allow inserting on the client, needed for testing. +Meteor.roleAssignment.allow({ + insert () { return true }, + update () { return true }, + remove () { return true } +}) + +const hasProp = (target, prop) => Object.hasOwnProperty.call(target, prop) + +describe('roles async', async function () { + let users = {} + const roles = ['admin', 'editor', 'user'] + + Meteor.publish('_roleAssignments', function () { + const loggedInUserId = this.userId + + if (!loggedInUserId) { + this.ready() + return + } + + return Meteor.roleAssignment.find({ _id: loggedInUserId }) + }) + + async function addUser (name) { + return await Meteor.users.insertAsync({ username: name }) + } + + async function testUser (username, expectedRoles, scope) { + const userId = users[username] + const userObj = await Meteor.users.findOneAsync({ _id: userId }) + + // check using user ids (makes db calls) + await _innerTest(userId, username, expectedRoles, scope) + + // check using passed-in user object + await _innerTest(userObj, username, expectedRoles, scope) + } + + async function _innerTest (userParam, username, expectedRoles, scope) { + // test that user has only the roles expected and no others + for (const role of roles) { + const expected = expectedRoles.includes(role) + const msg = username + ' expected to have \'' + role + '\' role but does not' + const nmsg = username + ' had the following un-expected role: ' + role + + if (expected) { + assert.isTrue(await Roles.userIsInRoleAsync(userParam, role, scope), msg) + } else { + assert.isFalse(await Roles.userIsInRoleAsync(userParam, role, scope), nmsg) + } + } + } + + beforeEach(async function () { + await Meteor.roles.removeAsync({}) + await Meteor.roleAssignment.removeAsync({}) + await Meteor.users.removeAsync({}) + + users = { + eve: await addUser('eve'), + bob: await addUser('bob'), + joe: await addUser('joe') + } + }) + + it('can create and delete roles', async function () { + const role1Id = await Roles.createRoleAsync('test1') + const test1a = await Meteor.roles.findOneAsync() + const test1b = await Meteor.roles.findOneAsync(role1Id) + assert.equal(test1a._id, 'test1') + assert.equal(test1b._id, 'test1') + + const role2Id = await Roles.createRoleAsync('test2') + const test2a = await Meteor.roles.findOneAsync({ _id: 'test2' }) + const test2b = await Meteor.roles.findOneAsync(role2Id) + assert.equal(test2a._id, 'test2') + assert.equal(test2b._id, 'test2') + + assert.equal(await Meteor.roles.countDocuments(), 2) + + await Roles.deleteRoleAsync('test1') + const undefinedTest = await Meteor.roles.findOneAsync({ _id: 'test1' }) + assert.equal(typeof undefinedTest, 'undefined') + + await Roles.deleteRoleAsync('test2') + const undefinedTest2 = await Meteor.roles.findOneAsync() + assert.equal(typeof undefinedTest2, 'undefined') + }) + + it('can try to remove non-existing roles without crashing', async function () { + try { + await Roles.deleteRoleAsync('non-existing-role') + } catch (e) { + assert.notExists(e) + } + // Roles.deleteRoleAsync('non-existing-role').should.be.fulfilled + }) + + it('can\'t create duplicate roles', async function () { + try { + await Roles.createRoleAsync('test1') + } catch (e) { + assert.notExists(e) + } + // assert.eventually.throws(Roles.createRoleAsync('test1')) + try { + await Roles.createRoleAsync('test1') + } catch (e) { + assert.exists(e) + } + assert.isNull(await Roles.createRoleAsync('test1', { unlessExists: true })) + }) + + it('can\'t create role with empty names', async function () { + await assert.isRejected(Roles.createRoleAsync(''), /Invalid role name/) + await assert.isRejected(Roles.createRoleAsync(null), /Invalid role name/) + await assert.isRejected(Roles.createRoleAsync(' '), /Invalid role name/) + await assert.isRejected(Roles.createRoleAsync(' foobar'), /Invalid role name/) + await assert.isRejected(Roles.createRoleAsync(' foobar '), /Invalid role name/) + }) + + it('can\'t use invalid scope names', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + await Roles.addUsersToRolesAsync(users.eve, ['admin', 'user'], 'scope1') + await Roles.addUsersToRolesAsync(users.eve, ['editor'], 'scope2') + + await assert.isRejected(Roles.addUsersToRolesAsync(users.eve, ['admin', 'user'], ''), /Invalid scope name/) + await assert.isRejected(Roles.addUsersToRolesAsync(users.eve, ['admin', 'user'], ' '), /Invalid scope name/) + await assert.isRejected(Roles.addUsersToRolesAsync(users.eve, ['admin', 'user'], ' foobar'), /Invalid scope name/) + await assert.isRejected(Roles.addUsersToRolesAsync(users.eve, ['admin', 'user'], ' foobar '), /Invalid scope name/) + await assert.isRejected(Roles.addUsersToRolesAsync(users.eve, ['admin', 'user'], 42), /Invalid scope name/) + }) + + it('can check if user is in role', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.addUsersToRolesAsync(users.eve, ['admin', 'user']) + + await testUser('eve', ['admin', 'user']) + }) + + it('can check if user is in role by scope', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + await Roles.addUsersToRolesAsync(users.eve, ['admin', 'user'], 'scope1') + await Roles.addUsersToRolesAsync(users.eve, ['editor'], 'scope2') + + testUser('eve', ['admin', 'user'], 'scope1') + testUser('eve', ['editor'], 'scope2') + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['admin', 'user'], 'scope2')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['editor'], 'scope1')) + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, ['admin', 'user'], { anyScope: true })) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, ['editor'], { anyScope: true })) + }) + + it('can check if user is in role by scope through options', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + await Roles.addUsersToRolesAsync(users.eve, ['admin', 'user'], { scope: 'scope1' }) + await Roles.addUsersToRolesAsync(users.eve, ['editor'], { scope: 'scope2' }) + + await testUser('eve', ['admin', 'user'], { scope: 'scope1' }) + await testUser('eve', ['editor'], { scope: 'scope2' }) + }) + + it('can check if user is in role by scope with global role', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + await Roles.addUsersToRolesAsync(users.eve, ['admin', 'user'], 'scope1') + await Roles.addUsersToRolesAsync(users.eve, ['editor'], 'scope2') + await Roles.addUsersToRolesAsync(users.eve, ['admin']) + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, ['user'], 'scope1')) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, ['editor'], 'scope2')) + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['user'])) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['editor'])) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['user'], null)) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['editor'], null)) + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['user'], 'scope2')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['editor'], 'scope1')) + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, ['admin'], 'scope2')) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, ['admin'], 'scope1')) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, ['admin'])) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, ['admin'], null)) + }) + + it('renaming scopes', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + await Roles.addUsersToRolesAsync(users.eve, ['admin', 'user'], 'scope1') + await Roles.addUsersToRolesAsync(users.eve, ['editor'], 'scope2') + + await testUser('eve', ['admin', 'user'], 'scope1') + await testUser('eve', ['editor'], 'scope2') + + await Roles.renameScopeAsync('scope1', 'scope3') + + await testUser('eve', ['admin', 'user'], 'scope3') + await testUser('eve', ['editor'], 'scope2') + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['admin', 'user'], 'scope1')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['admin', 'user'], 'scope2')) + + await assert.isRejected(Roles.renameScopeAsync('scope3'), /Invalid scope name/) + + await Roles.renameScopeAsync('scope3', null) + + await testUser('eve', ['admin', 'user', 'editor'], 'scope2') + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['editor'])) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, ['admin'])) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, ['user'])) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['editor'], null)) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, ['admin'], null)) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, ['user'], null)) + + await Roles.renameScopeAsync(null, 'scope2') + + await testUser('eve', ['admin', 'user', 'editor'], 'scope2') + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['editor'])) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['admin'])) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['user'])) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['editor'], null)) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['admin'], null)) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['user'], null)) + }) + + it('removing scopes', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + await Roles.addUsersToRolesAsync(users.eve, ['admin', 'user'], 'scope1') + await Roles.addUsersToRolesAsync(users.eve, ['editor'], 'scope2') + + await testUser('eve', ['admin', 'user'], 'scope1') + await testUser('eve', ['editor'], 'scope2') + + await Roles.removeScopeAsync('scope1') + + await testUser('eve', ['editor'], 'scope2') + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['admin', 'user'], 'scope1')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['admin', 'user'], 'scope2')) + }) + + it('can check if non-existant user is in role', async function () { + assert.isFalse(await Roles.userIsInRoleAsync('1', 'admin')) + }) + + it('can check if null user is in role', async function () { + assert.isFalse(await Roles.userIsInRoleAsync(null, 'admin')) + }) + + it('can check user against several roles at once', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + + await Roles.addUsersToRolesAsync(users.eve, ['admin', 'user']) + const user = await Meteor.users.findOneAsync({ _id: users.eve }) + + // we can check the non-existing role + assert.isTrue(await Roles.userIsInRoleAsync(user, ['editor', 'admin'])) + }) + + it('can\'t add non-existent user to role', async function () { + await Roles.createRoleAsync('admin') + + await Roles.addUsersToRolesAsync(['1'], ['admin']) + assert.equal(await Meteor.users.findOneAsync({ _id: '1' }), undefined) + }) + + it('can\'t add user to non-existent role', async function () { + await assert.isRejected(Roles.addUsersToRolesAsync(users.eve, ['admin']), /Role 'admin' does not exist/) + await Roles.addUsersToRolesAsync(users.eve, ['admin'], { ifExists: true }) + }) + + it('can\'t set non-existent user to role', async function () { + await Roles.createRoleAsync('admin') + + await Roles.setUserRolesAsync(['1'], ['admin']) + assert.equal(await Meteor.users.findOneAsync({ _id: '1' }), undefined) + }) + + it('can\'t set user to non-existent role', async function () { + await assert.isRejected(Roles.setUserRolesAsync(users.eve, ['admin']), /Role 'admin' does not exist/) + await Roles.setUserRolesAsync(users.eve, ['admin'], { ifExists: true }) + }) + + it('can add individual users to roles', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + await Roles.addUsersToRolesAsync(users.eve, ['admin', 'user']) + + await testUser('eve', ['admin', 'user']) + await testUser('bob', []) + await testUser('joe', []) + + await Roles.addUsersToRolesAsync(users.joe, ['editor', 'user']) + + await testUser('eve', ['admin', 'user']) + await testUser('bob', []) + await testUser('joe', ['editor', 'user']) + }) + + it('can add individual users to roles by scope', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + await Roles.addUsersToRolesAsync(users.eve, ['admin', 'user'], 'scope1') + + await testUser('eve', ['admin', 'user'], 'scope1') + await testUser('bob', [], 'scope1') + await testUser('joe', [], 'scope1') + + await testUser('eve', [], 'scope2') + await testUser('bob', [], 'scope2') + await testUser('joe', [], 'scope2') + + await Roles.addUsersToRolesAsync(users.joe, ['editor', 'user'], 'scope1') + await Roles.addUsersToRolesAsync(users.bob, ['editor', 'user'], 'scope2') + + await testUser('eve', ['admin', 'user'], 'scope1') + await testUser('bob', [], 'scope1') + await testUser('joe', ['editor', 'user'], 'scope1') + + await testUser('eve', [], 'scope2') + await testUser('bob', ['editor', 'user'], 'scope2') + await testUser('joe', [], 'scope2') + }) + + it('can add user to roles via user object', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + const eve = await Meteor.users.findOneAsync({ _id: users.eve }) + const bob = await Meteor.users.findOneAsync({ _id: users.bob }) + + await Roles.addUsersToRolesAsync(eve, ['admin', 'user']) + + await testUser('eve', ['admin', 'user']) + await testUser('bob', []) + await testUser('joe', []) + + await Roles.addUsersToRolesAsync(bob, ['editor']) + + await testUser('eve', ['admin', 'user']) + await testUser('bob', ['editor']) + await testUser('joe', []) + }) + + it('can add user to roles multiple times', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + await Roles.addUsersToRolesAsync(users.eve, ['admin', 'user']) + await Roles.addUsersToRolesAsync(users.eve, ['admin', 'user']) + + await testUser('eve', ['admin', 'user']) + await testUser('bob', []) + await testUser('joe', []) + + await Roles.addUsersToRolesAsync(users.bob, ['admin']) + await Roles.addUsersToRolesAsync(users.bob, ['editor']) + + await testUser('eve', ['admin', 'user']) + await testUser('bob', ['admin', 'editor']) + await testUser('joe', []) + }) + + it('can add user to roles multiple times by scope', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + await Roles.addUsersToRolesAsync(users.eve, ['admin', 'user'], 'scope1') + await Roles.addUsersToRolesAsync(users.eve, ['admin', 'user'], 'scope1') + + await testUser('eve', ['admin', 'user'], 'scope1') + await testUser('bob', [], 'scope1') + await testUser('joe', [], 'scope1') + + await Roles.addUsersToRolesAsync(users.bob, ['admin'], 'scope1') + await Roles.addUsersToRolesAsync(users.bob, ['editor'], 'scope1') + + await testUser('eve', ['admin', 'user'], 'scope1') + await testUser('bob', ['admin', 'editor'], 'scope1') + await testUser('joe', [], 'scope1') + }) + + it('can add multiple users to roles', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + await Roles.addUsersToRolesAsync([users.eve, users.bob], ['admin', 'user']) + + await testUser('eve', ['admin', 'user']) + await testUser('bob', ['admin', 'user']) + await testUser('joe', []) + + await Roles.addUsersToRolesAsync([users.bob, users.joe], ['editor', 'user']) + + await testUser('eve', ['admin', 'user']) + await testUser('bob', ['admin', 'editor', 'user']) + await testUser('joe', ['editor', 'user']) + }) + + it('can add multiple users to roles by scope', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + await Roles.addUsersToRolesAsync([users.eve, users.bob], ['admin', 'user'], 'scope1') + + await testUser('eve', ['admin', 'user'], 'scope1') + await testUser('bob', ['admin', 'user'], 'scope1') + await testUser('joe', [], 'scope1') + + await testUser('eve', [], 'scope2') + await testUser('bob', [], 'scope2') + await testUser('joe', [], 'scope2') + + await Roles.addUsersToRolesAsync([users.bob, users.joe], ['editor', 'user'], 'scope1') + await Roles.addUsersToRolesAsync([users.bob, users.joe], ['editor', 'user'], 'scope2') + + await testUser('eve', ['admin', 'user'], 'scope1') + await testUser('bob', ['admin', 'editor', 'user'], 'scope1') + await testUser('joe', ['editor', 'user'], 'scope1') + + await testUser('eve', [], 'scope2') + await testUser('bob', ['editor', 'user'], 'scope2') + await testUser('joe', ['editor', 'user'], 'scope2') + }) + + it('can remove individual users from roles', async function () { + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + // remove user role - one user + await Roles.addUsersToRolesAsync([users.eve, users.bob], ['editor', 'user']) + await testUser('eve', ['editor', 'user']) + await testUser('bob', ['editor', 'user']) + await Roles.removeUsersFromRolesAsync(users.eve, ['user']) + await testUser('eve', ['editor']) + await testUser('bob', ['editor', 'user']) + }) + + it('can remove user from roles multiple times', async function () { + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + // remove user role - one user + await Roles.addUsersToRolesAsync([users.eve, users.bob], ['editor', 'user']) + await testUser('eve', ['editor', 'user']) + await testUser('bob', ['editor', 'user']) + await Roles.removeUsersFromRolesAsync(users.eve, ['user']) + await testUser('eve', ['editor']) + await testUser('bob', ['editor', 'user']) + + // try remove again + await Roles.removeUsersFromRolesAsync(users.eve, ['user']) + await testUser('eve', ['editor']) + }) + + it('can remove users from roles via user object', async function () { + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + const eve = await Meteor.users.findOneAsync({ _id: users.eve }) + const bob = await Meteor.users.findOneAsync({ _id: users.bob }) + + // remove user role - one user + await Roles.addUsersToRolesAsync([eve, bob], ['editor', 'user']) + await testUser('eve', ['editor', 'user']) + await testUser('bob', ['editor', 'user']) + await Roles.removeUsersFromRolesAsync(eve, ['user']) + await testUser('eve', ['editor']) + await testUser('bob', ['editor', 'user']) + }) + + it('can remove individual users from roles by scope', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + // remove user role - one user + await Roles.addUsersToRolesAsync([users.eve, users.bob], ['editor', 'user'], 'scope1') + await Roles.addUsersToRolesAsync([users.joe, users.bob], ['admin'], 'scope2') + await testUser('eve', ['editor', 'user'], 'scope1') + await testUser('bob', ['editor', 'user'], 'scope1') + await testUser('joe', [], 'scope1') + await testUser('eve', [], 'scope2') + await testUser('bob', ['admin'], 'scope2') + await testUser('joe', ['admin'], 'scope2') + + await Roles.removeUsersFromRolesAsync(users.eve, ['user'], 'scope1') + await testUser('eve', ['editor'], 'scope1') + await testUser('bob', ['editor', 'user'], 'scope1') + await testUser('joe', [], 'scope1') + await testUser('eve', [], 'scope2') + await testUser('bob', ['admin'], 'scope2') + await testUser('joe', ['admin'], 'scope2') + }) + + it('can remove individual users from roles by scope through options', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + // remove user role - one user + await Roles.addUsersToRolesAsync([users.eve, users.bob], ['editor', 'user'], { scope: 'scope1' }) + await Roles.addUsersToRolesAsync([users.joe, users.bob], ['admin'], { scope: 'scope2' }) + await testUser('eve', ['editor', 'user'], 'scope1') + await testUser('bob', ['editor', 'user'], 'scope1') + await testUser('joe', [], 'scope1') + await testUser('eve', [], 'scope2') + await testUser('bob', ['admin'], 'scope2') + await testUser('joe', ['admin'], 'scope2') + + await Roles.removeUsersFromRolesAsync(users.eve, ['user'], { scope: 'scope1' }) + await testUser('eve', ['editor'], 'scope1') + await testUser('bob', ['editor', 'user'], 'scope1') + await testUser('joe', [], 'scope1') + await testUser('eve', [], 'scope2') + await testUser('bob', ['admin'], 'scope2') + await testUser('joe', ['admin'], 'scope2') + }) + + it('can remove multiple users from roles', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + // remove user role - two users + await Roles.addUsersToRolesAsync([users.eve, users.bob], ['editor', 'user']) + await testUser('eve', ['editor', 'user']) + await testUser('bob', ['editor', 'user']) + + assert.isFalse(await Roles.userIsInRoleAsync(users.joe, 'admin')) + await Roles.addUsersToRolesAsync([users.bob, users.joe], ['admin', 'user']) + await testUser('bob', ['admin', 'user', 'editor']) + await testUser('joe', ['admin', 'user']) + await Roles.removeUsersFromRolesAsync([users.bob, users.joe], ['admin']) + await testUser('bob', ['user', 'editor']) + await testUser('joe', ['user']) + }) + + it('can remove multiple users from roles by scope', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + // remove user role - one user + await Roles.addUsersToRolesAsync([users.eve, users.bob], ['editor', 'user'], 'scope1') + await Roles.addUsersToRolesAsync([users.joe, users.bob], ['admin'], 'scope2') + await testUser('eve', ['editor', 'user'], 'scope1') + await testUser('bob', ['editor', 'user'], 'scope1') + await testUser('joe', [], 'scope1') + await testUser('eve', [], 'scope2') + await testUser('bob', ['admin'], 'scope2') + await testUser('joe', ['admin'], 'scope2') + + await Roles.removeUsersFromRolesAsync([users.eve, users.bob], ['user'], 'scope1') + await testUser('eve', ['editor'], 'scope1') + await testUser('bob', ['editor'], 'scope1') + await testUser('joe', [], 'scope1') + await testUser('eve', [], 'scope2') + await testUser('bob', ['admin'], 'scope2') + await testUser('joe', ['admin'], 'scope2') + + await Roles.removeUsersFromRolesAsync([users.joe, users.bob], ['admin'], 'scope2') + await testUser('eve', [], 'scope2') + await testUser('bob', [], 'scope2') + await testUser('joe', [], 'scope2') + }) + + it('can remove multiple users from roles of any scope', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + // remove user role - one user + await Roles.addUsersToRolesAsync([users.eve, users.bob], ['editor', 'user'], 'scope1') + await Roles.addUsersToRolesAsync([users.joe, users.bob], ['user'], 'scope2') + await testUser('eve', ['editor', 'user'], 'scope1') + await testUser('bob', ['editor', 'user'], 'scope1') + await testUser('joe', [], 'scope1') + await testUser('eve', [], 'scope2') + await testUser('bob', ['user'], 'scope2') + await testUser('joe', ['user'], 'scope2') + + await Roles.removeUsersFromRolesAsync([users.eve, users.bob], ['user'], { anyScope: true }) + await testUser('eve', ['editor'], 'scope1') + await testUser('bob', ['editor'], 'scope1') + await testUser('joe', [], 'scope1') + await testUser('eve', [], 'scope2') + await testUser('bob', [], 'scope2') + await testUser('joe', ['user'], 'scope2') + }) + + it('can set user roles', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + const eve = await Meteor.users.findOneAsync({ _id: users.eve }) + const bob = await Meteor.users.findOneAsync({ _id: users.bob }) + + await Roles.setUserRolesAsync([users.eve, bob], ['editor', 'user']) + await testUser('eve', ['editor', 'user']) + await testUser('bob', ['editor', 'user']) + await testUser('joe', []) + + // use addUsersToRoles add some roles + await Roles.addUsersToRolesAsync([bob, users.joe], ['admin']) + await testUser('eve', ['editor', 'user']) + await testUser('bob', ['admin', 'editor', 'user']) + await testUser('joe', ['admin']) + + await Roles.setUserRolesAsync([eve, bob], ['user']) + await testUser('eve', ['user']) + await testUser('bob', ['user']) + await testUser('joe', ['admin']) + + await Roles.setUserRolesAsync(bob, 'editor') + await testUser('eve', ['user']) + await testUser('bob', ['editor']) + await testUser('joe', ['admin']) + + await Roles.setUserRolesAsync([users.joe, users.bob], []) + await testUser('eve', ['user']) + await testUser('bob', []) + await testUser('joe', []) + }) + + it('can set user roles by scope', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + const eve = await Meteor.users.findOneAsync({ _id: users.eve }) + const bob = await Meteor.users.findOneAsync({ _id: users.bob }) + const joe = await Meteor.users.findOneAsync({ _id: users.joe }) + + await Roles.setUserRolesAsync([users.eve, users.bob], ['editor', 'user'], 'scope1') + await Roles.setUserRolesAsync([users.bob, users.joe], ['admin'], 'scope2') + await testUser('eve', ['editor', 'user'], 'scope1') + await testUser('bob', ['editor', 'user'], 'scope1') + await testUser('joe', [], 'scope1') + await testUser('eve', [], 'scope2') + await testUser('bob', ['admin'], 'scope2') + await testUser('joe', ['admin'], 'scope2') + + // use addUsersToRoles add some roles + await Roles.addUsersToRolesAsync([users.eve, users.bob], ['admin'], 'scope1') + await Roles.addUsersToRolesAsync([users.bob, users.joe], ['editor'], 'scope2') + await testUser('eve', ['admin', 'editor', 'user'], 'scope1') + await testUser('bob', ['admin', 'editor', 'user'], 'scope1') + await testUser('joe', [], 'scope1') + await testUser('eve', [], 'scope2') + await testUser('bob', ['admin', 'editor'], 'scope2') + await testUser('joe', ['admin', 'editor'], 'scope2') + + await Roles.setUserRolesAsync([eve, bob], ['user'], 'scope1') + await Roles.setUserRolesAsync([eve, joe], ['editor'], 'scope2') + await testUser('eve', ['user'], 'scope1') + await testUser('bob', ['user'], 'scope1') + await testUser('joe', [], 'scope1') + await testUser('eve', ['editor'], 'scope2') + await testUser('bob', ['admin', 'editor'], 'scope2') + await testUser('joe', ['editor'], 'scope2') + + await Roles.setUserRolesAsync(bob, 'editor', 'scope1') + await testUser('eve', ['user'], 'scope1') + await testUser('bob', ['editor'], 'scope1') + await testUser('joe', [], 'scope1') + await testUser('eve', ['editor'], 'scope2') + await testUser('bob', ['admin', 'editor'], 'scope2') + await testUser('joe', ['editor'], 'scope2') + + const bobRoles1 = await Roles.getRolesForUserAsync(users.bob, { anyScope: true, fullObjects: true }) + const joeRoles1 = await Roles.getRolesForUserAsync(users.joe, { anyScope: true, fullObjects: true }) + assert.isTrue(bobRoles1.map(r => r.scope).includes('scope1')) + assert.isFalse(joeRoles1.map(r => r.scope).includes('scope1')) + + await Roles.setUserRolesAsync([bob, users.joe], [], 'scope1') + await testUser('eve', ['user'], 'scope1') + await testUser('bob', [], 'scope1') + await testUser('joe', [], 'scope1') + await testUser('eve', ['editor'], 'scope2') + await testUser('bob', ['admin', 'editor'], 'scope2') + await testUser('joe', ['editor'], 'scope2') + + // When roles in a given scope are removed, we do not want any dangling database content for that scope. + const bobRoles2 = await Roles.getRolesForUserAsync(users.bob, { anyScope: true, fullObjects: true }) + const joeRoles2 = await Roles.getRolesForUserAsync(users.joe, { anyScope: true, fullObjects: true }) + assert.isFalse(bobRoles2.map(r => r.scope).includes('scope1')) + assert.isFalse(joeRoles2.map(r => r.scope).includes('scope1')) + }) + + it('can set user roles by scope including GLOBAL_SCOPE', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('editor') + + const eve = await Meteor.users.findOneAsync({ _id: users.eve }) + + await Roles.addUsersToRolesAsync(eve, 'admin', Roles.GLOBAL_SCOPE) + await testUser('eve', ['admin'], 'scope1') + await testUser('eve', ['admin']) + + await Roles.setUserRolesAsync(eve, 'editor', Roles.GLOBAL_SCOPE) + await testUser('eve', ['editor'], 'scope2') + await testUser('eve', ['editor']) + }) + + it('can set user roles by scope and anyScope', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('editor') + + const eve = await Meteor.users.findOneAsync({ _id: users.eve }) + + const eveRoles = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(eveRoles.map(obj => { delete obj._id; return obj }), []) + + await Roles.addUsersToRolesAsync(eve, 'admin') + + const eveRoles2 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(eveRoles2.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'admin' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [{ _id: 'admin' }] + }]) + + await Roles.setUserRolesAsync(eve, 'editor', { anyScope: true, scope: 'scope2' }) + + const eveRoles3 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(eveRoles3.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'editor' }, + scope: 'scope2', + user: { _id: users.eve }, + inheritedRoles: [{ _id: 'editor' }] + }]) + }) + + it('can get all roles', async function () { + for (const role of roles) { + await Roles.createRoleAsync(role) + } + + // compare roles, sorted alphabetically + const expected = roles + const actual = Roles.getAllRoles().fetch().map(r => r._id) + + assert.sameMembers(actual, expected) + + assert.sameMembers(Roles.getAllRoles({ sort: { _id: -1 } }).fetch().map(r => r._id), expected.reverse()) + }) + + it('get an empty list of roles for an empty user', async function () { + assert.sameMembers(await Roles.getRolesForUserAsync(undefined), []) + assert.sameMembers(await Roles.getRolesForUserAsync(null), []) + assert.sameMembers(await Roles.getRolesForUserAsync({}), []) + }) + + it('get an empty list of roles for non-existant user', async function () { + assert.sameMembers(await Roles.getRolesForUserAsync('1'), []) + assert.sameMembers(await Roles.getRolesForUserAsync('1', 'scope1'), []) + }) + + it('can get all roles for user', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + + const userId = users.eve + let userObj + + // by userId + assert.sameMembers(await Roles.getRolesForUserAsync(userId), []) + + // by user object + userObj = await Meteor.users.findOneAsync({ _id: userId }) + assert.sameMembers(await Roles.getRolesForUserAsync(userObj), []) + + await Roles.addUsersToRolesAsync(userId, ['admin', 'user']) + + // by userId + assert.sameMembers(await Roles.getRolesForUserAsync(userId), ['admin', 'user']) + + // by user object + userObj = await Meteor.users.findOneAsync({ _id: userId }) + assert.sameMembers(await Roles.getRolesForUserAsync(userObj), ['admin', 'user']) + + const userRoles = await Roles.getRolesForUserAsync(userId, { fullObjects: true }) + assert.sameDeepMembers(userRoles.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'admin' }, + scope: null, + user: { _id: userId }, + inheritedRoles: [{ _id: 'admin' }] + }, { + role: { _id: 'user' }, + scope: null, + user: { _id: userId }, + inheritedRoles: [{ _id: 'user' }] + }]) + }) + + it('can get all roles for user by scope', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + + const userId = users.eve + let userObj + + // by userId + assert.sameMembers(await Roles.getRolesForUserAsync(userId, 'scope1'), []) + + // by user object + userObj = await Meteor.users.findOneAsync({ _id: userId }) + assert.sameMembers(await Roles.getRolesForUserAsync(userObj, 'scope1'), []) + + // add roles + await Roles.addUsersToRolesAsync(userId, ['admin', 'user'], 'scope1') + await Roles.addUsersToRolesAsync(userId, ['admin'], 'scope2') + + // by userId + assert.sameMembers(await Roles.getRolesForUserAsync(userId, 'scope1'), ['admin', 'user']) + assert.sameMembers(await Roles.getRolesForUserAsync(userId, 'scope2'), ['admin']) + assert.sameMembers(await Roles.getRolesForUserAsync(userId), []) + + // by user object + userObj = await Meteor.users.findOneAsync({ _id: userId }) + assert.sameMembers(await Roles.getRolesForUserAsync(userObj, 'scope1'), ['admin', 'user']) + assert.sameMembers(await Roles.getRolesForUserAsync(userObj, 'scope2'), ['admin']) + assert.sameMembers(await Roles.getRolesForUserAsync(userObj), []) + + const userRoles = await Roles.getRolesForUserAsync(userId, { fullObjects: true, scope: 'scope1' }) + assert.sameDeepMembers(userRoles.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'admin' }, + scope: 'scope1', + user: { _id: userId }, + inheritedRoles: [{ _id: 'admin' }] + }, { + role: { _id: 'user' }, + scope: 'scope1', + user: { _id: userId }, + inheritedRoles: [{ _id: 'user' }] + }]) + const userRoles2 = await Roles.getRolesForUserAsync(userId, { fullObjects: true, scope: 'scope2' }) + assert.sameDeepMembers(userRoles2.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'admin' }, + scope: 'scope2', + user: { _id: userId }, + inheritedRoles: [{ _id: 'admin' }] + }]) + + const userRoles3 = await Roles.getRolesForUserAsync(userId, { fullObjects: true, anyScope: true }) + assert.sameDeepMembers(userRoles3.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'admin' }, + scope: 'scope1', + user: { _id: userId }, + inheritedRoles: [{ _id: 'admin' }] + }, { + role: { _id: 'user' }, + scope: 'scope1', + user: { _id: userId }, + inheritedRoles: [{ _id: 'user' }] + }, { + role: { _id: 'admin' }, + scope: 'scope2', + user: { _id: userId }, + inheritedRoles: [{ _id: 'admin' }] + }]) + + await Roles.createRoleAsync('PERMISSION') + await Roles.addRolesToParentAsync('PERMISSION', 'user') + + const userRoles4 = await Roles.getRolesForUserAsync(userId, { fullObjects: true, scope: 'scope1' }) + assert.sameDeepMembers(userRoles4.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'admin' }, + scope: 'scope1', + user: { _id: userId }, + inheritedRoles: [{ _id: 'admin' }] + }, { + role: { _id: 'user' }, + scope: 'scope1', + user: { _id: userId }, + inheritedRoles: [{ _id: 'user' }, { _id: 'PERMISSION' }] + }]) + const userRoles5 = await Roles.getRolesForUserAsync(userId, { fullObjects: true, scope: 'scope2' }) + assert.sameDeepMembers(userRoles5.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'admin' }, + scope: 'scope2', + user: { _id: userId }, + inheritedRoles: [{ _id: 'admin' }] + }]) + assert.sameMembers(await Roles.getRolesForUserAsync(userId, { scope: 'scope1' }), ['admin', 'user', 'PERMISSION']) + assert.sameMembers(await Roles.getRolesForUserAsync(userId, { scope: 'scope2' }), ['admin']) + + const userRoles6 = await Roles.getRolesForUserAsync(userId, { fullObjects: true, anyScope: true }) + assert.sameDeepMembers(userRoles6.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'admin' }, + scope: 'scope1', + user: { _id: userId }, + inheritedRoles: [{ _id: 'admin' }] + }, { + role: { _id: 'user' }, + scope: 'scope1', + user: { _id: userId }, + inheritedRoles: [{ _id: 'user' }, { _id: 'PERMISSION' }] + }, { + role: { _id: 'admin' }, + scope: 'scope2', + user: { _id: userId }, + inheritedRoles: [{ _id: 'admin' }] + }]) + assert.sameMembers(await Roles.getRolesForUserAsync(userId, { anyScope: true }), ['admin', 'user', 'PERMISSION']) + + const userRoles7 = await Roles.getRolesForUserAsync(userId, { fullObjects: true, scope: 'scope1', onlyAssigned: true }) + assert.sameDeepMembers(userRoles7.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'admin' }, + scope: 'scope1', + user: { _id: userId }, + inheritedRoles: [{ _id: 'admin' }] + }, { + role: { _id: 'user' }, + scope: 'scope1', + user: { _id: userId }, + inheritedRoles: [{ _id: 'user' }, { _id: 'PERMISSION' }] + }]) + const userRoles8 = await Roles.getRolesForUserAsync(userId, { fullObjects: true, scope: 'scope2', onlyAssigned: true }) + assert.sameDeepMembers(userRoles8.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'admin' }, + scope: 'scope2', + user: { _id: userId }, + inheritedRoles: [{ _id: 'admin' }] + }]) + assert.sameMembers(await Roles.getRolesForUserAsync(userId, { scope: 'scope1', onlyAssigned: true }), ['admin', 'user']) + assert.sameMembers(await Roles.getRolesForUserAsync(userId, { scope: 'scope2', onlyAssigned: true }), ['admin']) + + const userRoles9 = await Roles.getRolesForUserAsync(userId, { fullObjects: true, anyScope: true, onlyAssigned: true }) + assert.sameDeepMembers(userRoles9.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'admin' }, + scope: 'scope1', + user: { _id: userId }, + inheritedRoles: [{ _id: 'admin' }] + }, { + role: { _id: 'user' }, + scope: 'scope1', + user: { _id: userId }, + inheritedRoles: [{ _id: 'user' }, { _id: 'PERMISSION' }] + }, { + role: { _id: 'admin' }, + scope: 'scope2', + user: { _id: userId }, + inheritedRoles: [{ _id: 'admin' }] + }]) + assert.sameMembers(await Roles.getRolesForUserAsync(userId, { anyScope: true, onlyAssigned: true }), ['admin', 'user']) + }) + + it('can get only scoped roles for user', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + + const userId = users.eve + + // add roles + await Roles.addUsersToRolesAsync(userId, ['user'], 'scope1') + await Roles.addUsersToRolesAsync(userId, ['admin']) + + await Roles.createRoleAsync('PERMISSION') + await Roles.addRolesToParentAsync('PERMISSION', 'user') + + assert.sameMembers(await Roles.getRolesForUserAsync(userId, { onlyScoped: true, scope: 'scope1' }), ['user', 'PERMISSION']) + assert.sameMembers(await Roles.getRolesForUserAsync(userId, { onlyScoped: true, onlyAssigned: true, scope: 'scope1' }), ['user']) + const userRoles = await Roles.getRolesForUserAsync(userId, { onlyScoped: true, fullObjects: true, scope: 'scope1' }) + assert.sameDeepMembers(userRoles.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'user' }, + scope: 'scope1', + user: { _id: userId }, + inheritedRoles: [{ _id: 'user' }, { _id: 'PERMISSION' }] + }]) + }) + + it('can get all roles for user by scope with periods in name', async function () { + await Roles.createRoleAsync('admin') + + await Roles.addUsersToRolesAsync(users.joe, ['admin'], 'example.k12.va.us') + + assert.sameMembers(await Roles.getRolesForUserAsync(users.joe, 'example.k12.va.us'), ['admin']) + }) + + it('can get all roles for user by scope including Roles.GLOBAL_SCOPE', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + const userId = users.eve + + await Roles.addUsersToRolesAsync([users.eve], ['editor'], Roles.GLOBAL_SCOPE) + await Roles.addUsersToRolesAsync([users.eve], ['admin', 'user'], 'scope1') + + // by userId + assert.sameMembers(await Roles.getRolesForUserAsync(userId, 'scope1'), ['editor', 'admin', 'user']) + assert.sameMembers(await Roles.getRolesForUserAsync(userId), ['editor']) + + // by user object + const userObj = await Meteor.users.findOneAsync({ _id: userId }) + assert.sameMembers(await Roles.getRolesForUserAsync(userObj, 'scope1'), ['editor', 'admin', 'user']) + assert.sameMembers(await Roles.getRolesForUserAsync(userObj), ['editor']) + }) + + describe('getRolesForUser', function () { + it('should not return null entries if user has no roles for scope', async function () { + await Roles.createRoleAsync('editor') + + const userId = users.eve + let userObj + + // by userId + assert.sameMembers(await Roles.getRolesForUserAsync(userId, 'scope1'), []) + assert.sameMembers(await Roles.getRolesForUserAsync(userId), []) + + // by user object + userObj = await Meteor.users.findOneAsync({ _id: userId }) + assert.sameMembers(await Roles.getRolesForUserAsync(userObj, 'scope1'), []) + assert.sameMembers(await Roles.getRolesForUserAsync(userObj), []) + + await Roles.addUsersToRolesAsync([users.eve], ['editor'], Roles.GLOBAL_SCOPE) + + // by userId + assert.sameMembers(await Roles.getRolesForUserAsync(userId, 'scope1'), ['editor']) + assert.sameMembers(await Roles.getRolesForUserAsync(userId), ['editor']) + + // by user object + userObj = await Meteor.users.findOneAsync({ _id: userId }) + assert.sameMembers(await Roles.getRolesForUserAsync(userObj, 'scope1'), ['editor']) + assert.sameMembers(await Roles.getRolesForUserAsync(userObj), ['editor']) + }) + + it('should not fail during a call of addUsersToRoles', async function () { + await Roles.createRoleAsync('editor') + + const userId = users.eve + const promises = [] + const interval = setInterval(() => { + promises.push(Promise.resolve().then(async () => { + await Roles.getRolesForUserAsync(userId) + })) + }, 0) + + await Roles.addUsersToRolesAsync([users.eve], ['editor'], Roles.GLOBAL_SCOPE) + clearInterval(interval) + + return Promise.all(promises) + }) + }) + + it('returns an empty list of scopes for null as user-id', async function () { + assert.sameMembers(await Roles.getScopesForUserAsync(undefined), []) + assert.sameMembers(await Roles.getScopesForUserAsync(null), []) + assert.sameMembers(await Roles.getScopesForUserAsync('foo'), []) + assert.sameMembers(await Roles.getScopesForUserAsync({}), []) + assert.sameMembers(await Roles.getScopesForUserAsync({ _id: 'foo' }), []) + }) + + it('can get all scopes for user', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + const userId = users.eve + + await Roles.addUsersToRolesAsync([users.eve], ['editor'], 'scope1') + await Roles.addUsersToRolesAsync([users.eve], ['admin', 'user'], 'scope2') + + // by userId + assert.sameMembers(await Roles.getScopesForUserAsync(userId), ['scope1', 'scope2']) + + // by user object + const userObj = await Meteor.users.findOneAsync({ _id: userId }) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj), ['scope1', 'scope2']) + }) + + it('can get all scopes for user by role', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + const userId = users.eve + + await Roles.addUsersToRolesAsync([users.eve], ['editor'], 'scope1') + await Roles.addUsersToRolesAsync([users.eve], ['editor', 'user'], 'scope2') + + // by userId + assert.sameMembers(await Roles.getScopesForUserAsync(userId, 'user'), ['scope2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userId, 'editor'), ['scope1', 'scope2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userId, 'admin'), []) + + // by user object + const userObj = await Meteor.users.findOneAsync({ _id: userId }) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, 'user'), ['scope2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, 'editor'), ['scope1', 'scope2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, 'admin'), []) + }) + + it('getScopesForUser returns [] when not using scopes', async function () { + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + const userId = users.eve + + await Roles.addUsersToRolesAsync([users.eve], ['editor', 'user']) + + // by userId + assert.sameMembers(await Roles.getScopesForUserAsync(userId), []) + assert.sameMembers(await Roles.getScopesForUserAsync(userId, 'editor'), []) + assert.sameMembers(await Roles.getScopesForUserAsync(userId, ['editor']), []) + assert.sameMembers(await Roles.getScopesForUserAsync(userId, ['editor', 'user']), []) + + // by user object + const userObj = await Meteor.users.findOneAsync({ _id: userId }) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj), []) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, 'editor'), []) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, ['editor']), []) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, ['editor', 'user']), []) + }) + + it('can get all groups for user by role array', async function () { + const userId = users.eve + + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + await Roles.createRoleAsync('moderator') + await Roles.createRoleAsync('admin') + + await Roles.addUsersToRolesAsync([users.eve], ['editor'], 'group1') + await Roles.addUsersToRolesAsync([users.eve], ['editor', 'user'], 'group2') + await Roles.addUsersToRolesAsync([users.eve], ['moderator'], 'group3') + + // by userId, one role + assert.sameMembers(await Roles.getScopesForUserAsync(userId, ['user']), ['group2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userId, ['editor']), ['group1', 'group2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userId, ['admin']), []) + + // by userId, multiple roles + assert.sameMembers(await Roles.getScopesForUserAsync(userId, ['editor', 'user']), ['group1', 'group2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userId, ['editor', 'moderator']), ['group1', 'group2', 'group3']) + assert.sameMembers(await Roles.getScopesForUserAsync(userId, ['user', 'moderator']), ['group2', 'group3']) + + // by user object, one role + const userObj = await Meteor.users.findOneAsync({ _id: userId }) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, ['user']), ['group2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, ['editor']), ['group1', 'group2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, ['admin']), []) + + // by user object, multiple roles + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, ['editor', 'user']), ['group1', 'group2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, ['editor', 'moderator']), ['group1', 'group2', 'group3']) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, ['user', 'moderator']), ['group2', 'group3']) + }) + + it('getting all scopes for user does not include GLOBAL_SCOPE', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + const userId = users.eve + + await Roles.addUsersToRolesAsync([users.eve], ['editor'], 'scope1') + await Roles.addUsersToRolesAsync([users.eve], ['editor', 'user'], 'scope2') + await Roles.addUsersToRolesAsync([users.eve], ['editor', 'user', 'admin'], Roles.GLOBAL_SCOPE) + + // by userId + assert.sameMembers(await Roles.getScopesForUserAsync(userId, 'user'), ['scope2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userId, 'editor'), ['scope1', 'scope2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userId, 'admin'), []) + assert.sameMembers(await Roles.getScopesForUserAsync(userId, ['user']), ['scope2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userId, ['editor']), ['scope1', 'scope2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userId, ['admin']), []) + assert.sameMembers(await Roles.getScopesForUserAsync(userId, ['user', 'editor', 'admin']), ['scope1', 'scope2']) + + // by user object + const userObj = await Meteor.users.findOneAsync({ _id: userId }) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, 'user'), ['scope2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, 'editor'), ['scope1', 'scope2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, 'admin'), []) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, ['user']), ['scope2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, ['editor']), ['scope1', 'scope2']) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, ['admin']), []) + assert.sameMembers(await Roles.getScopesForUserAsync(userObj, ['user', 'editor', 'admin']), ['scope1', 'scope2']) + }) + + it('can get all users in role', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + await Roles.addUsersToRolesAsync([users.eve, users.joe], ['admin', 'user']) + await Roles.addUsersToRolesAsync([users.bob, users.joe], ['editor']) + + const expected = [users.eve, users.joe] + const cursor = await Roles.getUsersInRoleAsync('admin') + const actual = cursor.fetch().map(r => r._id) + + assert.sameMembers(actual, expected) + }) + + it('can get all users in role by scope', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + + await Roles.addUsersToRolesAsync([users.eve, users.joe], ['admin', 'user'], 'scope1') + await Roles.addUsersToRolesAsync([users.bob, users.joe], ['admin'], 'scope2') + + let expected = [users.eve, users.joe] + const cursor1 = await Roles.getUsersInRoleAsync('admin', 'scope1') + let actual = cursor1.fetch().map(r => r._id) + + assert.sameMembers(actual, expected) + + expected = [users.eve, users.joe] + const cursor2 = await Roles.getUsersInRoleAsync('admin', { scope: 'scope1' }) + actual = cursor2.fetch().map(r => r._id) + assert.sameMembers(actual, expected) + + expected = [users.eve, users.bob, users.joe] + const cursor3 = await Roles.getUsersInRoleAsync('admin', { anyScope: true }) + actual = cursor3.fetch().map(r => r._id) + assert.sameMembers(actual, expected) + + const cursor4 = await Roles.getUsersInRoleAsync('admin') + actual = cursor4.fetch().map(r => r._id) + assert.sameMembers(actual, []) + }) + + // it('can get all users in role by scope including Roles.GLOBAL_SCOPE', function () { + // Roles.createRoleAsync('admin') + // Roles.createRoleAsync('user') + // + // Roles.addUsersToRolesAsync([users.eve], ['admin', 'user'], Roles.GLOBAL_SCOPE) + // Roles.addUsersToRolesAsync([users.bob, users.joe], ['admin'], 'scope2') + // + // let expected = [users.eve] + // let actual = await Roles.getUsersInRoleAsync('admin', 'scope1').fetch().map(r => r._id) + // + // assert.sameMembers(actual, expected) + // + // expected = [users.eve, users.bob, users.joe] + // actual = await Roles.getUsersInRoleAsync('admin', 'scope2').fetch().map(r => r._id) + // + // assert.sameMembers(actual, expected) + // + // expected = [users.eve] + // actual = await Roles.getUsersInRoleAsync('admin').fetch().map(r => r._id) + // + // assert.sameMembers(actual, expected) + // + // expected = [users.eve, users.bob, users.joe] + // actual = await Roles.getUsersInRoleAsync('admin', { anyScope: true }).fetch().map(r => r._id) + // + // assert.sameMembers(actual, expected) + // }) + // + // it('can get all users in role by scope excluding Roles.GLOBAL_SCOPE', function () { + // Roles.createRoleAsync('admin') + // + // Roles.addUsersToRolesAsync([users.eve], ['admin'], Roles.GLOBAL_SCOPE) + // Roles.addUsersToRolesAsync([users.bob], ['admin'], 'scope1') + // + // let expected = [users.eve] + // let actual = await Roles.getUsersInRoleAsync('admin').fetch().map(r => r._id) + // assert.sameMembers(actual, expected) + // + // expected = [users.eve, users.bob] + // actual = await Roles.getUsersInRoleAsync('admin', { scope: 'scope1' }).fetch().map(r => r._id) + // assert.sameMembers(actual, expected) + // + // expected = [users.bob] + // actual = await Roles.getUsersInRoleAsync('admin', { scope: 'scope1', onlyScoped: true }).fetch().map(r => r._id) + // assert.sameMembers(actual, expected) + // }) + + it('can get all users in role by scope and passes through mongo query arguments', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + + await Roles.addUsersToRolesAsync([users.eve, users.joe], ['admin', 'user'], 'scope1') + await Roles.addUsersToRolesAsync([users.bob, users.joe], ['admin'], 'scope2') + + const cursor = await Roles.getUsersInRoleAsync('admin', 'scope1', { fields: { username: 0 }, limit: 1 }) + const results = cursor.fetch() + + assert.equal(1, results.length) + assert.isTrue(hasProp(results[0], '_id')) + assert.isFalse(hasProp(results[0], 'username')) + }) + + it('can use Roles.GLOBAL_SCOPE to assign blanket roles', async function () { + await Roles.createRoleAsync('admin') + + await Roles.addUsersToRolesAsync([users.joe, users.bob], ['admin'], Roles.GLOBAL_SCOPE) + + await testUser('eve', [], 'scope1') + await testUser('joe', ['admin'], 'scope2') + await testUser('joe', ['admin'], 'scope1') + await testUser('bob', ['admin'], 'scope2') + await testUser('bob', ['admin'], 'scope1') + + await Roles.removeUsersFromRolesAsync(users.joe, ['admin'], Roles.GLOBAL_SCOPE) + + await testUser('eve', [], 'scope1') + await testUser('joe', [], 'scope2') + await testUser('joe', [], 'scope1') + await testUser('bob', ['admin'], 'scope2') + await testUser('bob', ['admin'], 'scope1') + }) + + it('Roles.GLOBAL_SCOPE is independent of other scopes', async function () { + await Roles.createRoleAsync('admin') + + await Roles.addUsersToRolesAsync([users.joe, users.bob], ['admin'], 'scope5') + await Roles.addUsersToRolesAsync([users.joe, users.bob], ['admin'], Roles.GLOBAL_SCOPE) + + await testUser('eve', [], 'scope1') + await testUser('joe', ['admin'], 'scope5') + await testUser('joe', ['admin'], 'scope2') + await testUser('joe', ['admin'], 'scope1') + await testUser('bob', ['admin'], 'scope5') + await testUser('bob', ['admin'], 'scope2') + await testUser('bob', ['admin'], 'scope1') + + await Roles.removeUsersFromRolesAsync(users.joe, ['admin'], Roles.GLOBAL_SCOPE) + + await testUser('eve', [], 'scope1') + await testUser('joe', ['admin'], 'scope5') + await testUser('joe', [], 'scope2') + await testUser('joe', [], 'scope1') + await testUser('bob', ['admin'], 'scope5') + await testUser('bob', ['admin'], 'scope2') + await testUser('bob', ['admin'], 'scope1') + }) + + it('Roles.GLOBAL_SCOPE also checked when scope not specified', async function () { + await Roles.createRoleAsync('admin') + + await Roles.addUsersToRolesAsync(users.joe, 'admin', Roles.GLOBAL_SCOPE) + + await testUser('joe', ['admin']) + + await Roles.removeUsersFromRolesAsync(users.joe, 'admin', Roles.GLOBAL_SCOPE) + + await testUser('joe', []) + }) + + it('can use \'.\' in scope name', async function () { + await Roles.createRoleAsync('admin') + + await Roles.addUsersToRolesAsync(users.joe, ['admin'], 'example.com') + await testUser('joe', ['admin'], 'example.com') + }) + + it('can use multiple periods in scope name', async function () { + await Roles.createRoleAsync('admin') + + await Roles.addUsersToRolesAsync(users.joe, ['admin'], 'example.k12.va.us') + await testUser('joe', ['admin'], 'example.k12.va.us') + }) + + it('renaming of roles', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + + await Roles.setUserRolesAsync([users.eve, users.bob], ['editor', 'user'], 'scope1') + await Roles.setUserRolesAsync([users.bob, users.joe], ['user', 'admin'], 'scope2') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'editor', 'scope1')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'editor', 'scope2')) + + assert.isFalse(await Roles.userIsInRoleAsync(users.joe, 'admin', 'scope1')) + assert.isTrue(await Roles.userIsInRoleAsync(users.joe, 'admin', 'scope2')) + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'user', 'scope1')) + assert.isTrue(await Roles.userIsInRoleAsync(users.bob, 'user', 'scope1')) + assert.isFalse(await Roles.userIsInRoleAsync(users.joe, 'user', 'scope1')) + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'user', 'scope2')) + assert.isTrue(await Roles.userIsInRoleAsync(users.bob, 'user', 'scope2')) + assert.isTrue(await Roles.userIsInRoleAsync(users.joe, 'user', 'scope2')) + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'user2', 'scope1')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'user2', 'scope2')) + + await Roles.renameRoleAsync('user', 'user2') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'editor', 'scope1')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'editor', 'scope2')) + + assert.isFalse(await Roles.userIsInRoleAsync(users.joe, 'admin', 'scope1')) + assert.isTrue(await Roles.userIsInRoleAsync(users.joe, 'admin', 'scope2')) + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'user2', 'scope1')) + assert.isTrue(await Roles.userIsInRoleAsync(users.bob, 'user2', 'scope1')) + assert.isFalse(await Roles.userIsInRoleAsync(users.joe, 'user2', 'scope1')) + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'user2', 'scope2')) + assert.isTrue(await Roles.userIsInRoleAsync(users.bob, 'user2', 'scope2')) + assert.isTrue(await Roles.userIsInRoleAsync(users.joe, 'user2', 'scope2')) + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'user', 'scope1')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'user', 'scope2')) + }) + + it('_addUserToRole', async function () { + await Roles.createRoleAsync('admin') + + const userRoles = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(userRoles.map(obj => { delete obj._id; return obj }), []) + + const roles = await Roles._addUserToRoleAsync(users.eve, 'admin', { scope: null, ifExists: false }) + assert.hasAnyKeys(roles, 'insertedId') + + const userRoles2 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(userRoles2.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'admin' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [{ _id: 'admin' }] + }]) + + const roles2 = await Roles._addUserToRoleAsync(users.eve, 'admin', { scope: null, ifExists: false }) + assert.hasAnyKeys(roles2, 'insertedId') + + const roles3 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(roles3.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'admin' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [{ _id: 'admin' }] + }]) + }) + + it('_removeUserFromRole', async function () { + await Roles.createRoleAsync('admin') + + await Roles.addUsersToRolesAsync(users.eve, 'admin') + + const rolesForUser = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(rolesForUser.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'admin' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [{ _id: 'admin' }] + }]) + + Roles._removeUserFromRole(users.eve, 'admin', { scope: null }) + + const rolesForUser2 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(rolesForUser2.map(obj => { delete obj._id; return obj }), []) + }) + + it('keep assigned roles', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('ALL_PERMISSIONS') + await Roles.createRoleAsync('VIEW_PERMISSION') + await Roles.createRoleAsync('EDIT_PERMISSION') + await Roles.createRoleAsync('DELETE_PERMISSION') + await Roles.addRolesToParentAsync('ALL_PERMISSIONS', 'user') + await Roles.addRolesToParentAsync('EDIT_PERMISSION', 'ALL_PERMISSIONS') + await Roles.addRolesToParentAsync('VIEW_PERMISSION', 'ALL_PERMISSIONS') + await Roles.addRolesToParentAsync('DELETE_PERMISSION', 'admin') + + await Roles.addUsersToRolesAsync(users.eve, ['user']) + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'VIEW_PERMISSION')) + + const rolesForUser = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(rolesForUser.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'user' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'user' }, + { _id: 'ALL_PERMISSIONS' }, + { _id: 'EDIT_PERMISSION' }, + { _id: 'VIEW_PERMISSION' } + ] + }]) + + await Roles.addUsersToRolesAsync(users.eve, 'VIEW_PERMISSION') + + assert.eventually.isTrue(Roles.userIsInRoleAsync(users.eve, 'VIEW_PERMISSION')) + + const rolesForUser2 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(rolesForUser2.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'user' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'user' }, + { _id: 'ALL_PERMISSIONS' }, + { _id: 'EDIT_PERMISSION' }, + { _id: 'VIEW_PERMISSION' } + ] + }, { + role: { _id: 'VIEW_PERMISSION' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'VIEW_PERMISSION' } + ] + }]) + + await Roles.removeUsersFromRolesAsync(users.eve, 'user') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'VIEW_PERMISSION')) + + const rolesForUser3 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(rolesForUser3.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'VIEW_PERMISSION' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'VIEW_PERMISSION' } + ] + }]) + + await Roles.removeUsersFromRolesAsync(users.eve, 'VIEW_PERMISSION') + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'VIEW_PERMISSION')) + + const rolesForUser4 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(rolesForUser4.map(obj => { delete obj._id; return obj }), []) + }) + + it('adds children of the added role to the assignments', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('ALBUM.ADMIN') + await Roles.createRoleAsync('ALBUM.VIEW') + await Roles.createRoleAsync('TRACK.ADMIN') + await Roles.createRoleAsync('TRACK.VIEW') + + await Roles.addRolesToParentAsync('ALBUM.VIEW', 'ALBUM.ADMIN') + await Roles.addRolesToParentAsync('TRACK.VIEW', 'TRACK.ADMIN') + + await Roles.addRolesToParentAsync('ALBUM.ADMIN', 'admin') + + await Roles.addUsersToRolesAsync(users.eve, ['admin']) + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'TRACK.VIEW')) + + await Roles.addRolesToParentAsync('TRACK.ADMIN', 'admin') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'TRACK.VIEW')) + }) + + it('removes children of the removed role from the assignments', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('ALBUM.ADMIN') + await Roles.createRoleAsync('ALBUM.VIEW') + await Roles.createRoleAsync('TRACK.ADMIN') + await Roles.createRoleAsync('TRACK.VIEW') + + await Roles.addRolesToParentAsync('ALBUM.VIEW', 'ALBUM.ADMIN') + await Roles.addRolesToParentAsync('TRACK.VIEW', 'TRACK.ADMIN') + + await Roles.addRolesToParentAsync('ALBUM.ADMIN', 'admin') + await Roles.addRolesToParentAsync('TRACK.ADMIN', 'admin') + + await Roles.addUsersToRolesAsync(users.eve, ['admin']) + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'TRACK.VIEW')) + + await Roles.removeRolesFromParentAsync('TRACK.ADMIN', 'admin') + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'TRACK.VIEW')) + }) + + it('modify assigned hierarchical roles', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('ALL_PERMISSIONS') + await Roles.createRoleAsync('VIEW_PERMISSION') + await Roles.createRoleAsync('EDIT_PERMISSION') + await Roles.createRoleAsync('DELETE_PERMISSION') + await Roles.addRolesToParentAsync('ALL_PERMISSIONS', 'user') + await Roles.addRolesToParentAsync('EDIT_PERMISSION', 'ALL_PERMISSIONS') + await Roles.addRolesToParentAsync('VIEW_PERMISSION', 'ALL_PERMISSIONS') + await Roles.addRolesToParentAsync('DELETE_PERMISSION', 'admin') + + await Roles.addUsersToRolesAsync(users.eve, ['user']) + await Roles.addUsersToRolesAsync(users.eve, ['ALL_PERMISSIONS'], 'scope') + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'MODERATE_PERMISSION')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'MODERATE_PERMISSION', 'scope')) + + const usersRoles = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'user' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'user' }, + { _id: 'ALL_PERMISSIONS' }, + { _id: 'EDIT_PERMISSION' }, + { _id: 'VIEW_PERMISSION' } + ] + }, { + role: { _id: 'ALL_PERMISSIONS' }, + scope: 'scope', + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'ALL_PERMISSIONS' }, + { _id: 'EDIT_PERMISSION' }, + { _id: 'VIEW_PERMISSION' } + ] + }]) + + await Roles.createRoleAsync('MODERATE_PERMISSION') + + await Roles.addRolesToParentAsync('MODERATE_PERMISSION', 'ALL_PERMISSIONS') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'MODERATE_PERMISSION')) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'MODERATE_PERMISSION', 'scope')) + + const usersRoles2 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles2.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'user' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'user' }, + { _id: 'ALL_PERMISSIONS' }, + { _id: 'EDIT_PERMISSION' }, + { _id: 'VIEW_PERMISSION' }, + { _id: 'MODERATE_PERMISSION' } + ] + }, { + role: { _id: 'ALL_PERMISSIONS' }, + scope: 'scope', + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'ALL_PERMISSIONS' }, + { _id: 'EDIT_PERMISSION' }, + { _id: 'VIEW_PERMISSION' }, + { _id: 'MODERATE_PERMISSION' } + ] + }]) + + await Roles.addUsersToRolesAsync(users.eve, ['admin']) + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'DELETE_PERMISSION')) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'DELETE_PERMISSION', 'scope')) + + const usersRoles3 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles3.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'user' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'user' }, + { _id: 'ALL_PERMISSIONS' }, + { _id: 'EDIT_PERMISSION' }, + { _id: 'VIEW_PERMISSION' }, + { _id: 'MODERATE_PERMISSION' } + ] + }, { + role: { _id: 'ALL_PERMISSIONS' }, + scope: 'scope', + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'ALL_PERMISSIONS' }, + { _id: 'EDIT_PERMISSION' }, + { _id: 'VIEW_PERMISSION' }, + { _id: 'MODERATE_PERMISSION' } + ] + }, { + role: { _id: 'admin' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'admin' }, + { _id: 'DELETE_PERMISSION' } + ] + }]) + + await Roles.addRolesToParentAsync('DELETE_PERMISSION', 'ALL_PERMISSIONS') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'DELETE_PERMISSION')) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'DELETE_PERMISSION', 'scope')) + + const usersRoles4 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles4.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'user' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'user' }, + { _id: 'ALL_PERMISSIONS' }, + { _id: 'EDIT_PERMISSION' }, + { _id: 'VIEW_PERMISSION' }, + { _id: 'MODERATE_PERMISSION' }, + { _id: 'DELETE_PERMISSION' } + ] + }, { + role: { _id: 'ALL_PERMISSIONS' }, + scope: 'scope', + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'ALL_PERMISSIONS' }, + { _id: 'EDIT_PERMISSION' }, + { _id: 'VIEW_PERMISSION' }, + { _id: 'MODERATE_PERMISSION' }, + { _id: 'DELETE_PERMISSION' } + ] + }, { + role: { _id: 'admin' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'admin' }, + { _id: 'DELETE_PERMISSION' } + ] + }]) + + await Roles.removeUsersFromRolesAsync(users.eve, ['admin']) + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'DELETE_PERMISSION')) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'DELETE_PERMISSION', 'scope')) + + const usersRoles5 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles5.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'user' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'user' }, + { _id: 'ALL_PERMISSIONS' }, + { _id: 'EDIT_PERMISSION' }, + { _id: 'VIEW_PERMISSION' }, + { _id: 'MODERATE_PERMISSION' }, + { _id: 'DELETE_PERMISSION' } + ] + }, { + role: { _id: 'ALL_PERMISSIONS' }, + scope: 'scope', + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'ALL_PERMISSIONS' }, + { _id: 'EDIT_PERMISSION' }, + { _id: 'VIEW_PERMISSION' }, + { _id: 'MODERATE_PERMISSION' }, + { _id: 'DELETE_PERMISSION' } + ] + }]) + + await await Roles.deleteRoleAsync('ALL_PERMISSIONS') + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'DELETE_PERMISSION')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'DELETE_PERMISSION', 'scope')) + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'MODERATE_PERMISSION')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'MODERATE_PERMISSION', 'scope')) + + const usersRoles6 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles6.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'user' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'user' } + ] + }]) + }) + + it('delete role with overlapping hierarchical roles', async function () { + await Roles.createRoleAsync('role1') + await Roles.createRoleAsync('role2') + await Roles.createRoleAsync('COMMON_PERMISSION_1') + await Roles.createRoleAsync('COMMON_PERMISSION_2') + await Roles.createRoleAsync('COMMON_PERMISSION_3') + await Roles.createRoleAsync('EXTRA_PERMISSION_ROLE_1') + await Roles.createRoleAsync('EXTRA_PERMISSION_ROLE_2') + + await Roles.addRolesToParentAsync('COMMON_PERMISSION_1', 'role1') + await Roles.addRolesToParentAsync('COMMON_PERMISSION_2', 'role1') + await Roles.addRolesToParentAsync('COMMON_PERMISSION_3', 'role1') + await Roles.addRolesToParentAsync('EXTRA_PERMISSION_ROLE_1', 'role1') + + await Roles.addRolesToParentAsync('COMMON_PERMISSION_1', 'role2') + await Roles.addRolesToParentAsync('COMMON_PERMISSION_2', 'role2') + await Roles.addRolesToParentAsync('COMMON_PERMISSION_3', 'role2') + await Roles.addRolesToParentAsync('EXTRA_PERMISSION_ROLE_2', 'role2') + + await Roles.addUsersToRolesAsync(users.eve, 'role1') + await Roles.addUsersToRolesAsync(users.eve, 'role2') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'COMMON_PERMISSION_1')) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'EXTRA_PERMISSION_ROLE_1')) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'EXTRA_PERMISSION_ROLE_2')) + + const usersRoles = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'role1' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'role1' }, + { _id: 'COMMON_PERMISSION_1' }, + { _id: 'COMMON_PERMISSION_2' }, + { _id: 'COMMON_PERMISSION_3' }, + { _id: 'EXTRA_PERMISSION_ROLE_1' } + ] + }, { + role: { _id: 'role2' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'role2' }, + { _id: 'COMMON_PERMISSION_1' }, + { _id: 'COMMON_PERMISSION_2' }, + { _id: 'COMMON_PERMISSION_3' }, + { _id: 'EXTRA_PERMISSION_ROLE_2' } + ] + }]) + + await Roles.removeUsersFromRolesAsync(users.eve, 'role2') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'COMMON_PERMISSION_1')) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'EXTRA_PERMISSION_ROLE_1')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'EXTRA_PERMISSION_ROLE_2')) + + const usersRoles2 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles2.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'role1' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'role1' }, + { _id: 'COMMON_PERMISSION_1' }, + { _id: 'COMMON_PERMISSION_2' }, + { _id: 'COMMON_PERMISSION_3' }, + { _id: 'EXTRA_PERMISSION_ROLE_1' } + ] + }]) + + await Roles.addUsersToRolesAsync(users.eve, 'role2') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'COMMON_PERMISSION_1')) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'EXTRA_PERMISSION_ROLE_1')) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'EXTRA_PERMISSION_ROLE_2')) + + const usersRoles3 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles3.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'role1' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'role1' }, + { _id: 'COMMON_PERMISSION_1' }, + { _id: 'COMMON_PERMISSION_2' }, + { _id: 'COMMON_PERMISSION_3' }, + { _id: 'EXTRA_PERMISSION_ROLE_1' } + ] + }, { + role: { _id: 'role2' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'role2' }, + { _id: 'COMMON_PERMISSION_1' }, + { _id: 'COMMON_PERMISSION_2' }, + { _id: 'COMMON_PERMISSION_3' }, + { _id: 'EXTRA_PERMISSION_ROLE_2' } + ] + }]) + + await Roles.deleteRoleAsync('role2') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'COMMON_PERMISSION_1')) + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'EXTRA_PERMISSION_ROLE_1')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'EXTRA_PERMISSION_ROLE_2')) + + const usersRoles4 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles4.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'role1' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [ + { _id: 'role1' }, + { _id: 'COMMON_PERMISSION_1' }, + { _id: 'COMMON_PERMISSION_2' }, + { _id: 'COMMON_PERMISSION_3' }, + { _id: 'EXTRA_PERMISSION_ROLE_1' } + ] + }]) + }) + + it('set parent on assigned role', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('EDIT_PERMISSION') + + await Roles.addUsersToRolesAsync(users.eve, 'EDIT_PERMISSION') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'EDIT_PERMISSION')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'admin')) + + const usersRoles = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'EDIT_PERMISSION' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [{ _id: 'EDIT_PERMISSION' }] + }]) + + await Roles.addRolesToParentAsync('EDIT_PERMISSION', 'admin') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'EDIT_PERMISSION')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'admin')) + + const usersRoles2 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles2.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'EDIT_PERMISSION' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [{ _id: 'EDIT_PERMISSION' }] + }]) + }) + + it('remove parent on assigned role', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('EDIT_PERMISSION') + + await Roles.addRolesToParentAsync('EDIT_PERMISSION', 'admin') + + await Roles.addUsersToRolesAsync(users.eve, 'EDIT_PERMISSION') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'EDIT_PERMISSION')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'admin')) + + const usersRoles = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'EDIT_PERMISSION' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [{ _id: 'EDIT_PERMISSION' }] + }]) + + await Roles.removeRolesFromParentAsync('EDIT_PERMISSION', 'admin') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'EDIT_PERMISSION')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'admin')) + + const usersRoles2 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles2.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'EDIT_PERMISSION' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [{ _id: 'EDIT_PERMISSION' }] + }]) + }) + + it('adding and removing extra role parents', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('EDIT_PERMISSION') + + await Roles.addRolesToParentAsync('EDIT_PERMISSION', 'admin') + + await Roles.addUsersToRolesAsync(users.eve, 'EDIT_PERMISSION') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'EDIT_PERMISSION')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'admin')) + + const usersRoles = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'EDIT_PERMISSION' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [{ _id: 'EDIT_PERMISSION' }] + }]) + + await Roles.addRolesToParentAsync('EDIT_PERMISSION', 'user') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'EDIT_PERMISSION')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'admin')) + + const usersRoles2 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles2.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'EDIT_PERMISSION' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [{ _id: 'EDIT_PERMISSION' }] + }]) + + await Roles.removeRolesFromParentAsync('EDIT_PERMISSION', 'user') + + assert.isTrue(await Roles.userIsInRoleAsync(users.eve, 'EDIT_PERMISSION')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'admin')) + + const usersRoles3 = await Roles.getRolesForUserAsync(users.eve, { anyScope: true, fullObjects: true }) + assert.sameDeepMembers(usersRoles3.map(obj => { delete obj._id; return obj }), [{ + role: { _id: 'EDIT_PERMISSION' }, + scope: null, + user: { _id: users.eve }, + inheritedRoles: [{ _id: 'EDIT_PERMISSION' }] + }]) + }) + + it('cyclic roles', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('editor') + await Roles.createRoleAsync('user') + + await Roles.addRolesToParentAsync('editor', 'admin') + await Roles.addRolesToParentAsync('user', 'editor') + + await assert.isRejected(Roles.addRolesToParentAsync('admin', 'user'), /form a cycle/) + }) + + describe('userIsInRole', function () { + it('userIsInRole returns false for unknown roles', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('user') + await Roles.createRoleAsync('editor') + await Roles.addUsersToRolesAsync(users.eve, ['admin', 'user']) + await Roles.addUsersToRolesAsync(users.eve, ['editor']) + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'unknown')) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, [])) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, null)) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, undefined)) + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, 'unknown', { anyScope: true })) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, [], { anyScope: true })) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, null, { anyScope: true })) + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, undefined, { anyScope: true })) + + assert.isFalse(await Roles.userIsInRoleAsync(users.eve, ['Role1', 'Role2', undefined], 'GroupName')) + }) + + it('userIsInRole returns false if user is a function', async function () { + await Roles.createRoleAsync('admin') + await Roles.addUsersToRolesAsync(users.eve, ['admin']) + + assert.isFalse(await Roles.userIsInRoleAsync(() => {}, 'admin')) + }) + }) + + describe('isParentOf', function () { + it('returns false for unknown roles', async function () { + await Roles.createRoleAsync('admin') + + assert.isFalse(await Roles.isParentOfAsync('admin', 'unknown')) + assert.isFalse(await Roles.isParentOfAsync('admin', null)) + assert.isFalse(await Roles.isParentOfAsync('admin', undefined)) + + assert.isFalse(await Roles.isParentOfAsync('unknown', 'admin')) + assert.isFalse(await Roles.isParentOfAsync(null, 'admin')) + assert.isFalse(await Roles.isParentOfAsync(undefined, 'admin')) + }) + + it('returns false if role is not parent of', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('editor') + await Roles.createRoleAsync('user') + await Roles.addRolesToParentAsync(['editor'], 'admin') + await Roles.addRolesToParentAsync(['user'], 'editor') + + assert.isFalse(await Roles.isParentOfAsync('user', 'admin')) + assert.isFalse(await Roles.isParentOfAsync('editor', 'admin')) + }) + + it('returns true if role is parent of the demanded role', async function () { + await Roles.createRoleAsync('admin') + await Roles.createRoleAsync('editor') + await Roles.createRoleAsync('user') + await Roles.addRolesToParentAsync(['editor'], 'admin') + await Roles.addRolesToParentAsync(['user'], 'editor') + + assert.isTrue(await Roles.isParentOfAsync('admin', 'user')) + assert.isTrue(await Roles.isParentOfAsync('editor', 'user')) + assert.isTrue(await Roles.isParentOfAsync('admin', 'editor')) + + assert.isTrue(await Roles.isParentOfAsync('admin', 'admin')) + assert.isTrue(await Roles.isParentOfAsync('editor', 'editor')) + assert.isTrue(await Roles.isParentOfAsync('user', 'user')) + }) + }) +}) diff --git a/testapp/.meteor/versions b/testapp/.meteor/versions index fa9f2a80..e0abc760 100644 --- a/testapp/.meteor/versions +++ b/testapp/.meteor/versions @@ -25,14 +25,10 @@ hot-code-push@1.0.4 hot-module-replacement@0.5.3 html-tools@1.1.3 htmljs@1.1.1 -http@1.0.10 id-map@1.1.1 inter-process-messaging@0.1.1 logging@1.3.2 meteor@1.11.3 -meteortesting:browser-tests@1.4.2 -meteortesting:mocha@2.1.0 -meteortesting:mocha-core@8.0.1 minifier-css@1.6.4 minifier-js@2.7.5 modern-browsers@0.1.9 @@ -57,6 +53,5 @@ templating-tools@1.2.2 tracker@1.3.2 typescript@4.9.4 underscore@1.0.13 -url@1.3.2 webapp@1.13.5 webapp-hashing@1.1.1 diff --git a/testapp/package-lock.json b/testapp/package-lock.json index 839665ed..ca5c8cc2 100644 --- a/testapp/package-lock.json +++ b/testapp/package-lock.json @@ -983,6 +983,15 @@ "type-detect": "^4.0.8" } }, + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "requires": { + "check-error": "^1.0.2" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", diff --git a/testapp/package.json b/testapp/package.json index 0d2cc3c2..b0227ca5 100644 --- a/testapp/package.json +++ b/testapp/package.json @@ -18,6 +18,7 @@ "devDependencies": { "babel-plugin-istanbul": "^6.1.1", "chai": "^4.3.10", + "chai-as-promised": "^7.1.1", "nyc": "^15.1.0", "puppeteer": "^19.11.1", "standard": "^17.1.0"