diff --git a/packages/roles/definitions.d.ts b/packages/roles/definitions.d.ts index d8f4441022..c15c8c9652 100644 --- a/packages/roles/definitions.d.ts +++ b/packages/roles/definitions.d.ts @@ -4,14 +4,14 @@ // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped // Minimum TypeScript Version: 4.1 -import { Mongo } from 'meteor/mongo' +import { Mongo } from "meteor/mongo"; /** * Provides functions related to user authorization. Compatible with built-in Meteor accounts packages. * * @module Roles */ -declare namespace Roles { +export declare namespace Roles { /** * Constant used to reference the special 'global' group that * can be used to apply blanket permissions across all groups. @@ -27,7 +27,7 @@ declare namespace Roles { * @static * @final */ - var GLOBAL_GROUP: string + var GLOBAL_GROUP: string; /** * Subscription handle for the currently logged in user's permissions. @@ -42,7 +42,7 @@ declare namespace Roles { * * @for Roles */ - var subscription: Subscription + var subscription: Subscription; /** * Add users to roles. @@ -68,12 +68,12 @@ declare namespace Roles { users: string | string[] | Meteor.User | Meteor.User[], roles: string | string[], options?: string | { scope?: string; ifExists?: boolean } - ): void + ): void; function addUsersToRolesAsync( users: string | string[] | Meteor.User | Meteor.User[], roles: string | string[], options?: string | { scope?: string; ifExists?: boolean } - ): Promise + ): Promise; /** * Create a new role. @@ -84,8 +84,14 @@ declare namespace Roles { * - `unlessExists`: if `true`, exception will not be thrown in the role already exists * @return {String} ID of the new role or null. */ - function createRole(roleName: string, options?: { unlessExists: boolean }): string - function createRoleAsync(roleName: string, options?: { unlessExists: boolean }): Promise + function createRole( + roleName: string, + options?: { unlessExists: boolean } + ): string; + function createRoleAsync( + roleName: string, + options?: { unlessExists: boolean } + ): Promise; /** * Delete an existing role. @@ -95,8 +101,8 @@ declare namespace Roles { * @method deleteRole * @param {String} roleName Name of role. */ - function deleteRole(roleName: string): void - function deleteRoleAsync(roleName: string): Promise + function deleteRole(roleName: string): void; + function deleteRoleAsync(roleName: string): Promise; /** * Rename an existing role. @@ -105,8 +111,8 @@ declare namespace Roles { * @param {String} oldName Old name of a role. * @param {String} newName New name of a role. */ - function renameRole(oldName: string, newName: string): void - function renameRoleAsync(oldName: string, newName: string): Promise + function renameRole(oldName: string, newName: string): void; + function renameRoleAsync(oldName: string, newName: string): Promise; /** * Add role parent to roles. @@ -118,8 +124,14 @@ declare namespace Roles { * @param {Array|String} rolesNames Name(s) of role(s). * @param {String} parentName Name of parent role. */ - function addRolesToParent(rolesNames: string | string[], parentName: string): void - function addRolesToParentAsync(rolesNames: string | string[], parentName: string): Promise + function addRolesToParent( + rolesNames: string | string[], + parentName: string + ): void; + function addRolesToParentAsync( + rolesNames: string | string[], + parentName: string + ): Promise; /** * Remove role parent from roles. @@ -131,8 +143,14 @@ declare namespace Roles { * @param {Array|String} rolesNames Name(s) of role(s). * @param {String} parentName Name of parent role. */ - function removeRolesFromParent(rolesNames: string | string[], parentName: string): void - function removeRolesFromParentAsync(rolesNames: string | string[], parentName: string): Promise + function removeRolesFromParent( + rolesNames: string | string[], + parentName: string + ): void; + function removeRolesFromParentAsync( + rolesNames: string | string[], + parentName: string + ): Promise; /** * Retrieve cursor of all existing roles. @@ -142,7 +160,7 @@ declare namespace Roles { * through to `Meteor.roles.find(query, options)`. * @return {Cursor} Cursor of existing roles. */ - function getAllRoles(queryOptions?: QueryOptions): Mongo.Cursor + function getAllRoles(queryOptions?: QueryOptions): Mongo.Cursor; /** * Retrieve users groups, if any @@ -153,8 +171,14 @@ declare namespace Roles { * * @return {Array} Array of user's groups, unsorted. Roles.GLOBAL_GROUP will be omitted */ - function getGroupsForUser(user: string | Meteor.User, role?: string): string[] - function getGroupsForUserAsync(user: string | Meteor.User, role?: string): Promise + function getGroupsForUser( + user: string | Meteor.User, + role?: string + ): string[]; + function getGroupsForUserAsync( + user: string | Meteor.User, + role?: string + ): Promise; /** * Retrieve users scopes, if any. @@ -165,8 +189,14 @@ declare namespace Roles { * * @return {Array} Array of user's scopes, unsorted. */ - function getScopesForUser(user: string | Meteor.User, roles?: string | string[]): string[] - function getScopesForUserAsync(user: string | Meteor.User, roles?: string | string[]): Promise + function getScopesForUser( + user: string | Meteor.User, + roles?: string | string[] + ): string[]; + function getScopesForUserAsync( + user: string | Meteor.User, + roles?: string | string[] + ): Promise; /** * Rename a scope. @@ -177,8 +207,8 @@ declare namespace Roles { * @param {String} oldName Old name of a scope. * @param {String} newName New name of a scope. */ - function renameScope(oldName: string, newName: string): void - function renameScopeAsync(oldName: string, newName: string): Promise + function renameScope(oldName: string, newName: string): void; + function renameScopeAsync(oldName: string, newName: string): Promise; /** * Remove a scope. @@ -189,8 +219,8 @@ declare namespace Roles { * @param {String} name The name of a scope. * */ - function removeScope(name: String): void - function removeScopeAsync(name: String): Promise + function removeScope(name: String): void; + function removeScopeAsync(name: String): Promise; /** * Find out if a role is an ancestor of another role. @@ -202,8 +232,11 @@ declare namespace Roles { * @param {String} childRoleName The role you expect to be among the children of parentRoleName. * @return {Boolean} */ - function isParentOf(parentRoleName: string, childRoleName: string): boolean - function isParentOfAsync(parentRoleName: string, childRoleName: string): Promise + function isParentOf(parentRoleName: string, childRoleName: string): boolean; + function isParentOfAsync( + parentRoleName: string, + childRoleName: string + ): Promise; /** * Retrieve user's roles. @@ -222,20 +255,30 @@ declare namespace Roles { * Alternatively, it can be a scope name string. * @return {Array} Array of user's roles, unsorted. */ - function getRolesForUser(user: string | Meteor.User, options?: string | { - scope?: string; - anyScope?: boolean; - onlyScoped?: boolean; - onlyAssigned?: boolean; - fullObjects?: boolean - }): string[] - function getRolesForUserAsync(user: string | Meteor.User, options?: string | { - scope?: string; - anyScope?: boolean; - onlyScoped?: boolean; - onlyAssigned?: boolean; - fullObjects?: boolean - }): Promise + function getRolesForUser( + user: string | Meteor.User, + options?: + | string + | { + scope?: string; + anyScope?: boolean; + onlyScoped?: boolean; + onlyAssigned?: boolean; + fullObjects?: boolean; + } + ): string[]; + function getRolesForUserAsync( + user: string | Meteor.User, + options?: + | string + | { + scope?: string; + anyScope?: boolean; + onlyScoped?: boolean; + onlyAssigned?: boolean; + fullObjects?: boolean; + } + ): Promise; /** * Retrieve all assignments of a user which are for the target role. @@ -257,11 +300,16 @@ declare namespace Roles { * Alternatively, it can be a scope name string. * @return {Cursor} Cursor of user assignments for roles. */ - function getUserAssignmentsForRole(roles: string | string[], options?: string | { - scope?: string - anyScope?: boolean - queryOptions?: QueryOptions - }): Mongo.Cursor + function getUserAssignmentsForRole( + roles: string | string[], + options?: + | string + | { + scope?: string; + anyScope?: boolean; + queryOptions?: QueryOptions; + } + ): Mongo.Cursor; /** * Retrieve all users who are in target role. @@ -288,14 +336,28 @@ declare namespace Roles { */ function getUsersInRole( roles: string | string[], - options?: string | { scope?: string; anyScope?: boolean; onlyScoped?: boolean; queryOptions?: QueryOptions }, + options?: + | string + | { + scope?: string; + anyScope?: boolean; + onlyScoped?: boolean; + queryOptions?: QueryOptions; + }, queryOptions?: QueryOptions - ): Mongo.Cursor + ): Mongo.Cursor; function getUsersInRoleAsync( roles: string | string[], - options?: string | { scope?: string; anyScope?: boolean; onlyScoped?: boolean; queryOptions?: QueryOptions }, + options?: + | string + | { + scope?: string; + anyScope?: boolean; + onlyScoped?: boolean; + queryOptions?: QueryOptions; + }, queryOptions?: QueryOptions - ): Promise> + ): Promise>; /** * Remove users from assigned roles. @@ -318,12 +380,12 @@ declare namespace Roles { users: string | string[] | Meteor.User | Meteor.User[], roles?: string | string[], options?: string | { scope?: string; anyScope?: boolean } - ): void + ): void; function removeUsersFromRolesAsync( users: string | string[] | Meteor.User | Meteor.User[], roles?: string | string[], options?: string | { scope?: string; anyScope?: boolean } - ): Promise + ): Promise; /** * Set users' roles. @@ -349,13 +411,17 @@ declare namespace Roles { function setUserRoles( users: string | string[] | Meteor.User | Meteor.User[], roles: string | string[], - options?: string | { scope?: string; anyScope?: boolean; ifExists?: boolean } - ): void + options?: + | string + | { scope?: string; anyScope?: boolean; ifExists?: boolean } + ): void; function setUserRolesAsync( users: string | string[] | Meteor.User | Meteor.User[], roles: string | string[], - options?: string | { scope?: string; anyScope?: boolean; ifExists?: boolean } - ): Promise + options?: + | string + | { scope?: string; anyScope?: boolean; ifExists?: boolean } + ): Promise; /** * Check if user has specified roles. @@ -389,55 +455,55 @@ declare namespace Roles { user: string | string[] | Meteor.User | Meteor.User[], roles: string | string[], options?: string | { scope?: string; anyScope?: boolean } - ): boolean + ): boolean; function userIsInRoleAsync( user: string | string[] | Meteor.User | Meteor.User[], roles: string | string[], options?: string | { scope?: string; anyScope?: boolean } - ): Promise + ): Promise; // The schema for the roles collection interface Role { - _id: string - name: string - children: { _id: string }[] + _id: string; + name: string; + children: { _id: string }[]; } // The schema for the role-assignment collection interface RoleAssignment { - _id: string + _id: string; user: { - _id: string - } + _id: string; + }; role: { - _id: string - } + _id: string; + }; inheritedRoles?: { - _id: string - }[] - scope?: string + _id: string; + }[]; + scope?: string; } interface QueryOptions { - sort?: Mongo.SortSpecifier | undefined - skip?: number | undefined - limit?: number | undefined - fields?: Mongo.FieldSpecifier | undefined - projection?: Mongo.FieldSpecifier | undefined - reactive?: boolean | undefined - transform?: Function | undefined + sort?: Mongo.SortSpecifier | undefined; + skip?: number | undefined; + limit?: number | undefined; + fields?: Mongo.FieldSpecifier | undefined; + projection?: Mongo.FieldSpecifier | undefined; + reactive?: boolean | undefined; + transform?: Function | undefined; } - } // module // Exported collections -declare type RolesCollection = Mongo.Collection -declare type RoleAssignmentsCollection = Mongo.Collection +export declare type RolesCollection = Mongo.Collection; +export declare type RoleAssignmentsCollection = + Mongo.Collection; // Additions to the Meteor object -declare module 'meteor/meteor' { - namespace Meteor { - const roles: Mongo.Collection - const roleAssignment: Mongo.Collection +declare module "meteor/meteor" { + export namespace Meteor { + const roles: Mongo.Collection; + const roleAssignment: Mongo.Collection; } } diff --git a/packages/roles/package-types.json b/packages/roles/package-types.json new file mode 100644 index 0000000000..7838991b97 --- /dev/null +++ b/packages/roles/package-types.json @@ -0,0 +1,3 @@ +{ + "typesEntry": "definitions.d.ts" +} diff --git a/packages/roles/package.js b/packages/roles/package.js index 2ff58f3dee..e3a7080835 100644 --- a/packages/roles/package.js +++ b/packages/roles/package.js @@ -15,8 +15,6 @@ Package.onUse(function (api) { both ); - api.use("zodern:types@1.0.13"); - api.use(["blaze@2.9.0 || 3.0.0"], "client", { weak: true }); api.export(["Roles", "RolesCollection", "RoleAssignmentCollection"]); @@ -25,17 +23,15 @@ Package.onUse(function (api) { api.addFiles("roles_common_async.js", both); api.addFiles("roles_server.js", "server"); api.addFiles(["client/debug.js", "client/uiHelpers.js"], "client"); + + api.addAssets("definitions.d.ts", "server"); + api.addAssets("package-types.json", "server"); }); Package.onTest(function (api) { const both = ["client", "server"]; - api.use([ - "tinytest", - "ecmascript", - "mongo", - "roles" - ], both); + api.use(["tinytest", "ecmascript", "mongo", "roles"], both); api.addFiles("tests/serverAsync.js", "server"); api.addFiles("tests/client.js", "client"); diff --git a/packages/roles/roles_common_async.js b/packages/roles/roles_common_async.js index 11079194d5..a78281db83 100644 --- a/packages/roles/roles_common_async.js +++ b/packages/roles/roles_common_async.js @@ -24,7 +24,6 @@ import { Mongo } from 'meteor/mongo' * - `scope`: scope name * - `inheritedRoles`: A list of all the roles objects inherited by the assigned role. * - * @module Roles */ export const RolesCollection = new Mongo.Collection('roles') @@ -39,7 +38,7 @@ if (!Meteor.roleAssignment) { } /** - * @class Roles + * class Roles */ if (typeof Roles === 'undefined') { Roles = {} // eslint-disable-line no-global-assign @@ -60,6 +59,10 @@ const asyncSome = async (arr, predicate) => { return false } +/** + * @namespace Roles + * @summary The namespace for all Roles types and methods. + */ Object.assign(Roles, { /** * Used as a global group (now scope) name. Not used anymore. @@ -71,14 +74,13 @@ Object.assign(Roles, { GLOBAL_GROUP: null, /** - * Create a new role. - * - * @method createRoleAsync + * @summary Create a new role. + * @memberof Roles + * @locus Anywhere * @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 + * - `unlessExists`: if `true`, exception will not be thrown in the role already exists + * @return {Promise} ID of the new role or null */ createRoleAsync: async function (roleName, options) { Roles._checkRoleName(roleName) @@ -116,14 +118,12 @@ Object.assign(Roles, { }, /** - * Delete an existing role. - * + * @summary Delete an existing role. * If the role is set for any user, it is automatically unset. - * - * @method deleteRoleAsync + * @memberof Roles + * @locus Anywhere * @param {String} roleName Name of role. * @returns {Promise} - * @static */ deleteRoleAsync: async function (roleName) { let roles @@ -182,13 +182,12 @@ Object.assign(Roles, { }, /** - * Rename an existing role. - * - * @method renameRoleAsync + * @summary Rename an existing role. + * @memberof Roles + * @locus Anywhere * @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 @@ -254,16 +253,12 @@ Object.assign(Roles, { }, /** - * 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 addRolesToParentAsync + * @summary Add role parent to roles. + * @memberof Roles + * @locus Anywhere * @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 @@ -339,16 +334,14 @@ Object.assign(Roles, { }, /** - * Remove role parent from roles. - * + * @summary 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 removeRolesFromParentAsync - * @param {Array|String} rolesNames Name(s) of role(s). - * @param {String} parentName Name of parent role. - * @returns {Promise} - * @static + * @memberof Roles + * @locus Anywhere + @param {Array|String} rolesNames Name(s) of role(s). + @param {String} parentName Name of parent role. + @returns {Promise} */ removeRolesFromParentAsync: async function (rolesNames, parentName) { // ensure arrays @@ -431,26 +424,16 @@ Object.assign(Roles, { }, /** - * Add users to roles. - * + * @summary Add users to roles. * Adds roles to existing roles for each user. - * - * @example - * Roles.addUsersToRolesAsync(userId, 'admin') - * Roles.addUsersToRolesAsync(userId, ['view-secrets'], 'example.com') - * Roles.addUsersToRolesAsync([user1, user2], ['user','editor']) - * Roles.addUsersToRolesAsync([user1, user2], ['glorious-admin', 'perform-action'], 'example.org') - * - * @method addUsersToRolesAsync + * @memberof Roles + * @locus Anywhere * @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 @@ -487,17 +470,10 @@ Object.assign(Roles, { }, /** - * Set users' roles. - * + * @summary Set users' roles. * Replaces all existing roles with a new set of roles. - * - * @example - * await Roles.setUserRolesAsync(userId, 'admin') - * await Roles.setUserRolesAsync(userId, ['view-secrets'], 'example.com') - * await Roles.setUserRolesAsync([user1, user2], ['user','editor']) - * await Roles.setUserRolesAsync([user1, user2], ['glorious-admin', 'perform-action'], 'example.org') - * - * @method setUserRolesAsync + * @memberof Roles + * @locus Anywhere * @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: @@ -505,9 +481,6 @@ Object.assign(Roles, { * - `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 @@ -714,23 +687,15 @@ Object.assign(Roles, { }, /** - * Remove users from assigned roles. - * - * @example - * await Roles.removeUsersFromRolesAsync(userId, 'admin') - * await Roles.removeUsersFromRolesAsync([userId, user2], ['editor']) - * await Roles.removeUsersFromRolesAsync(userId, ['user'], 'group1') - * - * @method removeUsersFromRolesAsync + * @summary Remove users from assigned roles. + * @memberof Roles + * @locus Anywhere * @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.") @@ -792,21 +757,9 @@ Object.assign(Roles, { }, /** - * Check if user has specified roles. - * - * @example - * // global roles - * await Roles.userIsInRoleAsync(user, 'admin') - * await Roles.userIsInRoleAsync(user, ['admin','editor']) - * await Roles.userIsInRoleAsync(userId, 'admin') - * await Roles.userIsInRoleAsync(userId, ['admin','editor']) - * - * // scope roles (global roles are still checked) - * await Roles.userIsInRoleAsync(user, 'admin', 'group1') - * await Roles.userIsInRoleAsync(userId, ['admin','editor'], 'group1') - * await Roles.userIsInRoleAsync(userId, ['admin','editor'], {scope: 'group1'}) - * - * @method userIsInRoleAsync + * @summary Check if user has specified roles. + * @memberof Roles + * @locus Anywhere * @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. @@ -818,7 +771,6 @@ Object.assign(Roles, { * * 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 @@ -869,9 +821,9 @@ Object.assign(Roles, { }, /** - * Retrieve user's roles. - * - * @method getRolesForUserAsync + * @summary Retrieve user's roles. + * @memberof Roles + * @locus Anywhere * @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 @@ -884,7 +836,6 @@ Object.assign(Roles, { * * Alternatively, it can be a scope name string. * @return {Promise} Array of user's roles, unsorted. - * @static */ getRolesForUserAsync: async function (user, options) { let id @@ -954,13 +905,133 @@ Object.assign(Roles, { }, /** - * Retrieve cursor of all existing roles. - * - * @method getAllRoles - * @param {Object} [queryOptions] Options which are passed directly - * through to `RolesCollection.find(query, options)`. + * @summary Retrieve users scopes, if any. + * @memberof Roles + * @locus Anywhere + * @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. + */ + 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)] + }, + + /** + * @summary Rename a scope. + * @memberof Roles + * @locus Anywhere + * @param {String} oldName Old name of a scope. + * @param {String} newName New name of a scope. + * @returns {Promise} + */ + 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) + }, + + /** + * @summary Remove a scope and all its role assignments. + * @memberof Roles + * @locus Anywhere + * @param {String} name The name of a scope. + * @returns {Promise} + */ + removeScopeAsync: async function (name) { + Roles._checkScopeName(name) + + await Meteor.roleAssignment.removeAsync({ scope: name }) + }, + + /** + * @summary Find out if a role is an ancestor of another role. + * @memberof Roles + * @locus Anywhere + * @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} True if parent role is ancestor of child role + */ + 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 + }, + + /** + * @summary Retrieve cursor of all existing roles. + * @memberof Roles + * @locus Anywhere + * @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 } } @@ -969,28 +1040,16 @@ Object.assign(Roles, { }, /** - * Retrieve all users who are in target role. - * - * Options: - * - * @method getUsersInRoleAsync - * @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. + * @summary Retrieve all users who are in target role. + * @memberof Roles + * @locus Anywhere + * @param {Array|String} roles Name of role or an array of roles. * @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) + * - `scope`: name of the scope to restrict roles to + * - `anyScope`: if set, role can be in any scope * - `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)` + * - `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 = ( @@ -1004,25 +1063,15 @@ Object.assign(Roles, { }, /** - * 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. + * @summary Retrieve all assignments of a user which are for the target role. + * @memberof Roles + * @locus Anywhere + * @param {Array|String} roles Name of role or an array of roles. * @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 `RoleAssignmentCollection.find(query, options)` - - * Alternatively, it can be a scope name string. + * - `scope`: name of the scope to restrict roles to + * - `anyScope`: if set, role can be in any scope + * - `queryOptions`: options which are passed directly through to `RoleAssignmentCollection.find(query, options)` * @return {Cursor} Cursor of user assignments for roles. - * @static */ getUserAssignmentsForRole: function (roles, options) { options = Roles._normalizeOptions(options) @@ -1114,157 +1163,6 @@ Object.assign(Roles, { return await Roles.getScopesForUser(...args) }, - /** - * Retrieve users scopes, if any. - * - * @method getScopesForUserAsync - * @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 renameScopeAsync - * @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 removeScopeAsync - * @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 isParentOfAsync - * @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. * diff --git a/packages/roles/tests/serverAsync.js b/packages/roles/tests/serverAsync.js index 1d207adeea..1e844550fc 100644 --- a/packages/roles/tests/serverAsync.js +++ b/packages/roles/tests/serverAsync.js @@ -57,23 +57,23 @@ const sameMembers = (test, value, expected) => { ); }; -const sameDeepMembers = (test, value, expected) => { - // Helper to sort object keys recursively - const sortObjectKeys = (obj) => { - if (Array.isArray(obj)) { - return obj.map(sortObjectKeys); - } - if (obj && typeof obj === "object") { - return Object.keys(obj) - .sort() - .reduce((sorted, key) => { - sorted[key] = sortObjectKeys(obj[key]); - return sorted; - }, {}); - } - return obj; - }; +// Helper to sort object keys recursively +const sortObjectKeys = (obj) => { + if (Array.isArray(obj)) { + return obj.map(sortObjectKeys); + } + if (obj && typeof obj === "object") { + return Object.keys(obj) + .sort() + .reduce((sorted, key) => { + sorted[key] = sortObjectKeys(obj[key]); + return sorted; + }, {}); + } + return obj; +}; +const sameDeepMembers = (test, value, expected) => { const sortedValue = sortObjectKeys(value); const sortedExpected = sortObjectKeys(expected); @@ -84,6 +84,16 @@ const sameDeepMembers = (test, value, expected) => { ); }; +const sameDeepUnorderedMembers = (test, value, expected) => { + const sortAndStringify = (arr) => { + return JSON.stringify(arr.map(item => JSON.stringify(sortObjectKeys(item))).sort()); + }; + const sortedValue = sortAndStringify(value); + const sortedExpected = sortAndStringify(expected); + + test.equal(sortedValue, sortedExpected, 'Arrays should have the same elements, regardless of order'); +}; + const hasProp = (target, prop) => Object.hasOwnProperty.call(target, prop); let users = {}; @@ -1885,7 +1895,7 @@ Tinytest.addAsync("roles -keep assigned roles", async function (test) { anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, rolesForUser.map((obj) => { delete obj._id; @@ -1914,7 +1924,7 @@ Tinytest.addAsync("roles -keep assigned roles", async function (test) { anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, rolesForUser2.map((obj) => { delete obj._id; @@ -1949,7 +1959,7 @@ Tinytest.addAsync("roles -keep assigned roles", async function (test) { anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, rolesForUser3.map((obj) => { delete obj._id; @@ -1973,7 +1983,7 @@ Tinytest.addAsync("roles -keep assigned roles", async function (test) { anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, rolesForUser4.map((obj) => { delete obj._id; @@ -2063,7 +2073,7 @@ Tinytest.addAsync( anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, usersRoles.map((obj) => { delete obj._id; @@ -2109,7 +2119,7 @@ Tinytest.addAsync( anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, usersRoles2.map((obj) => { delete obj._id; @@ -2153,7 +2163,7 @@ Tinytest.addAsync( anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, usersRoles3.map((obj) => { delete obj._id; @@ -2203,7 +2213,7 @@ Tinytest.addAsync( anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, usersRoles4.map((obj) => { delete obj._id; @@ -2255,7 +2265,7 @@ Tinytest.addAsync( anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, usersRoles5.map((obj) => { delete obj._id; @@ -2308,7 +2318,7 @@ Tinytest.addAsync( anyScope: true, fullObjects: true, }); - sameDeepMembers( + sameDeepUnorderedMembers( test, usersRoles6.map((obj) => { delete obj._id; diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index 4cd9c168f9..f57a717671 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -322,6 +322,10 @@ export default defineConfig({ text: "hot-module-replacement", link: "/packages/hot-module-replacement", }, + { + text: "roles", + link: "/packages/roles", + }, { text: "less", link: "/packages/less", @@ -374,22 +378,13 @@ export default defineConfig({ link: "/packages/packages-listing", text: "Maintained Packages", }, - { - link: "packages/community-packages", - text: "Community Packages", - }, ], collapsed: true, }, { text: "Community Packages", link: "/community-packages/index", - items: [ - { - text: "Meteor RPC", - link: "/community-packages/meteor-rpc", - }, - ], + items: [], collapsed: true, }, { @@ -400,10 +395,6 @@ export default defineConfig({ link: "/troubleshooting/expired-certificate", }, { text: "Windows", link: "/troubleshooting/windows" }, - { - text: "Known issues in 2.13", - link: "/troubleshooting/known-issues", - }, ], collapsed: true, }, diff --git a/v3-docs/docs/community-packages/index.md b/v3-docs/docs/community-packages/index.md index 3e2bd6d2fe..ab375c1c93 100644 --- a/v3-docs/docs/community-packages/index.md +++ b/v3-docs/docs/community-packages/index.md @@ -1,10 +1,18 @@ # Community Packages +::: tip +The Meteor community is making great efforts to migrate popular packages to Meteor 3.0. + +A [spreadsheet maintained](https://docs.google.com/spreadsheets/u/0/d/1JbUZmJab3owZ9LV71Ubto32YX_QWQljRypJTOQupxL8/htmlview) by [@harryadel](https://github.com/harryadel) tracks the status of your favorite packages and offers opportunities to help. +::: + There are some very popular community packages that do not have a documentation website or only have a readme file. This section tries to list and add some information about usage, configuration, and examples for these packages. > Think this section as an `Awesome List`, similar to [`awesome-node`](https://github.com/sindresorhus/awesome-nodejs) or [`awesome-react`](https://github.com/enaqx/awesome-react) +Many packages have been consolidated into the [Meteor Community organization](https://github.com/Meteor-Community-Packages). Others are maintained by individual developers or companies. + If you use or have a package that you think would be useful to add to this list, please open a pull request. Please bear in mind if you are adding a package to this list, try providing as much information as possible, including: diff --git a/v3-docs/docs/packages/community-packages.md b/v3-docs/docs/packages/community-packages.md deleted file mode 100644 index 626a7e871e..0000000000 --- a/v3-docs/docs/packages/community-packages.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[//]: # (Do not edit this file by hand.) - -[//]: # (This is a generated file.) - -[//]: # (If you want to change something in this file) - -[//]: # (go to meteor/docs/generators/packages-listing) - -# Community Packages - -The Meteor community is making great efforts to migrate popular packages to Meteor 3.0. - -Many packages have been consolidated into the [Meteor Community organization](https://github.com/Meteor-Community-Packages). Others are maintained by individual developers or companies. - -A [spreadsheet maintained](https://docs.google.com/spreadsheets/u/0/d/1JbUZmJab3owZ9LV71Ubto32YX_QWQljRypJTOQupxL8/htmlview) by [@harryadel](https://github.com/harryadel) tracks the status of your favorite packages and offers opportunities to help. diff --git a/v3-docs/docs/packages/roles.md b/v3-docs/docs/packages/roles.md new file mode 100644 index 0000000000..4461c2dbec --- /dev/null +++ b/v3-docs/docs/packages/roles.md @@ -0,0 +1,480 @@ +# Roles + +Authorization package for Meteor - compatible with built-in accounts package. + +> Available since Meteor 3.1.0 (previously alanning:roles) + +## Installation + +To add roles to your application, run this command in your terminal: + +```bash +meteor add roles +``` + +## Overview + +The roles package lets you attach roles to users and then check against those roles when deciding whether to grant access to Meteor methods or publish data. The core concept is simple - you create role assignments for users and then verify those roles later. This package provides helper methods to make the process of adding, removing, and verifying roles easier. + +## Concepts + +### Roles vs Permissions + +Although named "roles", you can define your **roles**, **scopes** or **permissions** however you like. They are essentially tags assigned to users that you can check later. + +You can have traditional roles like `admin` or `webmaster`, or more granular permissions like `view-secrets`, `users.view`, or `users.manage`. Often, more granular permissions are better as they handle edge cases without creating many higher-level roles. + +### Role Hierarchy + +Roles can be organized in a hierarchy: + +- Roles can have multiple parents and children (subroles) +- If a parent role is assigned to a user, all its descendant roles also apply +- This allows creating "super roles" that aggregate permissions + +Example hierarchy setup: + +```js +import { Roles } from "meteor/roles"; + +// Create base roles +await Roles.createRoleAsync("user"); +await Roles.createRoleAsync("admin"); + +// Create permission roles +await Roles.createRoleAsync("USERS_VIEW"); +await Roles.createRoleAsync("POST_EDIT"); + +// Set up hierarchy +await Roles.addRolesToParentAsync("USERS_VIEW", "admin"); +await Roles.addRolesToParentAsync("POST_EDIT", "admin"); +await Roles.addRolesToParentAsync("POST_EDIT", "user"); +``` + +### Scopes + +Scopes allow users to have independent sets of roles. Use cases include: + +- Different communities within your app +- Multiple tenants in a multi-tenant application +- Different resource groups + +Users can have both scoped roles and global roles: + +- Global roles apply across all scopes +- Scoped roles only apply within their specific scope +- Scopes are independent of each other + +Example using scopes: + +```js +// Assign scoped roles +await Roles.addUsersToRolesAsync(userId, ["manage-team"], "team-a"); +await Roles.addUsersToRolesAsync(userId, ["player"], "team-b"); + +// Check scoped roles +await Roles.userIsInRoleAsync(userId, "manage-team", "team-a"); // true +await Roles.userIsInRoleAsync(userId, "manage-team", "team-b"); // false + +// Assign global role +await Roles.addUsersToRolesAsync(userId, "super-admin", null); + +// Global roles work in all scopes +await Roles.userIsInRoleAsync(userId, ["manage-team", "super-admin"], "team-b"); // true +``` + +## Role Management + + + +Example: + +```js +import { Roles } from "meteor/roles"; + +// Create a new role +await Roles.createRoleAsync("admin"); + +// Create if doesn't exist +await Roles.createRoleAsync("editor", { unlessExists: true }); +``` + +### Modifying Roles + + + +Example: + +```js +// Make 'editor' a child role of 'admin' +await Roles.addRolesToParentAsync("editor", "admin"); + +// Add multiple child roles +await Roles.addRolesToParentAsync(["editor", "moderator"], "admin"); +``` + + + +Example: + +```js +// Remove 'editor' as child role of 'admin' +await Roles.removeRolesFromParentAsync("editor", "admin"); +``` + + + +Example: + +```js +// Delete role and all its assignments +await Roles.deleteRoleAsync("temp-role"); +``` + + + +Example: + +```js +// Rename an existing role +await Roles.renameRoleAsync("editor", "content-editor"); +``` + +### Assigning Roles + + + +Example: + +```js +// Add global roles +await Roles.addUsersToRolesAsync(userId, ["admin", "editor"]); + +// Add scoped roles +await Roles.addUsersToRolesAsync(userId, ["manager"], "department-a"); + +// Add roles to multiple users +await Roles.addUsersToRolesAsync([user1Id, user2Id], ["user"]); +``` + + + +Example: + +```js +// Replace user's global roles +await Roles.setUserRolesAsync(userId, ["editor"]); + +// Replace scoped roles +await Roles.setUserRolesAsync(userId, ["viewer"], "project-x"); + +// Clear all roles in scope +await Roles.setUserRolesAsync(userId, [], "project-x"); +``` + + + +Example: + +```js +// Remove global roles +await Roles.removeUsersFromRolesAsync(userId, ["admin"]); + +// Remove scoped roles +await Roles.removeUsersFromRolesAsync(userId, ["manager"], "department-a"); + +// Remove roles from multiple users +await Roles.removeUsersFromRolesAsync([user1Id, user2Id], ["temp-role"]); +``` + + + +Example: + +```js +// Rename a scope +await Roles.renameScopeAsync("department-1", "marketing"); +``` + + + +Example: + +```js +// Remove a scope and all its role assignments +await Roles.removeScopeAsync("old-department"); +``` + + + +Example: + +```js +// Get all roles sorted by name +const roles = Roles.getAllRoles({ sort: { _id: 1 } }); + +// Get roles with custom query +const customRoles = Roles.getAllRoles({ + fields: { _id: 1, children: 1 }, + sort: { _id: -1 }, +}); +``` + + + +Example: + +```js +// Find all admin users +const adminUsers = await Roles.getUsersInRoleAsync("admin"); + +// Find users with specific roles in a scope +const scopedUsers = await Roles.getUsersInRoleAsync( + ["editor", "writer"], + "blog" +); + +// Find users with custom options +const users = await Roles.getUsersInRoleAsync("manager", { + scope: "department-a", + queryOptions: { + sort: { createdAt: -1 }, + limit: 10, + }, +}); +``` + +## Checking Roles + + + +Example: + +```js +// Check global role +const isAdmin = await Roles.userIsInRoleAsync(userId, "admin"); + +// Check any of multiple roles +const canEdit = await Roles.userIsInRoleAsync(userId, ["editor", "admin"]); + +// Check scoped role +const isManager = await Roles.userIsInRoleAsync( + userId, + "manager", + "department-a" +); + +// Check role in any scope +const hasRole = await Roles.userIsInRoleAsync(userId, "viewer", { + anyScope: true, +}); +``` + + + +Example: + +```js +// Get user's global roles +const globalRoles = await Roles.getRolesForUserAsync(userId); + +// Get scoped roles +const deptRoles = await Roles.getRolesForUserAsync(userId, "department-a"); + +// Get all roles including inherited +const allRoles = await Roles.getRolesForUserAsync(userId, { + anyScope: true, + fullObjects: true, +}); +``` + + + +Example: + +```js +// Check if admin is a parent of editor +const isParent = await Roles.isParentOfAsync("admin", "editor"); + +// Can be used to check inheritance chains +const hasPermission = await Roles.isParentOfAsync("super-admin", "post-edit"); +``` + + + +Example: + +```js +// Get all scopes for user +const allScopes = await Roles.getScopesForUserAsync(userId); + +// Get scopes where user has specific roles +const editorScopes = await Roles.getScopesForUserAsync(userId, ["editor"]); +``` + +## Publishing Roles + +Role assignments need to be published to be available on the client. Example publication: + +```js +// Publish user's own roles +Meteor.publish(null, function () { + if (this.userId) { + return Meteor.roleAssignment.find({ "user._id": this.userId }); + } + this.ready(); +}); + +// Publish roles for specific scope +Meteor.publish("scopeRoles", function (scope) { + if (this.userId) { + return Meteor.roleAssignment.find({ scope: scope }); + } + this.ready(); +}); +``` + +## Client only APIs + +On the client alongside the async methods, you can use the `sync` versions of the functions: + +- `Roles.userIsInRole(userId, roles, scope)` +- `Roles.getRolesForUser(userId, scope)` +- `Roles.getScopesForUser(userId)` +- `Roles.isParentOf(parent, child)` +- `Roles.getUsersInRole(role, scope)` +- `Roles.getAllRoles(options)` +- `Roles.createRole(role, options)` +- `Roles.addUsersToRoles(userId, roles, scope)` +- `Roles.setUserRoles(userId, roles, scope)` +- `Roles.removeUsersFromRoles(userId, roles, scope)` +- `Roles.addRolesToParent(child, parent)` +- `Roles.removeRolesFromParent(child, parent)` +- `Roles.deleteRole(role)` +- `Roles.renameRole(oldRole, newRole)` +- `Roles.renameScope(oldScope, newScope)` +- `Roles.removeScope(scope)` + +## Using with Templates + +The roles package automatically provides an `isInRole` helper for templates: + +```handlebars +{{#if isInRole "admin"}} +
+ +
+{{/if}} + +{{#if isInRole "editor,writer" "blog"}} +
+ +
+{{/if}} +``` + +## Migration to Core Version + +If you are currently using the `alanning:roles` package, follow these steps to migrate to the core version: + +1. Make sure you are on version 3.6 of `alanning:roles` first +2. Run any pending migrations from previous versions +3. Switch all server-side role operations to use the async versions of the functions: + - createRoleAsync + - deleteRoleAsync + - addUsersToRolesAsync + - setUserRolesAsync + - removeUsersFromRolesAsync + - etc. +4. Remove `alanning:roles` package: + ```bash + meteor remove alanning:roles + ``` +5. Add the core roles package: + ```bash + meteor add roles + ``` +6. Update imports to use the new package: + ```js + import { Roles } from "meteor/roles"; + ``` + +The sync versions of these functions are still available on the client. + +## Security Considerations + +1. Client-side role checks are for convenience only - always verify permissions on the server +2. Publish only the role data that users need +3. Use scopes to properly isolate role assignments +4. Validate role names and scopes to prevent injection attacks +5. Consider using more granular permissions over broad role assignments + +## Example Usage + +### Method Security + +```js +// server/methods.js +Meteor.methods({ + deletePost: async function (postId) { + check(postId, String); + + const canDelete = await Roles.userIsInRoleAsync( + this.userId, + ["admin", "moderator"], + "posts" + ); + + if (!canDelete) { + throw new Meteor.Error("unauthorized", "Not authorized to delete posts"); + } + + Posts.remove(postId); + }, +}); +``` + +### Publication Security + +```js +// server/publications.js +Meteor.publish("secretDocuments", async function (scope) { + check(scope, String); + + const canView = await Roles.userIsInRoleAsync( + this.userId, + ["view-secrets", "admin"], + scope + ); + + if (canView) { + return SecretDocs.find({ scope: scope }); + } + + this.ready(); +}); +``` + +### User Management + +```js +// server/users.js +Meteor.methods({ + promoteToEditor: async function (userId, scope) { + check(userId, String); + check(scope, String); + + const canPromote = await Roles.userIsInRoleAsync( + this.userId, + "admin", + scope + ); + + if (!canPromote) { + throw new Meteor.Error("unauthorized"); + } + + await Roles.addUsersToRolesAsync(userId, ["editor"], scope); + }, +}); +``` diff --git a/v3-docs/docs/troubleshooting/known-issues.md b/v3-docs/docs/troubleshooting/known-issues.md deleted file mode 100644 index f533d80a76..0000000000 --- a/v3-docs/docs/troubleshooting/known-issues.md +++ /dev/null @@ -1,39 +0,0 @@ - -# Known issues in 2.13 - -Troubleshooting in Meteor 2.13 - -## Cannot extract version of meteor tool {#cannot-extract-meteor-tool} - - -For some users, the `meteor update` to version 2.13 command may fail with the following error or similar: - -```shell -Error: incorrect data check - at Zlib.zlibOnError [as onerror] (zlib.js:187:17) - => awaited here: - ... - at /tools/cli/main.js:1165:7 { - errno: -3, - code: 'Z_DATA_ERROR' - } - -``` - -## The issue {#the-issue} - -It seems related to [our first ESM version of Node.js v14.21.4](https://github.com/meteor/node-v14-esm) and the `zlib` package. -We have been able to reproduce this issue only in Mac Intel. - -You can follow along with the [GitHub issue](https://github.com/meteor/meteor/issues/12731) for updates. - -## Solution {#solution} - -The solution for this issue is running the following command in your terminal: - -```shell - -curl https://install.meteor.com/\?release\=2.13.3 | sh - -``` -