Meteor - Deny read? - meteor

How do you deny reads of a collection in meteor?
There is no allow/deny method for this and you can't do it in the collection publish filter since it only runs once. I'm shocked, I thought it would just make sense that a template could render blank if you denied a read.
Meteor looks like it works fine in a website with a single type of user but how do I segregate data more for reading?

You can call .stop() in your publish callback function after checking the user role
There is an example here. https://github.com/alanning/meteor-roles#usage-examples
Meteor.publish('secrets', function (group) {
if (Roles.userIsInRole(this.userId, ['view-secrets','admin'], group)) {
return Meteor.secrets.find({group: group});
} else {
// user not authorized. do not publish secrets
this.stop();
return;
}
});
I found this answer after quite a bit of googling. Hopefully this helps other people. I still find it odd that you can't just define a read property on allow or deny and achieve the same behavior.

My answer is for deny read operations on collections:
There is no official solution for this AFAIK,but I think there is a community package for that
try
meteor add matb33:collection-hooks
link to the package on atmosphere https://atmospherejs.com/matb33/collection-hooks
From the page
Extends Mongo.Collection with before/after hooks for
insert/update/remove/find/find
Copy pasted some example codes from github
.before.find(userId, selector, options)
.after.find(userId, selector, options, cursor)
.before.findOne(userId, selector, options)
.after.findOne(userId, selector, options, doc)

Related

Firestore per-field security rule

I have studied the answer to this question (which has an extremely similar title): Per field rules in Firestore Security Rules. The solution in that case was to make a field unmodifiable, I do not think that is what I am after here.
I have a posts collection with a data structure as follows.
{
uid: string, // this is the original poster's UID
title: string,
content: string,
likesCount: number,
likerUIDs: string[]
}
I would like to restrict writes to the title and content fields to users with an auth token UID that matches the post's uid field. However, any authenticated user should be able to increment the likesCount and add their own UID to the likerUIDs field.
It seems like per-field security rules are not really supported. Is the solution here to maintain a separate collection with different rules but the same keys as the posts, for example post-likes, that contains the likesCount and likerUIDs fields? Or is there a firestore security rule trick to achieving this?
EDIT
Thanks to Doug and Frank's comments (extremely helpful video series by the way), I was able to come up with a solution to my initial question. As suggested in the accepted answer, I'm going to do this with a callable function, since it is perfect for this case. For those who stumble upon this question and want to accomplish something similar, I've pasted the rules I ended up with here. These rules accomplish exactly what is described in the question, but a callable function was definitely the way to go here.
function isOwnerCurrent() {
return request.auth.uid == resource.data.uid;
}
function isOwnerIncoming() {
return request.auth.uid == request.resource.data.uid;
}
function isUnmodified(key) {
return request.resource.data[key] == resource.data[key]
}
match /posts/{post} {
function validateNonOwnerPostUpdate() {
return isUnmodified('title') && isUnmodified('content') &&
isUnmodified('created') && isUnmodified('updated');
}
allow read: if true;
allow create: if isOwnerIncoming();
allow update: if (isOwnerCurrent() || validateNonOwnerPostUpdate()) && isUnmodified('uid');
allow delete: if isOwnerCurrent();
}
For updates, I am checking if the user is either the owner of the post, or only updating the so-called "public" fields of likesCount and likerUIDs, and for both they must not be modifying the owner UID of the post. Like mentioned in the accepted answer, this isn't great because anyone will be able to edit these fields and mess up the numbers.
I think it is better to use cloud function to solve this. you can use callable cloud function when that other users (not document owner) like that post. https://firebase.google.com/docs/functions/callable . because cloud function can bypass security rules
I think it is safer you do it through cloud function unless that likesCount is not that important. because if someone can hack your client app than they can modify your code. you will update the document like this
db.doc(`posts/${postID}`).update({
likesCount: admin.firestore.FieldValue.increment(1)
})
if they hack your app, then they can change the increment from 1 to 100 for example. yes you can do the same via security rules but you have to add additional check , and it will be complicated and error prone IMO.

Firestore rules for a survey web app and a shared link

I'm trying to build a survey web app with firebase and have a certain user flow in my mind but I don't know if it can be done:
In this app you would signup/login and build your survey, which will be stored like this:
collection("creator").doc("creatorID").collection("surveyData").
After storing the data, you would get a link (e.g. webapp.com/forUser/secret/surveyName) you then can share with your friends. This link opens the same web app (from another entry point) but with anonymous authentication, hydrating and displaying only the content of this one survey from this one user.
There are two main problems here:
Pointing the user's web app to the right data via the link and only being able to access this one survey.
Writing the user answers in the creator's files
For 1) Can I pass a secret via the link (could I use the creatorID for this or is this not safe/secure?), which points the user to this one survey data (without granting any other access)? If so, how to do this without hard coding the secret into the authentication rules?
For 2) I could use a two-step process:
The user answers the survey questions and the results are stored in something like:
collection("user").doc("secret").collection("surveyData").
When the creator opens the app later on (or done via cloud functions), the app fetches all the answers from the shared secret doc. Can this be done in one step?
I hope this makes sense somehow. Maybe my ideas are way too complicated and there is an easy way to do this or are there some best practices in this regard?
Thanks for your help!
A while ago I built a hashing package into Firebase Rules. It might be possible to use that?
Example rules:
rules_version="2"
function hashUserCreds(auth) {
let combinationOfCreds = string(auth.uid) /* + string(auth.other.stuff) */;
// hashing.sha256 will work on strings as well, but it's more readable to
// make this explicit.
return hashing.sha256(combinationOfCreds.toUtf8()).toBase64();
}
service cloud.firestore {
match /databases/{database}/documents {
match /{ownerHash}/{surveyId} {
function isOwner() {
return hashUserCreds(request.auth) == ownerHash;
}
allow read;
allow write: if isOwner();
match /{responseId} {
function isResponder() {
return hashUserCreds(request.auth) == responseId;
}
allow update: if isResponder();
allow read, create: if isOwner();
}
}
}
}
If you want open-responses then you change the final two permissions, but it sounds like you want the survey creator to choose who can respond which I've tried to express there.
Using hashes instead of names will provide better anonymity, obscure PII, and force the restriction that a user can only edit their data.

Check if username exists in Meteor

Been digging around for a solution but none for Meteor. If any, please let me know. I want to check if a username is already taken.
I understand that this only works on the server side only:
u = Accounts.findUserByUsername('foo');
console.log(u.username); #=> foo
I cant get my head around their pub/sub as I can only see information based on the current user. Is meteor saying that what I want is not possible?
When a user is filling out their details upon registration, I want them to be alerted (as they type) if the username they are using is already taken. But that logic I can easily code but need to know how to talk to the server to tell me the information.
You could write a Meteor method for that:
Meteor.methods({
doesUserExist(name) {
return Accounts.findUserByUsername(name) != null;
}
});
Note that you have to define this method on the server but not on the client (e.g., by defining it in a file inside the server directory). That way Meteor won't try to simulate it on the client (which would fail because Accounts.findUserByUsername is not defined there).
Call the method as the user types:
Meteor.call('doesUserExist', name, function(error, result) {
// `result` is true if the user exists.
});

How to Reproduce Meteor.user() Client-Server effect for a different Collection?

Just how Meteor.user() method is available on Client & Server for the "current user" I would love to reproduce this kind of functionality for different custom collections. For example, my app uses a "clouds" collection as a type of room for a group of users to be in. Obviously there are various cloud instances and I don't always want to be passing the cloudId into every single meteor method. Ideally I could have like a Meteor.cloud() function that would give me the current cloud on the client and server.
My thoughts on approaching this:
What I have been doing thus far is piggy-backing off of Meteor.user() by storing a currentCloudId property inside the user profile and setting that on a route beforeAction. However this limits the user to only being in 1 cloud at a time.
Using the Meteor.connection ID somehow to keep a map of connectionIds to cloudIds. This would work great in theory....however it seems that Meteor connection IDs cannot be heavily relied on as they might change during reconnects or other random scenarios. Also you would have to then "manange" that collection of "cloudConnections" and remove old stale ones and such.
Im using Iron Router....and if it were possible to get the current route data on the server that would also solve my problem but I am not sure how to access that on the server?
--- Basically I would love for a simple straight forward way to mimic Meteor.user() behavior for other collections.
Thanks for all your help :)
You can just create a function inside /lib that looks something like this:
getUserClouds = function getUserClouds () {
return Clouds.find({ $elemMatch: { $eq: Meteor.userId() } })
}
This will work both on the client and on the server. But it will always return a Cursor pointing to 0 docs. So you'll need a publication:
Meteor.publish('userClouds', function () {
return Clouds.find({ $elemMatch: { $eq: this.userId } })
})

How to properly use user_access($string, $account) in Drupal?

I would like to limit the use of some url's. Let's say node/add and node/7 (just random examples). I'm thinking the best way to do this is to use the user_access function.
But as we are used to it, the Drupal documentation doesn't help much. When I just use the function, I get the message the function is already in use. So my best guess is to use this existing function with my own arguments in my custom function in my custom module.
But in this way I need to catch the page before loading it. Or I'm I missing something here?
EDIT:
I've set this
global $user;
$items['node/add/%']['access callback'] = array('_mymodule_node_access');
$items['node/add/%']['access arguments'] = array(0,2, $user);
But for some reason, Drupal isn't picking up the % card for all types. It's just working for one type (script). Other terms like page or fiche aren't getting picked up... % is a Drupal wildcard right?
EDIT:
I just found out there are already some paths in the database. How can I overwrite them? What I need is one selector which can select all four content types (fiche, page, script and news-item).
The way to define a particular access function for a path is to set the access callback for the path's menu item in hook_menu(). This is slightly different for existing paths, in that you need to implement hook_menu_alter() to edit the existing access callback for that path:
function mymodule_menu_alter(&$items) {
$items['node/add']['access callback'] = 'mymodule_node_add_access_callback';
}
function mymodule_node_add_access_callback() {
// return TRUE to allow access, FALSE to deny
}
This gets a bit more fun when we're talking about node pages as their menu items is defined using a wildcard node/%. This means that using hook_menu_alter() you can only change the access callback for all nodes.
Fortunately Drupal has a hook_node_access hook to come to the rescue:
function mymodule_node_access($node, $op, $account) {
$restricted_nids = array(7, 10, 12);
if (in_array($node->nid, $restricted_nids) && $op == 'view') {
if ($some_condition_is_true) {
return NODE_ACCESS_ALLOW;
}
return NODE_ACCESS_DENY;
}
return NODE_ACCESS_IGNORE;
}
Hope that helps
EDIT
If that all seems like a bit much hassle you might get some joy installing the Path Access module, I think it has the functionality you're after.
ANOTHER EDIT
I think the reason overriding the wildcard isn't working in this case is because the node module explicitly defines a path for each node type, e.g. node/add/page, node/add/article, etc. Because Drupal will take an exact match (node/add/page) over a wildcard match (node/add/%) you're actually overriding the wrong menu item.
Try specifying the path explicitly in your hook_menu_alter() function (note that the access callback should be a string and not an array as you currently have):
$items['node/add/page']['access callback'] = '_mymodule_node_access';
$items['node/add/page']['access arguments'] = array(0,2, $user);
It's also worth noting that the $user object you're passing will always be the user object of the logged in user who cleared Drupal's caches (since menu items are rebuilt when the cache is rebuilt). If you're looking to pass the current logged in user (i.e. the one logged in at the time the page is accessed) that's a different thing altogether...I'd advise asking another question on it as it can be a tricky bugger and you want to get as much input as possible from people on here.

Resources