How to create time-expiring data with Firebase Rules? - firebase

This talk mentions time-expiring data using Firebase rules at 22:55
https://www.youtube.com/watch?v=PUBnlbjZFAI
How can one do this ?
I didn't find any information regarding this.

I recommend two solutions.
1) Use cloud functions to record a message path and the date it was posted. Then every hour sort that list by date, pick all the expired ones, and create a deep update object to null out every expired message. Nowadays you can use Cron Scheduler to handle the periodic flush.
2) Make a rule that says anyone can delete expired messages and make it so that clients automatically delete expired messages when they are in a chat room.

Written here: https://firebase.google.com/docs/database/security/securing-data
You can't have it auto delete your data but you can make them unreadable (which is the same thing from the user standpoint). Just send a timestamp child field with you data and check against it.
{
"rules": {
"messages": {
"$message": {
// only messages from the last ten minutes can be read
".read": "data.child('timestamp').val() > (now - 600000)",
// new messages must have a string content and a number timestamp
".validate": "newData.hasChildren(['content', 'timestamp']) && newData.child('content').isString() && newData.child('timestamp').isNumber()"
}
}
}
}

Same question here.
You can't do it using firebase rules. You should either have a NodeJS backend removing your old data or clients doing it for you. For example, before a client retrieves data, he could remove old data.

Related

Delete node automatically using Firebase Rule

I am using the firebase for store some users data. So now i want to delete old data with check 'last_ubdated' time < 5 min. please help for create the firebase rule for this. below is my data structure.
I have created the rule, but its not working properly.(code is below)
{ "rules": {
".read": true,
".write": true,
"$drivers":{
".indexOn": ["driverId"],
".write": "newData.exists() || data.child('last_updated').val() > (now)",
} } }
Firebase security rules can't change any data after it's been added. They only take effect at the time data is read or written by the client. If you want to arrange to delete something on a schedule, you will need another solution.
See also: Cloud Functions for Firebase trigger on time?
As Doug said, security rules can't change the data. They merely affect what (read and write) operations are allowed.
An alternative would be to use a query to only request nodes that have not expired, and then use security rules to ensure only that query is allowed. For an example of that, see my answer here: How to make data unreadable once the time indicated on its time stamp has passed?

Firestore Rules verify timestamp with a Flutter client

I want to send the creation time of a Firestore document through the client and verify the time with Firestore Rules to avoid Cloud Functions calls (pricing).
Scenario
I am testing requests from clients against Firestore rules like this:
allow create: if request.resource.data.TIMEFIELD == request.time;
The request contains a TIMEFIELD that has a timestamp, just like request.time.
Problem
Apparently the request time and the time I am setting as a field right before sending the request are not equivalent, which makes this comparison impossible.
The following is the defition of request.time from the documentation.
When the request was received by the service.
I wonder if there is a way to set a field in a document equal to request.time.
I am unable to use server side timestamps because of an issue with Flutter.
Because of that I need to know how I could possibly validate client side timestamps like time.now with Firestore Rules.
You can use the Timestamp to add constraints to the time field (docs).
Here is an example of how to ensure that the change was within a certain amount of seconds:
function withinSeconds(secs) {
return request.resource.data.TIMEFIELD.seconds() - request.time.seconds() <= secs
&& request.resource.data.TIMEFIELD.seconds() - request.time.seconds() >= -secs
}
Edit
The above is for setting the value within a threshold of the request.time.
You can also just use the REST API in the mean time. Just make a write request that includes an update and a transform. The transform is where you would set the server timestamp. Here is a tool to help understand how to build the requests.
This has been implemented into the Flutter plugin for Cloud Firestore:
FieldValue.serverTimestamp()
Using this as a field's value will assign a timestamp equal to request.time to the field, server-side.
You can find out more about it in the API reference for cloud_firestore.
you'd first have to remember the creation (or last updated) timestamp:
firestore().collection("items").add({
....
created: firebase.firestore.FieldValue.serverTimestamp()
});
in order to let the client know of the timestamp, which you are trying to compare later on.

Tracking if a User 'likes' a post

This is more of a theoretical how database should be setup, and less about programming.
Lets say I have a news feed full of cards, which each contain a message and a like count. Each user is able to like a mesesage. I want it to be displayed to a user if they have already liked that particular card. (The same way you can see the post you like on facebook, even if you come back days later)
How would you implement that with this Firestore type database? Speed is definitely a concern..
storying it locally isn't an option, my guess would be on each card object, you would have to reference a collection that just kept a list of people who liked it. The only thing is that is a lot more querying.. which feels like it would be slow..
is there a better way to do this?
TL;DR
This approach requires more to setup, ie a cron service, knowledge of Firestore Security Rules and Cloud Functions for Firebase. With that said, the following is the best approach I've come up with. Please note, only pseudo-rules that are required are shown.
Firestore structure with some rules
/*
allow read
allow update if auth.uid == admin_uid and the
admin is updating total_likes ...
*/
messages/{message_key} : {
total_likes: <int>,
other_field:
[,...]
}
/*allow read
allow write if newData == {updated: true} and
docId exists under /messages
*/
messages_updated/{message_key} : {
updated: true
}
/*
allow read
allow create if auth.uid == liker_uid && !counted && !delete and
liker_uid/message_key match those in the docId...
allow update if auth.uid == admin_uid && the admin is
toggling counted from false -> true ...
allow update if auth.uid == liker_uid && the liker is
toggling delete ...
allow delete if auth.uid == admin_uid && delete == true and
counted == true
*/
likes/{liker_uid + '#' + message_key} : {
liker_uid:,
message_key:,
counted: <bool>,
delete: <bool>,
other_field:
[,...]
}
count_likes/{request_id}: {
message_key:,
request_time: <timestamp>
}
Functions
Function A
Triggered every X minutes to count message likes for potentially all messages.
query /messages_updated for BATCH_SIZE docs
for each, set its docId to true in a local object.
go to step 1 if BATCH_SIZE docs were retrieved (there's more to read in)
for each message_key in local object, add to /count_likes a doc w/ fields request_time and message_key.
Function B
Triggered onCreate of count_likes/{request_id}
Delete created docs message_key from /messages_updated.
let delta_likes = 0
query /likes for docs where message_key == created docs message_key and where counted == false.
for each, try to update counted to true (in parallel, not atomically)
if successful, increment delta_likes by 1.
query /likes for docs where message_key == created docs message_key, where delete == true and where counted == true.
for each doc, try to delete it (in parallel, not atomically)
if successful, decrement delta_likes by 1
if delta_likes != 0, transact the total likes for this message under
/messages by delta_likes.
delete this doc from /count_likes.
Function C (optional)
Triggered every Y minutes to delete /count_likes requests that were never met.
query docs under /count_likes that have request_time older than Z.
for each doc, delete it.
On the client
to see if you liked a message, query under /likes for a doc where liker_uid equals your uid, where message_key equals the message's key and where delete == false. if a doc exists, you have liked it.
to like a message, batch.set a like under /likes and batch.set a /messages_updated. if this batch fails, try a batch_two.update on the like by updating its delete field to false and batch_two.set its /messages_updated.
to unlike a message, batch.update on the like by updating its delete field to true and batch.set its /messages_updated.
Pros of this approach
this can be extended to counters for other things, not just messages.
a user can see if they've liked something.
a user can only like something once.
a user can spam toggle a like button and this still works.
any user can see who's liked what message by querying /likes by message_key.
any user can see all the messages any user has liked by querying /likes by liker_uid.
only a cloud function admin updates your like counts.
if a function is fired multiple times for the same event, this function is safe, meaning like counts will not be incremented multiple times for the same like.
if a function is not fired for some event, this approach still works. It just means that the count will not update until the next time someone else likes the same message.
likes are denormalized to only one root level collection, instead of the two that would be required if you had the like under the the message's likes subcollection and under the liker's messages_liked subcollection.
like counts for each message are updated in batches, ie if something has been liked 100 times, only 1 transaction of 100 is required, not 100 transactions of 1. this reduces write rate conflicts due to like counter transactions significantly.
Cons of this approach
Counts are only updated however often your cron job fires.
Relies on a cron service to fire and in general there's just more to set up.
Requires the function to authenticate with limited privileges to perform secure writes under /likes. In the Realtime Database this is possible. In Firestore, it's possible, but a bit hacky. If you can wait and don't want to use the hacky approach, use the regular unrestricted admin in development until Firestore supports authenticating with limited privileges.
May be costly depending on your standpoint. There are function invocations and read/write counts you should think about.
Things to consider
When you transact the count in Function B, you may want to consider trying this multiple times in case the max write rate of 1/sec is exceeded and the transaction fails.
In Function B, you may want to implement batch reading like in Function A if you expect to be counting a lot of likes per message.
If you need to update anything else periodically for the message (in another cron job), you may want to consider merging that function into Function B so the write rate of 1/sec isn't exceeded.

Firebase database rules – `data.exists()` always seems to be true, possible bug?

I am trying to secure my firebase database to allow the creation of new records, but not allow the deletion of existing records. Ultimately, I plan to utilise Firebase authentication in my app as well, and allow users to update existing records if they are the author, but I am trying to get the simple case working first.
However! No matter what I try in the database rules simulator, despite what the documentation seems to suggest, the value of data.exists() seems to always be true. From what I what I can understand from the documentation, the variable data represents a record in the database as it did before an operation took-place. That is to say, for creates, data would not exist, and for updates/deletes, data would refer to a real record that exists in the database. This does not seem to be the case, to the point where I am actually suspecting a bug in Firebase, as when setting the following rules on my database, all write operations are disallowed:
{
"rules": {
".read": true,
".write": "!data.exists()"
}
}
No matter what values I put into the simulator, be it Location or Data. I have even written a small EmberJS app to verify if the Simulator is telling the truth and it too, is denied permission for all write operations.
I really have no idea where to go from here as I am pretty much out of things to try. I tried deleting all records from my database, which lets the simulator think it can perform write operations, but my test app still gets PERMISSION_DENIED, so I don't know what's causing inconsistencies there.
Is my understanding of the predefined data variable correct? If so, why can't I write the rules I want? I have seen snippets literally trying to achieve my "create only, no-delete" rule that seem to line up with my understanding.
Last note: I am trying this in a totally new Firebase project with JUST the rules above, and only ~a few records of junk data laying around my database.
Because you have placed the !data.exists() at the root location of your database, data refers to the entire database. You will only be able to write to the database when it is completely empty.
You indicate that you run your tests with only a few records of junk data laying around my database. Those records will cause data.exists() to be true.
You can achieve your goal by placing the !data.exists() rule in your tree at the specific location where you want to require that no data already exists. This is typically done at a location with a wildcard key, as in the example you linked:
{
"rules": {
// default rules are false if not specified
"posts": {
".read": true, // everyone can read all posts
"$postId": {
// a new post can be created if it does not exist
// existing posts can only be edited by their original "author"
".write": "!data.exists() && newData.exists() || data.child('author').val() == auth.uid",
".validate": "newData.hasChildren(['title', 'author', 'timestamp'])",
}
}
}
}

Data fails to load due to permissions but only initially

What's happening is that usually the first time I authenticate a user after enough time has passed that the session has expired my requests for data that the user should have access to are denied. The requests will start working eventually with no actual rules or code changing. It can take from 2 to 10 minutes but will eventually right itself.
I don't have hard data on this; it's just something I've observed.
I have no idea what I can do about this. Is anyone else seeing this? Is this a known bug? I've search but haven't found any other accounts of this happening.
Thanks.
here are the rules:
{
"rules": {
"users": {
"$user": {
".read" :"$user == auth.id || data.child('email').val() == auth.email",
".write":"$user == auth.id"
}
},
"todos":{
"$list":{
".read":"data.child('members').hasChild(auth.id)",
".write":"newData.child('members').hasChild(auth.id)"
}
}
}
}
By default, your auth tokens live for 24 hours, so if you're seeing security errors before then, it's probably not related to your session. To verify this, you can attach a callback to tell you when your session has expired. See:
https://www.firebase.com/docs/javascript/firebase/auth.html
I suspect what's going on here is simply that the data your rules are depending on is changing and causing writes to succeed some times, and fail others. This is the intended behavior of Security Rules in Firebase. For example, if someone was added / removed from /todos/blah/members/ it would affect your ability to read at /todos/blah
One odd thing about your rules is that you appear to be allowing anyone to write to /todos/$list as long as the data they are writing contains a member list that contains the writer's user id. I suspect this is not your intended behavior. Perhaps you meant to make the write rules depend on the data that was already in Firebase rather than new data?

Resources