Vulcan/packages/vulcan-users/lib/modules/permissions.js
Apollinaire ac0a875c3a remove backwards comp. from function name change and add new names to...
all instances of functions Users.can... in the whole code
2018-06-21 14:27:20 +02:00

342 lines
11 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);