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)"
Related
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
So I am trying to give premission to read data from the parent but I am not sure what I am doing wrong, probably some syntax but I tryed a lot of combinations and I cant figure out why, what is the proper way to do it?
Thanks in advance!
tryed a lot of combinations with "}" but Its always says my syntax is wrong
"rules": {
"ca": {
"$date": {
"$game": {
///if($game.hasChild(auth.uid))
"$uid":{
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
}
}
}
}
if the user is from $game so is allowed to read data from there as well as read is own data.. Thanks!
Firebase security rules do not by themselves filter data.
If you want to retrieve only data that has a certain child, you'll need to query on the value of the child. E.g. ref.orderByChild("first_letter").startAt("A").endAt("Z"). Once you have such a query, you may be able to enforce it with query based security rules.
But your structure hints at a scenario that isn't well modeled. Your current data model makes it easy to find the users for a game, it does however not make it easy to find the games for a user. To make that equally easy, you'll need to add a secondary data structure where you store the game (IDs or data) for each user.
So something like:
"user_games": {
"$uid": {
"$gameid": true
}
}
Also see:
Firebase query if child of child contains a value
Firebase Query Double Nested
I have sign up system where I want only users to sign up if they have a valid secret key which I shall provide to users who want to register. If key is in db, then proceed to sign up. Thus I have generated random non repeated 8 chars and stored them in the real time database in the following structure:
Secrets:
"x5f1n9v0":
"Status" : 1
"C8vT2xxY":
"Status" : 1
And so on
..
{
"rules": {
"secrets":{
"$secret": {
".read": true,
".write": false
}
}
}
}
First question regarding the aboves rules:-
In this case no one can add a new secret key ?
Also the read will only be valid if someone has a valid key from my list ? Nobody can read the whole list ? Any bugs in this ?
Now suppose another set of rules where I want to write to the child of each key iff the user knows the valid id.
If I change the rule for write to true, will this work and no bugs to hack it ?
"rules": {
"secrets":{
"$secret": {
".read": true,
".write": true
}
}
}
}
Thanks
In this case no one can add a new secret key?
With those first rules, only someone with administrative access can add keys.
Also the read will only be valid if someone has a valid key from my list? Nobody can read the whole list?
There is indeed no way to read the entire list with your first set of rules. Someone can only read a secret if they know its key.
With your second set of rules:
"secrets":{
"$secret": {
".read": true,
".write": true
}
}
Now anyone can write any secret. That is probably not what you want. If you want to only allow them to change the data that already exists under an existing key, you'll want to check if there is any data already:
"secrets":{
"$secret": {
".read": true,
".write": "data.exists()"
}
}
If you want them to not be able to change-but-not-delete the existing data, that would be data.exists() && newData.exists(). If you have additional requirements about the data formats users can write, you'll want to add those to a corresponding .validate rule.
Your last set of rules is both invalid and meaningless. It's invalid because $secrets is not defined. But even if it was defined $secrets === $secrets will always be true.
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
I'm having an issue setting my Security rules properly, specifically reading the post data.
The data hierarchy goes:
posts : {
0 : {
title: "Post One",
userId: 6
}
},
users : {
6 : {
name: "My Name"
}
}
And my rules are:
{
"rules": {
"posts" : {
"$post": {
".read":"data.child('userId').val() == auth.id",
".write":"newData.child('userId').val() == auth.id"
}
},
"users":{
"$user": {
".read":"auth.id == $user",
".write":"auth.id == $user"
}
}
}
}
I know that the "auth.id" is 6, because it's pulling the rules correctly for my user info. If I change the rules to pull the number statically, it works:
"$post": {
".read":"data.child('userId').val() == 6",
".write":"newData.child('userId').val() == auth.id"
}
but using auth.id does not. Is there something I'm missing?
One thing to keep in mind is that security rules are type-safe. In particular, In the rules, "6" != 6 (since one is a string and one is a number). So perhaps your auth.id is "6" (as a string), but your userId is 6 as a number?
If that's the case, one potential fix would be changing your rule expression to something like:
data.child('userId').val() + '' == auth.id
which will force userId to be a string. Alternatively, you could change your data to make sure userId is always stored as a string.
You haven't included the code you're using to look up this data--probably where the error is--or the error you are receiving; those would help quite a bit.
Your rules should work fine, assuming you are attempting to read a single post at a time, and assuming your authentication is set up correctly.
A quick guess would be that you're trying to read the entire "posts" path, and using security rules to filter your posts. But security rules are essentially atomic. If you try to read "posts", and one of the posts has a rule that prevents read, the entire operation is going to fail.
Instead, you need to segment the posts into paths where all the data can be read by the authenticated user, then you can apply security rules accordingly.
One thing that will help immensely is to test your security rules by going into your Forge and using the "simulator". You can log in as any user, then try a read/write, and see exactly which security rules is failing and why.