owner -> member; set allow/deny for posts, comments, users

This commit is contained in:
Sacha Greif 2015-04-28 17:15:53 +09:00
parent 40d38d1364
commit fc8af1c9da
15 changed files with 190 additions and 312 deletions

View file

@ -1,12 +1,6 @@
Template.comment_edit.helpers({
commentFields: function () {
var schema = Comments.simpleSchema()._schema;
var comment = this.comment;
var fields = _.filter(_.keys(schema), function (fieldName) {
var field = schema[fieldName];
return Users.can.editField(Meteor.user(), field, comment);
});
return fields;
return Comments.simpleSchema().getEditableFields(Meteor.user());
}
});

View file

@ -1,11 +1,6 @@
Template.comment_submit.helpers({
commentFields: function () {
var schema = Comments.simpleSchema()._schema;
var fields = _.filter(_.keys(schema), function (fieldName) {
var field = schema[fieldName];
return Users.can.submitField(Meteor.user(), field);
});
return fields;
return Comments.simpleSchema().getEditableFields(Meteor.user());
},
reason: function () {
return !!Meteor.user() ? i18n.t('sorry_you_do_not_have_the_rights_to_comments'): i18n.t('please_log_in_to_comment');

View file

@ -27,7 +27,7 @@ Telescope.schemas.comments = new SimpleSchema({
},
body: {
type: String,
editableBy: ["owner", "admin"],
editableBy: ["member", "admin"],
autoform: {
rows: 5
}
@ -73,7 +73,7 @@ Telescope.schemas.comments = new SimpleSchema({
postId: {
type: String,
optional: true,
editableBy: ["owner", "admin"], // TODO: should users be able to set postId, but not modify it?
editableBy: ["member", "admin"], // TODO: should users be able to set postId, but not modify it?
autoform: {
omit: true // never show this
}
@ -91,23 +91,7 @@ Telescope.schemas.comments = new SimpleSchema({
Telescope.schemas.comments.internationalize();
Comments.attachSchema(Telescope.schemas.comments);
/**
* Attach schema to Posts collection
*/
// Note: is the allow/deny code still needed?
Comments.deny({
update: function(userId, post, fieldNames) {
if(Users.is.adminById(userId))
return false;
// deny the update if it contains something other than the following fields
return (_.without(fieldNames, 'body').length > 0);
}
});
Comments.allow({
update: Users.can.editById,
remove: Users.can.editById
});
update: _.partial(Telescope.allowCheck, Comments),
remove: _.partial(Telescope.allowCheck, Comments)
});

View file

@ -3,7 +3,7 @@ var thumbnailProperty = {
propertySchema: {
type: String,
optional: true,
editableBy: ["owner", "admin"],
editableBy: ["member", "admin"],
autoform: {
type: 'bootstrap-postthumbnail'
}

View file

@ -26,8 +26,47 @@ Meteor.Collection.prototype.removeField = function (fieldName) {
collection.attachSchema(schema, {replace: true});
}
/**
* Check if an operation is allowed
* @param {Object} collection the collection to which the document belongs
* @param {String} userId the userId of the user performing the operation
* @param {Object} document the document being modified
* @param {[String]} fieldNames the names of the fields being modified
* @param {Object} modifier the modifier
*/
Telescope.allowCheck = function (collection, userId, document, fieldNames, modifier) {
var schema = collection.simpleSchema();
var user = Meteor.users.findOne(userId);
var allowedFields = schema.getEditableFields(user);
// allow update only if:
// 1. user has rights to edit the document
// 2. there is no fields in fieldNames that are not also in allowedFields
return Users.can.edit(userId, document) && _.difference(fieldNames, allowedFields).length == 0;
}
// Note: using the prototype doesn't work in allow/deny for some reason
Meteor.Collection.prototype.allowCheck = function (userId, document, fieldNames, modifier) {
Telescope.allowCheck(this, userId, document, fieldNames, modifier);
}
/**
* Global schemas object. Note: not reactive, won't be updated after initialization
* @namespace Telescope.schemas
*/
Telescope.schemas = {};
Telescope.schemas = {};
/**
* Get a list of all fields editable by a specific user for a given schema
* @param {Object} user the user for which to check field permissions
*/
SimpleSchema.prototype.getEditableFields = function (user) {
var schema = this._schema;
var fields = _.filter(_.keys(schema), function (fieldName) {
var field = schema[fieldName];
return Users.can.editField(user, field);
});
return fields;
}

View file

@ -1,12 +1,6 @@
Template.post_edit.helpers({
postFields: function () {
var schema = Posts.simpleSchema()._schema;
var post = this.post;
var fields = _.filter(_.keys(schema), function (fieldName) {
var field = schema[fieldName];
return Users.can.editField(Meteor.user(), field, post);
});
return fields;
return Posts.simpleSchema().getEditableFields(Meteor.user());
}
});

View file

@ -1,11 +1,6 @@
Template.post_submit.helpers({
postFields: function () {
var schema = Posts.simpleSchema()._schema;
var fields = _.filter(_.keys(schema), function (fieldName) {
var field = schema[fieldName];
return Users.can.submitField(Meteor.user(), field);
});
return fields;
return Posts.simpleSchema().getEditableFields(Meteor.user());
}
});

View file

@ -29,7 +29,7 @@ Telescope.schemas.posts = new SimpleSchema({
url: {
type: String,
optional: true,
editableBy: ["owner", "admin"],
editableBy: ["member", "admin"],
autoform: {
type: "bootstrap-url"
}
@ -37,12 +37,12 @@ Telescope.schemas.posts = new SimpleSchema({
title: {
type: String,
optional: false,
editableBy: ["owner", "admin"]
editableBy: ["member", "admin"]
},
body: {
type: String,
optional: true,
editableBy: ["owner", "admin"],
editableBy: ["member", "admin"],
autoform: {
rows: 5
}
@ -160,7 +160,10 @@ Telescope.schemas.posts.internationalize();
*/
Posts.attachSchema(Telescope.schemas.posts);
Posts.allow({
update: _.partial(Telescope.allowCheck, Posts),
remove: _.partial(Telescope.allowCheck, Posts)
});
//////////////////////////////////////////////////////
// Collection Hooks //

View file

@ -4,7 +4,7 @@ Posts.registerField(
propertySchema: {
type: [String],
optional: true,
editableBy: ["owner", "admin"],
editableBy: ["member", "admin"],
autoform: {
noselect: true,
options: function () {

View file

@ -1,104 +1,11 @@
<template name="userAccount">
<div class="grid-small grid-module dialog user-edit">
{{#if profileIncomplete}}
<div>{{_ "please_complete_your_profile_below_before_continuing"}}</div>
{{/if}}
{{> quickForm collection="Meteor.users" doc=user id="editUserForm" template="bootstrap3-horizontal" input-col-class="controls" type="update" fields=userFields}}
<!-- {{#if profileIncomplete}}
<div>
{{_ "please_complete_your_profile_below_before_continuing"}}
</div>
{{/if}}
<form id="account-form">
<h2>{{_ "account"}}</h2>
<div class="control-group">
<label>{{_ "username"}}</label>
<div class="controls">
<input id="username" name="username" disabled="disabled" type="text" value="{{userName}}" />
</div>
<p class="note">Profile URL: {{profileUrl}}</p>
</div>
<div class="control-group">
<label>{{_ "display_name"}}</label>
<div class="controls">
<input name="name" type="text" value="{{profile.username}}" />
</div>
</div>
<div class="control-group">
<label>{{_ "email"}}</label>
<div class="controls">
<input name="email" type="text" value="{{userEmail}}" />
</div>
</div>
<div class="control-group">
<label>{{_ "bio"}}</label>
<div class="controls"><textarea name="bio" type="text">{{profile.bio}}</textarea></div>
</div>
<div class="control-group">
<label>{{_ "city"}}</label>
<div class="controls">
<input name="city" type="text" value="{{profile.city}}" />
</div>
</div>
<div class="control-group">
<label>{{_ "twitter_username"}}</label>
<div class="controls">
<input name="twitter" type="text" value="{{getTwitter}}" />
</div>
</div>
<div class="control-group">
<label>{{_ "github_username"}}</label>
<div class="controls">
<input name="github" type="text" value="{{getGitHub}}" />
</div>
</div>
<div class="control-group">
<label>{{_ "site_url"}}</label>
<div class="controls">
<input name="site" type="text" value="{{profile.site}}" />
</div>
</div>
{{#if hasPassword}}
<h3>{{_ "change_password"}}</h3>
<div class="control-group">
<label>{{_ "old_password"}}</label>
<div class="controls"><input name="old_password" type="password" value="" /></div>
</div>
<div class="control-group">
<label>{{_ "new_password"}}</label>
<div class="controls"><input name="new_password" type="password" value="" /></div>
</div>
{{/if}}
<div class="control-group">
<label class="control-label">{{_ "email_notifications"}}</label>
<div class="controls">
{{#if isAdmin}}
<label class="checkbox">
<input id="notifications_users" type="checkbox" name="notifications_users" {{hasNotificationsUsers}} /> {{_ "new_users"}}
</label>
{{/if}}
<label class="checkbox">
<input id="notifications_posts" type="checkbox" name="notifications_posts" {{hasNotificationsPosts}} /> {{_ "new_posts"}}
</label>
<label class="checkbox">
<input id="notifications_comments" type="checkbox" name="notifications_comments" {{hasNotificationsComments}} /> {{_ "comments_on_my_posts"}}
</label>
<label class="checkbox">
<input id="notifications_replies" type="checkbox" name="notifications_replies" {{hasNotificationsReplies}} /> {{_ "replies_to_my_comments"}}
</label>
</div>
</div>
{{#if isAdmin}}
<div class="control-group">
<h3>Invites</h3>
<label>Invites</label>
<div class="controls">
<input name="inviteCount" type="text" value="{{inviteCount}}" />
</div>
</div>
{{/if}}
<div class="form-actions">
<a href="/forgot-password">{{_ "forgot_password"}}</a>
<input type="submit" class="button btn btn-primary" value="{{_ "submit"}}" />
</div>
</form> -->
</div>
</template>

View file

@ -15,104 +15,74 @@ Template.userAccount.helpers({
// filter out uneditable fields and only keep "fieldName"
var fields = _.pluck(_.filter(userDataSchema, function(field){
return Users.can.editField(user, user, field);
return Users.can.editField(user, field, user);
}), "fieldName");
return fields;
},
profileIncomplete : function() {
return this && !this.loading && !Users.userProfileComplete(this);
},
userName: function(){
return Users.getUserName(this);
},
userEmail : function(){
return Users.getEmail(this);
},
getTwitter: function(){
return Users.getTwitterName(this) || "";
},
getGitHub: function(){
return Users.getGitHubName(this) || "";
},
profileUrl: function(){
return Users.getProfileUrlBySlugOrId(this.slug);
},
hasNotificationsUsers : function(){
return Users.getUserSetting('notifications.users', '', this) ? 'checked' : '';
},
hasNotificationsPosts : function(){
return Users.getUserSetting('notifications.posts', '', this) ? 'checked' : '';
},
hasNotificationsComments : function(){
return Users.getUserSetting('notifications.comments', '', this) ? 'checked' : '';
},
hasNotificationsReplies : function(){
return Users.getUserSetting('notifications.replies', '', this) ? 'checked' : '';
},
hasPassword: function () {
return Users.hasPassword(Meteor.user());
}
});
Template.userAccount.events({
'submit #account-form': function(e){
e.preventDefault();
// Template.userAccount.events({
// 'submit #account-form': function(e){
// e.preventDefault();
Messages.clearSeen();
if(!Meteor.user())
Messages.flash(i18n.t('you_must_be_logged_in'), 'error');
// Messages.clearSeen();
// if(!Meteor.user())
// Messages.flash(i18n.t('you_must_be_logged_in'), 'error');
var $target=$(e.target);
var name = $target.find('[name=name]').val();
var email = $target.find('[name=email]').val();
var user = this;
var update = {
"profile.username": name,
"profile.slug": Telescope.utils.slugify(name),
"profile.bio": $target.find('[name=bio]').val(),
"profile.city": $target.find('[name=city]').val(),
"profile.email": email,
"profile.twitter": $target.find('[name=twitter]').val(),
"profile.github": $target.find('[name=github]').val(),
"profile.site": $target.find('[name=site]').val(),
"profile.notifications.users": $('input[name=notifications_users]:checked').length, // only actually used for admins
"profile.notifications.posts": $('input[name=notifications_posts]:checked').length,
"profile.notifications.comments": $('input[name=notifications_comments]:checked').length,
"profile.notifications.replies": $('input[name=notifications_replies]:checked').length
};
// var $target=$(e.target);
// var name = $target.find('[name=name]').val();
// var email = $target.find('[name=email]').val();
// var user = this;
// var update = {
// "profile.username": name,
// "profile.slug": Telescope.utils.slugify(name),
// "profile.bio": $target.find('[name=bio]').val(),
// "profile.city": $target.find('[name=city]').val(),
// "profile.email": email,
// "profile.twitter": $target.find('[name=twitter]').val(),
// "profile.github": $target.find('[name=github]').val(),
// "profile.site": $target.find('[name=site]').val(),
// "profile.notifications.users": $('input[name=notifications_users]:checked').length, // only actually used for admins
// "profile.notifications.posts": $('input[name=notifications_posts]:checked').length,
// "profile.notifications.comments": $('input[name=notifications_comments]:checked').length,
// "profile.notifications.replies": $('input[name=notifications_replies]:checked').length
// };
var old_password = $target.find('[name=old_password]').val();
var new_password = $target.find('[name=new_password]').val();
// var old_password = $target.find('[name=old_password]').val();
// var new_password = $target.find('[name=new_password]').val();
if(old_password && new_password){
Accounts.changePassword(old_password, new_password, function(error){
// TODO: interrupt update if there's an error at this point
if(error)
Messages.flash(error.reason, "error");
});
}
// if(old_password && new_password){
// Accounts.changePassword(old_password, new_password, function(error){
// // TODO: interrupt update if there's an error at this point
// if(error)
// Messages.flash(error.reason, "error");
// });
// }
update = Users.hooks.userEditClientCallbacks.reduce(function(result, currentFunction) {
return currentFunction(user, result);
}, update);
// update = Users.hooks.userEditClientCallbacks.reduce(function(result, currentFunction) {
// return currentFunction(user, result);
// }, update);
Meteor.users.update(user._id, {
$set: update
}, function(error){
if(error){
Messages.flash(error.reason, "error");
} else {
Messages.flash(i18n.t('profile_updated'), 'success');
}
Deps.afterFlush(function() {
var element = $('.grid > .error');
$('html, body').animate({scrollTop: element.offset().top});
});
});
// Meteor.users.update(user._id, {
// $set: update
// }, function(error){
// if(error){
// Messages.flash(error.reason, "error");
// } else {
// Messages.flash(i18n.t('profile_updated'), 'success');
// }
// Deps.afterFlush(function() {
// var element = $('.grid > .error');
// $('html, body').animate({scrollTop: element.offset().top});
// });
// });
Meteor.call('changeEmail', user._id, email);
// Meteor.call('changeEmail', user._id, email);
}
// }
});
// });

View file

@ -2,4 +2,18 @@
* Telescope Users namespace
* @namespace Users
*/
Users = Meteor.users;
Users = Meteor.users;
Users.getUser = function (userOrUserId) {
if (typeof userOrUserId === "undefined") {
if (!Meteor.user()) {
throw new Error();
} else {
return Meteor.user();
}
} else if (typeof userOrUserId === "string") {
return Meteor.users.findOne(userOrUserId);
} else {
return userOrUserId;
}
}

View file

@ -21,6 +21,14 @@ Users.can.view = function (user) {
return true;
};
Users.can.viewById = function (userId) {
// if an invite is required to view, run permission check, else return true
if (Settings.get('requireViewInvite', false)) {
return !!userId ? Users.can.view(Meteor.users.findOne(userId)) : false;
}
return true;
};
Users.can.viewPendingPosts = function (user) {
user = (typeof user === 'undefined') ? Meteor.user() : user;
return Users.is.admin(user);
@ -31,13 +39,6 @@ Users.can.viewRejectedPosts = function (user) {
return Users.is.admin(user);
};
Users.can.viewById = function (userId) {
// if an invite is required to view, run permission check, else return true
if (Settings.get('requireViewInvite', false)) {
return !!userId ? Users.can.view(Meteor.users.findOne(userId)) : false;
}
return true;
};
Users.can.post = function (user, returnError) {
user = (typeof user === 'undefined') ? Meteor.user() : user;
@ -65,16 +66,27 @@ Users.can.vote = function (user, returnError) {
return Users.can.post(user, returnError);
};
Users.can.edit = function (user, item, returnError) {
/**
* Check if a user can edit a document
* @param {Object} user - The user performing the action
* @param {Object} document - The document being edited
*/
Users.can.edit = function (user, document) {
user = (typeof user === 'undefined') ? Meteor.user() : user;
if (!user || !item || (user._id !== item.userId &&
user._id !== item._id &&
!Users.is.admin(user))) {
return returnError ? "no_rights" : false;
} else {
return true;
if (!user || !document) {
return false;
}
var adminCheck = Users.is.admin(user);
var ownerCheck = Users.is.owner(user, document);
return adminCheck || ownerCheck;
};
Users.can.editById = function (userId, document) {
var user = Meteor.users.findOne(userId);
return Users.can.edit(user, document);
};
/**
@ -88,37 +100,19 @@ Users.can.submitField = function (user, field) {
return false;
}
var adminCheck = _.contains(field.editableBy, "admin") && Users.is.admin(user);
var ownerCheck = _.contains(field.editableBy, "owner");
var adminCheck = _.contains(field.editableBy, "admin") && Users.is.admin(user); // is the field editable by admins?
var memberCheck = _.contains(field.editableBy, "member"); // is the field editable by regular users?
return adminCheck || ownerCheck;
return adminCheck || memberCheck;
}
/**
* Check if a user can edit a field on a specific document
* Check if a user can edit a field for now, identical to Users.can.submitField
* @param {Object} user - The user performing the action
* @param {Object} field - The field being edited or inserted
* @param {Object} document - The document being modified
*/
Users.can.editField = function (user, field, document) {
if (!field.editableBy || !user) {
return false;
}
var adminCheck = _.contains(field.editableBy, "admin") && Users.is.admin(user);
var ownerCheck = _.contains(field.editableBy, "owner") && Users.is.owner(user, document);
return adminCheck || ownerCheck;
}
Users.can.editById = function (userId, item) {
var user = Meteor.users.findOne(userId);
return Users.can.edit(user, item);
};
Users.can.editField = Users.can.submitField
Users.can.currentUserEdit = function (item) {
return Users.can.edit(Meteor.user(), item);

View file

@ -4,23 +4,13 @@
*/
Users.is = {};
getUser = function (userOrUserId) {
if (typeof userOrUserId === "undefined") {
if (!Meteor.user()) {
throw new Error();
} else {
return Meteor.user();
}
} else if (typeof userOrUserId === "string") {
return Meteor.users.findOne(userOrUserId);
} else {
return userOrUserId;
}
}
/**
* Check if a user is an admin
* @param {Object|string} userOrUserId - The user or their userId
*/
Users.is.admin = function (userOrUserId) {
try {
var user = getUser(userOrUserId);
var user = Users.getUser(userOrUserId);
return !!user && !!user.isAdmin;
} catch (e) {
return false; // user not logged in
@ -28,19 +18,31 @@ Users.is.admin = function (userOrUserId) {
};
Users.is.adminById = Users.is.admin;
/**
* 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.is.owner = function (userOrUserId, document) {
try {
var user = getUser(userOrUserId);
return user._id === document.userId;
var user = Users.getUser(userOrUserId);
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 to check
return user._id === document._id;
}
} catch (e) {
return false; // user not logged in
}
};
Users.is.ownerById = Users.is.owner;
Users.is.invited = function (userOrUserId) {
try {
var user = getUser(userOrUserId);
var user = Users.getUser(userOrUserId);
return Users.is.admin(user) || Users.is.invited(user);
} catch (e) {
return false; // user not logged in

View file

@ -24,7 +24,7 @@ Telescope.schemas.userData = new SimpleSchema({
bio: {
type: String,
optional: true,
editableBy: ["owner", "admin"]
editableBy: ["member", "admin"]
},
commentCount: {
type: Number,
@ -34,7 +34,7 @@ Telescope.schemas.userData = new SimpleSchema({
type: String,
regEx: /^[a-zA-Z-]{2,25}$/,
optional: true,
editableBy: ["owner", "admin"]
editableBy: ["member", "admin"]
},
downvotedComments: {
type: [Telescope.schemas.votes],
@ -48,7 +48,7 @@ Telescope.schemas.userData = new SimpleSchema({
type: String,
regEx: /^[a-zA-Z]{2,25}$/,
optional: true,
editableBy: ["owner", "admin"]
editableBy: ["member", "admin"]
},
emailHash: {
type: String,
@ -85,7 +85,7 @@ Telescope.schemas.userData = new SimpleSchema({
twitterUsername: {
type: String,
optional: true,
editableBy: ["owner", "admin"]
editableBy: ["member", "admin"]
},
upvotedComments: {
type: [Telescope.schemas.votes],
@ -99,7 +99,7 @@ Telescope.schemas.userData = new SimpleSchema({
type: String,
regEx: SimpleSchema.RegEx.Url,
optional: true,
editableBy: ["owner", "admin"]
editableBy: ["member", "admin"]
}
});
@ -156,24 +156,11 @@ Telescope.schemas.users.internationalize();
*/
Meteor.users.attachSchema(Telescope.schemas.users);
/**
* Users collection permissions
*/
Users.deny({
update: function(userId, post, fieldNames) {
if(Users.is.adminById(userId))
return false;
// deny the update if it contains something other than the profile field
return (_.without(fieldNames, 'profile', 'username', 'slug').length > 0);
}
});
Users.allow({
update: function(userId, doc){
return Users.is.adminById(userId) || userId == doc._id;
},
remove: function(userId, doc){
return Users.is.adminById(userId) || userId == doc._id;
}
});
update: _.partial(Telescope.allowCheck, Meteor.users),
remove: _.partial(Telescope.allowCheck, Meteor.users)
});