import Users from './collection.js'; import { Utils } from 'meteor/vulcan:lib'; import intersection from 'lodash/intersection'; /** * @summary Users.groups object */ Users.groups = {}; /** * @summary Group class */ class Group { constructor() { this.actions = []; } can(actions) { actions = Array.isArray(actions) ? actions : [actions]; this.actions = this.actions.concat(actions); } cannot(actions) { actions = Array.isArray(actions) ? actions : [actions]; this.actions = _.difference(this.actions, actions); } } //////////////////// // Helpers // //////////////////// /** * @summary create a new group * @param {String} groupName */ Users.createGroup = groupName => { Users.groups[groupName] = new Group(); }; /** * @summary get a list of a user's groups * @param {Object} user */ Users.getGroups = user => { let userGroups = []; if (!user) { // guests user userGroups = ["guests"]; } else { userGroups = ["members"]; if (user.groups) { // custom groups userGroups = userGroups.concat(user.groups); } if (Users.isAdmin(user)) { // admin userGroups.push("admins"); } } return userGroups; }; /** * @summary get a list of all the actions a user can perform * @param {Object} user */ Users.getActions = user => { let userGroups = Users.getGroups(user); if (!userGroups.includes('guests')) { // always give everybody permission for guests actions, too userGroups.push('guests'); } let groupActions = userGroups.map(groupName => { // note: make sure groupName corresponds to an actual group const group = Users.groups[groupName]; return group && group.actions; }); return _.unique(_.flatten(groupActions)); }; /** * @summary check if a user is a member of a group * @param {Array} user * @param {String} group or array of groups */ Users.isMemberOf = (user, groupOrGroups) => { const groups = Array.isArray(groupOrGroups) ? groupOrGroups : [groupOrGroups]; // everybody is considered part of the guests group if (groups.indexOf('guests') !== -1) return true; // every logged in user is part of the members group if (groups.indexOf('members') !== -1) return !!user; // the admin group have their own function if (groups.indexOf('admin') !== -1) return Users.isAdmin(user); // else test for the `groups` field return intersection(Users.getGroups(user), groups).length > 0; }; /** * @summary check if a user can perform at least one of the specified actions * @param {Object} user * @param {String/Array} action or actions */ Users.canDo = (user, actionOrActions) => { const authorizedActions = Users.getActions(user); const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]; return Users.isAdmin(user) || intersection(authorizedActions, actions).length > 0; }; // DEPRECATED // TODO: remove this /** * @summary Check if a user can edit a document * @param {Object} user - The user performing the action * @param {Object} document - The document being edited */ // Users.canEdit = function (user, document) { // user = (typeof user === 'undefined') ? Meteor.user() : user; // // note(apollo): use of `__typename` given by react-apollo // //const collectionName = document.getCollectionName(); // const collectionName = document.__typename ? Utils.getCollectionNameFromTypename(document.__typename) : document.getCollectionName(); // if (!user || !document) { // return false; // } // if (document.hasOwnProperty('isDeleted') && document.isDeleted) return false; // if (Users.owns(user, document)) { // // if this is user's document, check if user can edit own documents // return Users.canDo(user, `${collectionName}.edit.own`); // } else { // // if this is not user's document, check if they can edit all documents // return Users.canDo(user, `${collectionName}.edit.all`); // } // }; /** * @summary Check if a user owns a document * @param {Object|string} userOrUserId - The user or their userId * @param {Object} document - The document to check (post, comment, user object, etc.) */ Users.owns = function (user, document) { try { if (!!document.userId) { // case 1: document is a post or a comment, use userId to check return user._id === document.userId; } else { // case 2: document is a user, use _id or slug to check return document.slug ? user.slug === document.slug : user._id === document._id; } } catch (e) { return false; // user not logged in } }; /** * @summary Check if a user is an admin * @param {Object|string} userOrUserId - The user or their userId */ Users.isAdmin = function (userOrUserId) { try { var user = Users.getUser(userOrUserId); return !!user && !!user.isAdmin; } catch (e) { return false; // user not logged in } }; Users.isAdminById = Users.isAdmin; /** * @summary Check if a user can view a field * @param {Object} user - The user performing the action * @param {Object} field - The field being edited or inserted */ Users.canReadField = function ( user, field, document) { const canRead = field.canRead || field.viewableBy; //OpenCRUD backwards compatibility if (canRead) { if (typeof canRead === 'function') { // if canRead is a function, execute it with user and document passed. it must return a boolean return canRead(user, document); } else if (typeof canRead === 'string') { // if canRead is just a string, we assume it's the name of a group and pass it to isMemberOf return Users.isMemberOf(user, canRead); } else if (Array.isArray(canRead) && canRead.length > 0) { // if canRead is an array, we do a recursion on every item and return true if one of the items return true // this also makes it possible to use nested arrays, such as ['admins', ['group1', function1, [function2, 'group2'], function3]] return canRead.reduce((accumulator, currentValue)=> accumulator || Users.canReadField(user, currentValue, document)); } } return false; }; /** * @summary Get a list of fields viewable by a user * @param {Object} user - The user performing the action * @param {Object} collection - The collection * @param {Object} document - Optionally, get a list for a specific document */ Users.getViewableFields = function (user, collection, document) { return Utils.arrayToFields(_.compact(_.map(collection.simpleSchema()._schema, (field, fieldName) => { return Users.canReadField(user, field, document) ? fieldName : null; } ))); } // collection helper Users.helpers({ getViewableFields(collection, document) { return Users.getViewableFields(this, collection, document); } }); /** * @summary For a given document or list of documents, keep only fields viewable by current user * @param {Object} user - The user performing the action * @param {Object} collection - The collection * @param {Object} document - The document being returned by the resolver */ Users.restrictViewableFields = function (user, collection, docOrDocs) { if (!docOrDocs) return {}; const restrictDoc = document => { // get array of all keys viewable by user const viewableKeys = _.keys(Users.getViewableFields(user, collection, document)); const restrictedDocument = _.clone(document); // loop over each property in the document and delete it if it's not viewable _.forEach(restrictedDocument, (value, key) => { if (!viewableKeys.includes(key)) { delete restrictedDocument[key]; } }); return restrictedDocument; }; return Array.isArray(docOrDocs) ? docOrDocs.map(restrictDoc) : restrictDoc(docOrDocs); } /** * @summary Check if a user can submit a field * @param {Object} user - The user performing the action * @param {Object} field - The field being edited or inserted */ Users.canCreateField = function (user, field) { const canCreate = field.canCreate || field.insertableBy; //OpenCRUD backwards compatibility if (canCreate) { if (typeof canCreate === 'function') { // if canCreate is a function, execute it with user and document passed. it must return a boolean return canCreate(user, document); } else if (typeof canCreate === 'string') { // if canCreate is just a string, we assume it's the name of a group and pass it to isMemberOf return Users.isMemberOf(user, canCreate); } else if (Array.isArray(canCreate) && canCreate.length > 0) { // if canCreate is an array, we do a recursion on every item and return true if one of the items return true // this also makes it possible to use nested arrays, such as ['admins', ['group1', function1, [function2, 'group2'], function3]] return canCreate.reduce((accumulator, currentValue)=> accumulator || Users.canCreateField(user, currentValue, document)); } } return false; }; /** @function * Check if a user can edit a field * @param {Object} user - The user performing the action * @param {Object} field - The field being edited or inserted */ Users.canUpdateField = function (user, field, document) { const canUpdate = field.canUpdate || field.editableBy; //OpenCRUD backwards compatibility if (canUpdate) { if (typeof canUpdate === 'function') { // if canUpdate is a function, execute it with user and document passed. it must return a boolean return canUpdate(user, document); } else if (typeof canUpdate === 'string') { // if canUpdate is just a string, we assume it's the name of a group and pass it to isMemberOf return Users.isMemberOf(user, canUpdate); } else if (Array.isArray(canUpdate) && canUpdate.length > 0) { // if canUpdate is an array, we do a recursion on every item and return true if one of the items return true // this also makes it possible to use nested arrays, such as ['admins', ['group1', function1, [function2, 'group2'], function3]] return canUpdate.reduce((accumulator, currentValue)=> accumulator || Users.canUpdateField(user, currentValue, document)); } } return false; }; //////////////////// // Initialize // //////////////////// /** * @summary initialize the 3 out-of-the-box groups */ Users.createGroup("guests"); // non-logged-in users Users.createGroup("members"); // regular users const membersActions = [ "users.create", "users.update.own", // OpenCRUD backwards compatibility "users.new", "users.edit.own", "users.remove.own", ]; Users.groups.members.can(membersActions); Users.createGroup("admins"); // admin users const adminActions = [ "users.create", "users.update.all", "users.delete.all", "settings.update", // OpenCRUD backwards compatibility "users.new", "users.edit.all", "users.remove.all", "settings.edit", ]; Users.groups.admins.can(adminActions);