Vulcan/packages/vulcan-users/lib/permissions.js

297 lines
7.9 KiB
JavaScript

import Users from './collection.js';
import { Utils } from 'meteor/vulcan:lib';
/**
* @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 a specific action
* @param {Object} user
* @param {String} action
*/
Users.canDo = (user, action) => {
return Users.isAdmin(user) || Users.getActions(user).indexOf(action) !== -1;
};
// 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.canViewField = function (user, field, document) {
if (field.viewableBy) {
return typeof field.viewableBy === 'function' ? field.viewableBy(user, document) : Users.isMemberOf(user, field.viewableBy)
}
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.canViewField(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.canInsertField = function (user, field) {
if (field.insertableBy) {
return typeof field.insertableBy === 'function' ? field.insertableBy(user) : Users.isMemberOf(user, field.insertableBy)
}
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.canEditField = function (user, field, document) {
if (field.editableBy) {
return typeof field.editableBy === 'function' ? field.editableBy(user, document) : Users.isMemberOf(user, field.editableBy)
}
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.new",
"users.edit.own",
"users.remove.own"
];
Users.groups.members.can(membersActions);
Users.createGroup("admins"); // admin users
const adminActions = [
"users.new",
"users.edit.all",
"users.remove.all",
"settings.edit"
];
Users.groups.admins.can(adminActions);