Firebase push and set multiple nodes atomically with security rules [duplicate] - firebase

I want to create an increment field for article likes.
I am referring to this link: https://firebase.google.com/docs/database/android/save-data#save_data_as_transactions
In the example there is code for increment field:
if (p.stars.containsKey(getUid())) {
// Unstar the post and remove self from stars
p.starCount = p.starCount - 1;
p.stars.remove(getUid());
} else {
// Star the post and add self to stars
p.starCount = p.starCount + 1;
p.stars.put(getUid(), true);
}
But how can I be sure if the user already liked/unliked the article?
In the example, user (hacker) might as well clear whole stars Map like this and it will save anyway:
p.stars = new HashMap<>();
and it will ruin the logic for other users who were already liked it.
I do not even think you can make rules for this, especially for "decrease count" action.
Any help, suggestions?

The security rules can do a few things:
ensure that a user can only add/remove their own uid to the stars node
"stars": {
"$uid": {
".write": "$uid == auth.uid"
}
}
ensure that a user can only change the starCount when they are adding their own uid to the stars node or removing it from there
ensure that the user can only increase/decrease starCount by 1
Even with these, it might indeed still be tricky to have a security rule that ensures that the starCount is equal to the number of uids in the stars node. I encourage you to try it though, and share your result.
The way I've seen most developers deal with this though is:
do the start counting on the client (if the size of the stars node is not too large, this is reasonable).
have a trusted process running on a server that aggregates the stars into starCount. It could use child_added/child_removed events for incrementing/decrementing.
Update: with working example
I wrote up a working example of a voting system. The data structure is:
votes: {
uid1: true,
uid2: true,
},
voteCount: 2
When a user votes, the app sends a multi-location update:
{
"/votes/uid3": true,
"voteCount": 3
}
And then to remove their vote:
{
"/votes/uid3": null,
"voteCount": 2
}
This means the app needs to explicitly read the current value for voteCount, with:
function vote(auth) {
ref.child('voteCount').once('value', function(voteCount) {
var updates = {};
updates['votes/'+auth.uid] = true;
updates.voteCount = voteCount.val() + 1;
ref.update(updates);
});
}
It's essentially a multi-location transaction, but then built in app code and security rules instead of the Firebase SDK and server itself.
The security rules do a few things:
ensure that the voteCount can only go up or down by 1
ensure that a user can only add/remove their own vote
ensure that a count increase is accompanied by a vote
ensure that a count decrease is accompanied by a "unvote"
ensure that a vote is accompanied by a count increase
Note that the rules don't:
ensure that an "unvote" is accompanied by a count decrease (can be done with a .write rule)
retry failed votes/unvotes (to handle concurrent voting/unvoting)
The rules:
"votes": {
"$uid": {
".write": "auth.uid == $uid",
".validate": "(!data.exists() && newData.val() == true &&
newData.parent().parent().child('voteCount').val() == data.parent().parent().child('voteCount').val() + 1
)"
}
},
"voteCount": {
".validate": "(newData.val() == data.val() + 1 &&
newData.parent().child('votes').child(auth.uid).val() == true &&
!data.parent().child('votes').child(auth.uid).exists()
) ||
(newData.val() == data.val() - 1 &&
!newData.parent().child('votes').child(auth.uid).exists() &&
data.parent().child('votes').child(auth.uid).val() == true
)",
".write": "auth != null"
}
jsbin with some code to test this: http://jsbin.com/yaxexe/edit?js,console

Related

Using Security Rules in Firebase to Limit Access in a Social Media App

I am implementing a social media app where people make posts and others can upvote/downvote them. I am using Firebase Realtime Database. I want authenticated users to be able to add posts and change the score of other posts by upvoting or downvoting but I don't want them to be able to change the contents of or delete other posts. Is there a way to give users access to add new posts without letting them change existing posts? Also is there a way to make sure that when users change the score of a post they only increment or decrement it? I was thinking of doing something like this but it doesn't work:
{
"rules": {
".read": "auth.uid !== null",
"posts": {
".write": "auth.uid !== null",
"$postID": {
"$score": {
".validate": "newData.isNumber() && (newData.val() == $score + 1 || newData.val() == $score - 1)"
}
}
}
}
}
please limit yourself to one question per post
is there a way to make sure that when users change the score of a post they only increment or decrement it?
Yes that is possible, but in your code you're comparing newData.val() (the new value) to $score, which is the key (string) of the node. You'll want to compare against data.val() instead, so:
".validate": "newData.isNumber() && (
newData.val() == data.val() + 1 ||
newData.val() == data.val() - 1
)"
Note btw that .validate rules are not evaluated when a node is deleted, so you might want to do this in the .write rule if it should also prevent deletes.

What is the appropriate handling of security rules for the value of a Double Child?

Say you have a child called peopleWhoLike that has the form:
"people": {
"$uid": {
peopleWhoLike: {
ChildByAutoID: uid of the user who likes the current user.
}
}
"$uid1” ….
////for example
“people” : {
“Cawefaeerfregherthe” : {
“peopleWhoLike" : {
"Optional(\"-MDWpTyQjA6a1VzTWvEV\")" : "QqQpDheHC4cC9bs4KtreArFiS8g2",
}
}
I know how to target rules for peopleWhoLike, for example giving authorized users access to the node. What I am talking about here is not rules for that node, but rather rules for its value. So in this instance, in the Double (ChildByAutoID: UID of user who likes logged in user), I need to make a rule that checks who that UID can be. For example only allow write if he is auth.uid
I thought maybe something like this:
"peopleWhoLike" : {
"$peopleWhoLike": { "
.read": "auth.uid != null",
".write": "$peopleWhoLike == auth.uid"
} }
In practical terms:
So in practical terms, the user QqQpDheHC4cC9bs4KtreArFiS8g2 wants to like the user Cawefaeerfregherthe. I need a write rule to allow this. I want to make sure that QqQpDheHC4cC9bs4KtreArFiS8g2 is the current user (auth.uid), so I need to make some kind of rule at the Double (Optional(\"-MDWpTyQjA6a1VzTWvEV\")" : "QqQpDheHC4cC9bs4KtreArFiS8g2) to see if he is auth.uid – frank timmon just now

firebase rule - retrieve only items where child has certain value

Is there a way to add a firebase security rule that prevents certain items in a collection from being read based on a value within each child item?
My example:
JSON:
orders{
orderA: {
name: x,
company:a
isDeleted: true
}
orderB: {
name: y,
company:a
isDeleted: false
}
}
It would be great to restrict users to be only able to read all orders where isDeleted === false
My Rule as I currently have (NOT WORKING):
"rules": {
"orders": {
".indexOn": "companyId",
".read": "auth !== null && data.child('isDeleted').val() === false",
"$ord": {
".write": etc
}
},...
The above doesnt work because "data" doesnt represent the right object - I can only use data inside the "$res" area.
If I remove "&& data.child('isDeleted').val() === false" it works but of course brings back both records.
My request is something like this, so the $res doesn't apply - as I'm getting ALL orders by companyId
http://mysite.firebase.io/orders?auth="xyz"&orderBy="companyId"&equalTo="a"
Is it even possible for a "retrieve all" type REST call like this and to filter out certain values via the firebase security rules? Am I just as well to retrieve all and then filter them out once I get them back in the front end??
Firebase's server-side security rules don't filter data. I highly recommend checking out the documentation, and some previous questions on this topic, as it's a very common misconception.
Instead the rules merely ensure that any read (in this case) operation, adhere to your requirements. So for your ".read": "auth !== null && data.child('isDeleted').val() === false", rule that means that the server checks if the user is logged in (they are), and that the node they are reading has a child isDeleted that is false. And since /orders/isDeleted does not exist, the read gets rejected.
You can securely allow access to only undeleted data by combining a query that only selects undeleted nodes with security rules that validate this query. Based on the example in the documentation on query based rules that'd look something like:
"rules": {
"orders": {
".indexOn": "companyId",
".read": "auth !== null &&
query.orderByChild == 'isDeleted' &&
query.equalTo == false"
}
}
This will work to get only non-deleted nodes. But since you can only order/filter on one property, you can't then also filter on companyId. You could allow that by introducing a synthesized isDeleted_companyId property, as shown in my answer here: Query based on multiple where clauses in Firebase

Firebase database rules, differentiate between create and update

I want to differentiate somehow between creating or updating a list with write rules. Any user can create a new chat, while only a user inside that chat should be able to update it. So basically, I want to have an update rule that checks in another denormalized list if that user is inside that chat before being able to update (similar if not equal to the read rule which works fine) without breaking the auth != null rule for create new chat.
"chats": {
"$chat": {
".write": "auth != null",
".read": "root.child('chats_by_user').child(auth.uid).child($chat).exists()"
}
}
Is there a way to do this?
An update usually means that newData will be different from existing data.
So the rule to only let the user update if he is inside the chat would be:
"newData.val() != data.val() && root.child('chats_by_user').child(auth.uid).child($chat).exists()"
And a create means that there is currently no data under that node.
So the rule to only allow create operations would be:
"!data.exists()"
Now putting it all together:
"chats":{
"$chat":{
".write":"auth!=null && ((newData.val() != data.val() && root.child('chats_by_user').child(auth.uid).child($chat).exists()) || !data.exists() )",
".read":"root.child('chats_by_user').child(auth.uid).child($chat).exists()"
}
}

Setting a "publishing time" for Firebase database entries?

I am planning an app that would allow users to create posts which shouldn't be readable by other users until the date/time that the creator of the post has selected.
Is this possible using the Firebase Realtime Database? How would I implement something like this?
I guess that simply implementing it in client code would not be secure, since authenticated users could still GET all posts manually, even the "not yet published" ones? Can I use database rules to do it, even though each post would have their individual publish date/time?
Yes, it's possible with Firebase. All you need to do is to add a flag for each post with the default boolean value of false and a TIMESTAMP. This means that by default, the post cannot be readable by other users. Then you need to use a listener on that TIMESTAMP filed to see when the current date/time is equal with the date/time that the creator of the post has selected. If it's equal then just set the value of the flag to true. This means that the post can be readable by other users. That's it!
You can achieve this also using security rules like this:
//Ensure that data being read is less than or equal with the current date/time.
".read": "data.child('timestamp').val() <= now"
The only solution I can think of is creating an entirely new node scheduledPosts with it's own rules to only allow the creator to see/edit it before the publish date (if that's what you're aiming for).
{
"users": {
"uid_1": {
"someData": "data",
...
}
},
"scheduledPosts": {
"pid_1": {
"postData": "data",
"uid": "uid_1",
"publishDate": 1695840299, // must be in milliseconds since epoch
...
}
}
}
And your scheduledPosts's rules would look as follows:
{
"rules": {
"scheduledPosts": {
"$post_id": {
".read": "root.child('scheduledPosts').child($post_id).child("publishDate").val() < now || root.child('scheduledPosts').child($post_id).child("uid") === auth.uid",
".write": "root.child('scheduledPosts').child($post_id).child("publishDate").val() < now || root.child('scheduledPosts').child($post_id).child("uid") === auth.uid"
}
}
}
}
You can use read rule, with combination of now
The rule will look something like this:
".read": "(auth != null) && (data.child('publish_time').val() < now)"

Resources