allow and deny parameters in collections - meteor

In the discover meteor book, the deny statement is used as follows:
https://github.com/DiscoverMeteor/Microscope/commit/chapter8-3
Posts.deny({
update: function(userId, post, fieldNames) {
.....
});
I don't understand how the update function is getting UserId, post, or even fieldnames since the edit form is doing the following:
var postProperties = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
}
Posts.update(currentPostId, {$set: postProperties}, function(error) {

Those parameters are given by Meteor. The signature for update functions on the client and in the deny object are different.
http://docs.meteor.com/#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.
fieldNames is an array of the (top-level) fields in doc that the
client wants to modify, for example ['name', 'score'].
modifier is the raw Mongo modifier that the client wants to execute;
for example, {$set: {'name.first': "Alice"}, $inc: {score: 1}}.
Only Mongo modifiers are supported (operations like $set and $push).
If the user tries to replace the entire document rather than use
$-modifiers, the request will be denied without checking the allow
functions.

The short answer is that these values are filled out for you by meteor. It understands who is making what modifications to which document and tells the server about it.
The client is calling Posts.update which sends a message to the server that userId is attempting to update a document (the contents of which are post), and the fields being updated are fieldNames. The server can then choose the accept the update based on those inputs.
This is documented here and here.

Related

Meteor Publish/Subscribe passing object with string parameter issue

I am trying to pass a object { key:value} and send it to meteor publish so i can query to database.
My Mongo db database has (relevant datas only) for products:
products : {
categs:['Ladies Top','Gents'],
name : Apple
}
In meteor Publish i have the following:
Meteor.publish('product', (query) =>{
return Clothings.find(query);
})
In client i use the following to subscribe:
let query = {categs:'/ladies top/i'}; // please notice the case is lower
let subscribe = Meteor.subscribe('product',query);
if (subscribe.ready()){
clothings = Products.find(query).fetch().reverse();
let count = Products.find(query).fetch().reverse().length; // just for test
}
The issue is, when i send the query from client to server, it is automatically encoded eg:
{categs:'/ladies%top/i'}
This query doesnot seem to work at all. There are like total of more than 20,000 products and fetching all is not an option. So i am trying to fetch based on the category (roughly around 100 products each).
I am new to ,meteor and mongo db and was trying to follow existing code, however this doesnot seem to be correct. Is there a better way to improve the code and achieve the same ?
Any suggestion or idea is highly appreciated.
I did go through meteor docs but they dont seem to have examples for my scenario so i hope someone out there can help me :) Cheers !
Firstly, you are trying to send a regex as a parameter. That's why it's being encoded. Meteor doesn't know how to pass functions or regexes as parameters afaict.
For this specific publication, I recommend sending over the string you want to search for and building the regex on the server:
client:
let categorySearch = 'ladies top';
let obj = { categorySearch }; // and any other things you want to query on.
Meteor.subscribe('productCategory',obj);
server:
Meteor.publish('productCategory',function(obj){
check(obj,Object);
let query = {};
if (obj.categorySearch) query.category = { $regex: `/${obj.categorySearch}/i` };
// add any other search parameters to the query object here
return Products.find(query);
});
Secondly, sending an entire query objet to a publication (or Method) is not at all secure since an attacker can then send any query. Perhaps it doesn't matter with your Products collection.

How to get Iron-router query parameters in server hook

I am trying to add a referral system to my project, so currently I am basing it off of this package. The issue I am running into is my project only uses accounts-google and not accounts-password. The way this package works is it adds the iron router query parameters for the referrerCode (/register?r=ReferralCodeHere)through a preSignUpHook. I believe this only works with accounts-password wont work when creating an account with an API such as accounts-google.
My idea around this is to use a Meteor.users.before.insert hook to grab the iron router query parameters and insert them into my referrerCode field in Meteor.users since I'm already using Meteor Collection Hooks for a couple of other things.
The issue is I havent been able to find a way to get the query parameters on the server, I was hoping to do something like this:
Meteor.users.before.insert(function(userId, doc) {
doc.referrerCode = Referrer._referrerCode; // Link 1
});
(Link 1)
But this will just come up as undefined.
If I'm at my register page and it has a query like this for example: example.com/register?r=12345 Then I run Router.current().params.query.r on the client it returns 12345. Basically I just need to have that saved to the referralCode field in Meteor.users when a new user creates an account, if a referral code exists in the register URL.
I'm a bit lost with this one. I thought about setting it as a Session variable and then getting that in the before.insert hook, but that again only works on the client side. I'm thinking a meteor method might be best for this, but I'm not exactly sure how I would structure it. Any help is greatly appreciated!
Put the referral token into profile
Use that in your hook
Below I've copied some code that I've used before. It is built around an Invitations collection that tracks who invited who:
client:
var profile = {};
... any other profile settings you've captured
if ( token ) profile.referralToken = token;
Accounts.createUser({ email: email, password: password, profile: profile }, function(err){ ...})
hook:
if ( options.profile.referralToken ){ // referral case
var invitation = Invitations.findOne({ token: options.profile.referralToken });
if ( invitation )
user.invitationId = invitation._id; // the invitation used
user.invitedBy = invitation.userId; // the referring user
}
delete options.profile.referralToken;
}
return user;

How to use firebase rule to check is user group array and record group array intersect

I have a list of records in firebase which will have a group property with zero or more groups on it. I also have the firebase auth object which will also have zero or more groups on it as well. I would like to set up a .read firebase rule for my records that will check if the two have at lease one group that exists in both lists.
Put another way I have a user that has an array of groups that have been assigned to it. I have some records that also has some list of groups on them that specify what groups the user must have to access them. If the logged in user tries to access the record, I want to make sure that the user has at least one group that the record requires.
On the client I would do something like _.intersect(userGroups, recordGroups).length > 0
I'm not sure how I would do this in a firebase rule expression. It would be cool if it worked something like this.
Record:
{
someData: "test"
groups: ['foo', 'bar']
}
Firebase Auth Object:
{
userName: "Bob",
groups: ['foo', 'bar']
}
Rule Data:
{
"rules": {
"records": {
"$recordId": {
".read": "data.child('groups').intersectsWith(auth.groups)"
}
}
}
}
Thanks.
Update:
I think that if hasChildren() used || instead of && I could put the group names in they key position and check for their existence this way. Something like "data.child('groups').hasChildren(auth.groups, 'or')"
Where Record:
{
someData: "test"
groups: {
'foo': '',
'bar': ''
}
}
Update2:
Based off Kato's comment & link I realize that even if hasChildren could do OR it still wouldn't work quite right. Requests for individual records would work but requests for all records would error if the current user didn't have access to every record.
It is still not clear how you would structure data to make this work. If a record could belong to many groups how would that work? This is a very common scenario(basically how linux group permissions work) so I can't be the only one trying to do this. Anyone have any ideas/examples of how to accomplish this in firebase?
At the current moment, I believe it's impossible. There's a limited number of variables, methods, and operators allowed, listed here:
Firebase Security Rules API
Since function definitions are not allowed in the rules, you can't do anything fancy like call array.some(callback) on an array to do the matching yourself.
You have three options that I know of:
1) Copy data so you don't need to do the check. This is what I did in my project: I wanted some user data (names) available to users that shared a network in their network lists. Originally I wanted to check both member's network lists to see if there was at least one match. Eventually I realized it would be easier to just save each user's name as part of the network data so there wouldn't have to be a user look up requiring this odd permissions. I don't know enough about your data to suggest what you need to copy.
2) Use strings instead of arrays. You can turn one string into a regex (or just save it in regex format) and use it to search the other string for a match.Firebase DB Regex Docs
3) If you have enough weird cases like this, actually run a server that validates the request in a custom fashion. In the DB, just allow permissions to your server. You could use Firebase Cloud Functions or roll your own server that uses the Firebase Admin SDK
Nowadays, there's another possibility: to use Firestore to deliver your content, possibly in sync with the Realtime Database.
In Firestore, you can create rules like this:
function hasAccessTo(permissionList) {
return get(/databases/$(database)/documents/permissions/$(request.auth.uid))
.data.userPermissions.keys().hasAny(permissionList)
}
match /content/{itemId} {
allow read: if hasAccessTo(resource.data.permissions.keys());
}
The following data would allow a read of $CONTENTID by $UID, because the user permissions set intersects with the possible permissions required to access the content (with access123). My scenario is that a piece of content can be unlocked by multiple In-App Purchases.
{
permissions: {
$UID: { userPermissions: { access123:true, access456:true } },
...
},
content: {
$CONTENTID: { ..., permissions: { access123, access789 } },
...
}
}
For a progressive migration, you can keep data in sync between the Realtime Database and Firestore by using a one-way Cloud Function like this for example:
exports.fsyncContent = functions.database
.ref("/content/{itemId}")
.onWrite((snapshot, context) => {
const item = snapshot.after.val();
return admin
.firestore()
.collection("content")
.doc(context.params.itemId)
.set(item);
});

How get a collection's count on the server to the client?

I'm trying to get a collection's count from the server to the client. I want to use it for paging and just so users will know about the number of documents available. It's important the count does update if documents are added or removed.
One problem is paging, where I'm limiting the amount of documents sent to the client with publish/subscribe. But in the case below, the client will not know if the MyPix collection does contain more than 4 documents:
Meteor.publish('MyPix', function(cursor) {
return MyPix.find({}, {limit:4, skip:cursor});
})
This is abit tricky,
a quick solution is to use this package, publish-counts
Server
Meteor.publish('publication', function() {
Counts.publish(this, 'numberOfPosts', Posts.find());
Counts.publish(this, 'numberOfUsers', Users.find());
});
Client
Meteor.subscribe('publication')
then to get numberofUsers or numberOfPosts
Counts.get('numberOfUsers') // returns numberOfUSers users

Meteor.user() not fully loaded after login

I am using Meteor 0.8.2 with accounts-facebook. I set up a limited publication for the users this way:
Meteor.publish('users', function () {
return Meteor.users.find({}, {fields: {'profile.picture': 1, 'profile.gender':1, 'profile.type':1}, sort: {'profile.likes': -1}});
});
Now this works great: when I requests a user list from the client I get a list of all users, with the current user's fields all shown and only the 3 published fields for the others. Except: right after login.
When I login and type Meteor.user(), here is what I get:
_id: "uACx6sTiHSc4j4khk"
profile: Object { gender="male", type="1", picture="http://....jpg"}
This stays like that until I refresh the page using the browser button. After refreshing, Meteor.user() gives all the fields available, while Meteor.users.find() still gives the correct restrictions. (except for the current user of course)
Why does my current user not get all its fields right away? I read about a Meteor.userLoaded() method used to wait for the user to be loaded, but it seems to be obsolete in the latest version.
You're running into an interaction between the restriction of merging fields across publications, and the default user publication which sends the profile field.
First, note that there is a built-in publication that always sends the currently logged in user's entire profile field to that user:
https://github.com/meteor/meteor/blob/devel/packages/accounts-base/accounts_server.js#L1172
Second, merging of fields at more than one level deep is currently not supported:
https://github.com/meteor/meteor/issues/998
What you currently have is an issue where the default publication is sending something like the following
{
username: ...,
emails: [ ... ],
profile: {
... all fields ...
}
}
whereas the publication you have set up is sending
{
profile: {
picture: ...
gender: ...
type: ...
}
}
These get merged on the client according to the rules for how subscriptions are resolved (http://docs.meteor.com/#meteor_subscribe). In particular, see the last paragraph. Meteor knows to merge the username and email fields with the profile field. However, it doesn't do this merging at the inner level. So one of the profile fields will get chosen arbitrarily to show up in the client's collection. If the first one wins, you will see profile.likes. If the second one wins, you won't.
It's likely that this behavior is somewhat deterministic and changes depending on whether a normal login handler is called or a resume handler (i.e. when reloading the browser). Hence why it looks like it hasn't loaded.
As Andrew explained, and as I kinda thought, what happened is that there is another "hidden" publication for the current user, which conflicts with mine. All I had to do in order to fix this was to simply exclude the current user from my publication, since it is already fully published by default:
Meteor.publish('users', function () {
return Meteor.users.find({_id:{$ne: this.userId}}, {fields: {'profile.picture': 1, 'profile.gender':1, 'profile.type':1}, sort: {'profile.likes': -1}});
});
This simple $ne does it for me.

Resources