Okay, so I am a bit confused about something with Meteor.js. I created a site with it to test the various concepts, and it worked fine. Once I removed "insecure" and "autopublish", I get multiple "access denied" errors when trying to retrieve and push to the server. I belive it has something to do with the following snippet:
Template.posts.posts = function () {
return Posts.find({}, {sort: {time: -1}});
}
I think that it is trying to access the collection directly, which it was allowed to do with "insecure" and "autopublish" enabled, but once they were disabled it was given access denied. Another piece I think is problematic:
else {
Posts.insert({
user: Meteor.user().profile.name,
post: post.value,
time: Date.now(),
});
I think that the same sort of thing is happening: it is trying to access the collection directly, which it is not allowed to do.
My question is, how do I re-factor it so that I do not need "insecure" and "autopublish" enabled?
Thanks.
EDIT
Final:
/**
* Models
*/
Posts = new Meteor.Collection('posts');
posts = Posts
if (Meteor.isClient) {
Meteor.subscribe('posts');
}
if (Meteor.isServer) {
Meteor.publish('posts', function() {
return posts.find({}, {time:-1, limit: 100});
});
posts.allow({
insert: function (document) {
return true;
},
update: function () {
return false;
},
remove: function () {
return false;
}
});
}
Ok, so there are two parts to this question:
Autopublish
To publish databases in meteor, you need to have code on both the server-side, and client-side of the project. Assuming you have instantiated the collection (Posts = new Meteor.Collection('posts')), then you need
if (Meteor.isServer) {
Meteor.publish('posts', function(subsargs) {
//subsargs are args passed in the next section
return posts.find()
//or
return posts.find({}, {time:-1, limit: 5}) //etc
})
}
Then for the client
if (Meteor.isClient) {
Meteor.subscribe('posts', subsargs) //here is where you can pass arguments
}
Insecure
The purpose of insecure is to allow the client to indiscriminately add, modify, and remove any database entries it wants. However, most of the time you don't want that. Once you remove insecure, you need to set up rules on the server detailing who can do what. These two functions are db.allow and db.deny. E.g.
if (Meteor.isServer) {
posts.allow({
insert:function(userId, document) {
if (userId === "ABCDEFGHIJKLMNOP") { //e.g check if admin
return true;
}
return false;
},
update: function(userId,doc,fieldNames,modifier) {
if (fieldNames.length === 1 && fieldNames[0] === "post") { //they are only updating the post
return true;
}
return false;
},
remove: function(userId, doc) {
if (doc.user === userId) { //if the creator is trying to remove it
return true;
}
return false;
}
});
}
Likewise, db.deny will behave the exact same way, except a response of true will mean "do not allow this action"
Hope this answers all your questions
Related
I'm enjoying working with Meteor and trying out new things, but I often try to keep security in mind. So while I'm building out a prototype app, I'm trying to find the best practices for keeping the app secure. One thing I keep coming across is restricting a user based on either a roll, or whether or not they're logged in. Here are two examples of issues I'm having.
// First example, trying to only fire an event if the user is an admin
// This is using the alaning:roles package
Template.homeIndex.events({
"click .someclass": function(event) {
if (Roles.userIsInRole(Meteor.user(), 'admin', 'admin-group') {
// Do something only if an admin in admin-group
}
});
My problem with the above is I can override this by typing:
Roles.userIsInRole = function() { return true; } in this console. Ouch.
The second example is using Iron Router. Here I want to allow a user to the "/chat" route only if they're logged in.
Router.route("/chat", {
name: 'chatHome',
onBeforeAction: function() {
// Not secure! Meteor.user = function() { return true; } in the console.
if (!Meteor.user()) {
return this.redirect('homeIndex');
} else {
this.next();
}
},
waitOn: function () {
if (!!Meteor.user()) {
return Meteor.subscribe("messages");
}
},
data: function () {
return {
chatActive: true
}
}
});
Again I run into the same problem. Meteor.user = function() { return true; } in this console blows this pattern up. The only way around this I have found thus far is using a Meteor.method call, which seems improper, as they are stubs that require callbacks.
What is the proper way to address this issue?
Edit:
Using a Meteor.call callback doesn't work for me since it's calling for a response asynchronously. It's moving out of the hook before it can handle the response.
onBeforeAction: function() {
var self = this;
Meteor.call('someBooleanFunc', function(err, res) {
if (!res) {
return self.redirect('homeIndex');
} else {
self.next();
}
})
},
I guess you should try adding a check in the publish method in server.
Something like this:
Meteor.publish('messages') {
if (Roles.userIsInRole(this.userId, 'admin', 'admin-group')) {
return Meteor.messages.find();
}
else {
// user not authorized. do not publish messages
this.stop();
return;
}
});
You may do a similar check in your call methods in server.
I am trying to test the upload functionality using this guide with the only exception of using cfs-s3 package. This is very basic with simple code but I am getting an error on the client console - Error: Access denied. No allow validators set on restricted collection for method 'insert'. [403]
I get this error even though I have set the allow insert in every possible way.
Here is my client code:
// client/images.js
var imageStore = new FS.Store.S3("images");
Images = new FS.Collection("images", {
stores: [imageStore],
filter: {
allow: {
contentTypes: ['image/*']
}
}
});
Images.deny({
insert: function(){
return false;
},
update: function(){
return false;
},
remove: function(){
return false;
},
download: function(){
return false;
}
});
Images.allow({
insert: function(){
return true;
},
update: function(){
return true;
},
remove: function(){
return true;
},
download: function(){
return true;
}
});
And there is a simple file input button on the homepage -
// client/home.js
'change .myFileInput': function(e, t) {
FS.Utility.eachFile(e, function(file) {
Images.insert(file, function (err, fileObj) {
if (err){
console.log(err) // --- THIS is the error
} else {
// handle success depending what you need to do
console.log("fileObj id: " + fileObj._id)
//Meteor.users.update(userId, {$set: imagesURL});
}
});
});
}
I have set the proper policies and everything on S3 but I don't think this error is related to S3 at all.
// server/images.js
var imageStore = new FS.Store.S3("images", {
accessKeyId: "xxxx",
secretAccessKey: "xxxx",
bucket: "www.mybucket.com"
});
Images = new FS.Collection("images", {
stores: [imageStore],
filter: {
allow: {
contentTypes: ['image/*']
}
}
});
I have also published and subscribed to the collections appropriately. I have been digging around for hours but can't seem to figure out what is happening.
EDIT: I just readded insecure package and everything now works. So basically, the problem is with allow/deny rules but I am actually doing it. I am not sure why it is not acknowledging the rules.
You need to define the FS.Collection's allow/deny rules in sever-only code. These are server-side rules applied to the underlying Mongo.Collection that FS.Collection creates.
The best approach is to export the AWS keys as the following environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, remove the accessKeyId and secretAccessKey options from the FS.Store, and then move the FS.Collection constructor calls to run on both the client and server. The convenience of using env vars is mentioned on the cfs:s3 page
In addition to this you can control the bucket name using Meteor.settings.public, which is handy when you want to use different buckets based on the environment.
I want to have a filter on routing level, checking if the user is in a specific role.
this.route('gamePage', {
path: '/game/:slug/',
onBeforeAction: teamFilter,
waitOn: function() { return […]; },
data: function() { return Games.findOne({slug: this.params.slug}); }
});
Here is my filter:
var teamFilter = function(pause) {
if (Meteor.user()) {
Meteor.call('checkPermission', this.params.slug, Meteor.userId(), function(error, result) {
if (error) {
throwError(error.reason, error.details);
return null;
}
console.log(result); // returns always false
if (!result) {
this.render('noAccess');
pause();
}
});
}
}
In my collection:
checkPermission: function(gameSlug, userId) {
if (serverVar) { // only executed on the server
var game = Games.findOne({slug: gameSlug});
if (game) {
if (!Roles.userIsInRole(userId, game._id, ['administrator', 'team'])) {
return false;
} else {
return true;
}
}
}
}
My first problem is that Roles.userIsInRole(userId, game._id, ['administrator', 'team'] always returns false. At first, I had this code in my router.js, but then I thought that it does not work because of a missing publication/subscription, so I ensured that the code runs only on the server. I checked the database and the user is in the role.
My second problem is that I get an exception (Exception in delivering result of invoking 'checkPermission': http://localhost:3000/lib/router.js?77b3b67967715e480a1ce463f3447ec61898e7d5:14:28) at this point: this.render('noAccess'); and I don't know why.
I already read this: meteor Roles.userIsInRole() always returning false but it didn't solve my problem.
Any help would be greatly appreciated.
In teamFilter hook you call Meteor.method checkPermission which works asynchronously and OnBeforeAction expects synchronous execution ( no callbacks ). That is why you always receive false.
Another thing is that you are using Roles.userIsInRole incorrectly:
Should be:
Roles.userIsInRole(this.userId, ['view-secrets','admin'], group)
In this case I would check roles on client side:
Roles.userIsInRole(userId, ['administrator', 'team'])
Probably you are worried about security with this solution.
I don't think you should.
What is the most important is data and data is protected by publish function which should check the roles.
Please note that all templates are accessible to client.
You can add roles to the user only on server for that you can user Meteor.call({}); check here method from client to call method on server's main.js and you can check after this method call if the role is added in users collection using meteor mongo and db.users.find({}).pretty() and see if the roles array is added the user of that usedId then you can use Roles.userIsInRole() function anywhere on client to check loggedin users role.
I'm getting an object not found error when I try and lookup the owner of the objects
i'm trying to render. I'm looping through a collection of video clips, that can be updated or administered by users. The code works fine when I'm logged in, but when I try to use this and I'm logged out, I get "Exception in queued task: TypeError: Cannot read property '_id' of undefined at Object.Template.video_info.creatorName "
I've tried to debug this by doing this:
console.log(this.owner);
var owner = Meteor.users.findOne(this.owner);
console.log(owner);
When I check the console log, I can see that the correct userid is being found, and when i manually run Meteor.users.findOne with this id I get a user object returned. Is there something strange about the timings in Meteor that is preventing this?
UPDATE: If I add a try...catch to the template creatorname function then 2 errors get logged but the template still renders... ??? Seems like this template is being called twice, one when it's not ready, and again once it is. Why would that be.
Example of the try...catch block:
Template.video_info.creatorName = function () {
try{
var owner = Meteor.users.findOne(this.owner);
if (owner._id === Meteor.userId())
return "me";
return displayName(owner);
} catch (e){
console.log(e);
}
};
ORIGINAL BROKEN CODE BELOW THIS POINT
This is in my HTML:
<body>
<div>
{{> video_list}}
</div>
</body>
<template name="video_list">
<h1>Video List</h1>
{{#each videos}}
<ul>
{{> video_info}}
</ul>
{{else}}
No videos yet.
{{/each}}
<div class="footer">
<button>Like!</button>
</div>
</template>
<template name="video_info">
<li class="video-list {{maybe_selected}}">
<img src="{{image}}" />
<div>
<h3>{{title}}</h3>
<p>{{description}}</p>
<h4>{{creatorName}}</h4>
</div>
</li>
</template>
This is in my client.js
Meteor.subscribe("videos");
if (Meteor.isClient) {
Template.video_list.videos = function() {
return Videos.find({}, {sort: {title: 1}});
};
Template.video_list.events = {
'click button': function(){
Videos.update(Session.get('session_video'),{$inc: {likes: 1}});
}
}
Template.video_info.maybe_selected = function() {
return Session.equals('session_video', this._id) ? "selected" : "";
}
Template.video_info.events = {
'click': function(){
Session.set('session_video', this._id);
}
}
Template.video_info.creatorName = function () {
var owner = Meteor.users.findOne(this.owner);
if (owner._id === Meteor.userId())
return "me";
return displayName(owner);
};
}
if (Meteor.isServer) {
Meteor.startup(function () {
// code to run on server at startup
});
}
This is in my model.js
Videos = new Meteor.Collection("videos");
Videos.allow({
insert: function (userId, video) {
return false; // no cowboy inserts -- use createParty method
},
update: function (userId, video, fields, modifier) {
if (userId !== video.owner)
return false; // not the owner
var allowed = ["title", "description", "videoid", "image", "start"];
if (_.difference(fields, allowed).length)
return false; // tried to write to forbidden field
// A good improvement would be to validate the type of the new
// value of the field (and if a string, the length.) In the
// future Meteor will have a schema system to makes that easier.
return true;
},
remove: function (userId, video) {
// You can only remove parties that you created and nobody is going to.
return video.owner === userId; //&& attending(video) === 0;
}
});
var NonEmptyString = Match.Where(function (x) {
check(x, String);
return x.length !== 0;
});
var NonEmptyNumber = Match.Where(function (x) {
check(x, Number);
return x.length !== 0;
});
createVideo = function (options) {
var id = Random.id();
Meteor.call('createVideo', _.extend({ _id: id }, options));
return id;
};
Meteor.methods({
// options should include: title, description, x, y, public
createVideo: function (options) {
check(options, {
title: NonEmptyString,
description: NonEmptyString,
videoid: NonEmptyString,
image:NonEmptyString,
start: NonEmptyNumber,
_id: Match.Optional(NonEmptyString)
});
if (options.title.length > 100)
throw new Meteor.Error(413, "Title too long");
if (options.description.length > 1000)
throw new Meteor.Error(413, "Description too long");
if (! this.userId)
throw new Meteor.Error(403, "You must be logged in");
var id = options._id || Random.id();
Videos.insert({
_id: id,
owner: this.userId,
videoid: options.videoid,
image: options.image,
start: options.start,
title: options.title,
description: options.description,
public: !! options.public,
invited: [],
rsvps: []
});
return id;
},
});
///////////////////////////////////////////////////////////////////////////////
// Users
displayName = function (user) {
if (user.profile && user.profile.name)
return user.profile.name;
return user.emails[0].address;
};
var contactEmail = function (user) {
if (user.emails && user.emails.length)
return user.emails[0].address;
if (user.services && user.services.facebook && user.services.facebook.email)
return user.services.facebook.email;
return null;
};
I think I've found the solution to this one. After reading about the caching works in Meteor, I've discovered the subscription model and how this relates to meteors minimongo http://docs.meteor.com/#dataandsecurity. The reason this was failing then succeeding was that on the first load the data is still being cached in minimongo. I'm currently checking against Accounts login Services Configured to check if the user data has been loaded. I'm currently using this because I can't find a way to subscribe to the Metor users service, but my guess is that the Accounts login service would rely on the Metor users collection. My current solution looks like this:
if(Accounts.loginServicesConfigured()){
var owner = Meteor.users.findOne(this.owner);
if (owner._id === Meteor.userId())
return "me";
return displayName(owner);
}
Currently this appears to be working correctly. I'm still delving into how to subscribe to this users service.Couple of really userful resferences I found while searching for a solution for this
https://github.com/oortcloud/unofficial-meteor-faq
http://psychopyko.com/cool-stuff/meteor-6-simple-tips/
https://groups.google.com/forum/#!topic/meteor-talk/QKXe7qfBfqg
The app might not be publishing the user id to the client when you are logged out. You can try calling the find method on the server and return the user. Or use a different key for querying/
I'm having trouble adding custom user fields to a Meteor user object (Meteor.user). I'd like a user to have a "status" field, and I'd rather not nest it under "profile" (ie, profile.status), which I do know is r/w by default. (I've already removed autopublish.)
I've been able to publish the field to the client just fine via
Meteor.publish("directory", function () {
return Meteor.users.find({}, {fields: {username: 1, status: 1}});
});
...but I can't get set permissions that allow a logged-in user to update their own status.
If I do
Meteor.users.allow({
update: function (userId) {
return true;
}});
in Models.js, a user can edit all the fields for every user. That's not cool.
I've tried doing variants such as
Meteor.users.allow({
update: function (userId) {
return userId === Meteor.userId();
}});
and
Meteor.users.allow({
update: function (userId) {
return userId === this.userId();
}});
and they just get me Access Denied errors in the console.
The documentation addresses this somewhat, but doesn't go into enough detail. What silly mistake am I making?
(This is similar to this SO question, but that question only addresses how to publish fields, not how to update them.)
This is how I got it to work.
In the server I publish the userData
Meteor.publish("userData", function () {
return Meteor.users.find(
{_id: this.userId},
{fields: {'foo': 1, 'bar': 1}}
);
});
and set the allow as follows
Meteor.users.allow({
update: function (userId, user, fields, modifier) {
// can only change your own documents
if(user._id === userId)
{
Meteor.users.update({_id: userId}, modifier);
return true;
}
else return false;
}
});
in the client code, somewhere I update the user record, only if there is a user
if(Meteor.userId())
{
Meteor.users.update({_id: Meteor.userId()},{$set:{foo: 'something', bar: 'other'}});
}
Try:
Meteor.users.allow({
update: function (userId, user) {
return userId === user._id;
}
});
From the documentation for collection.allow:
update(userId, doc, fieldNames, modifier)
The user userId wants to update a document doc. (doc is the current version of the document from the database, without the proposed update.) Return true to permit the change.