I launched my first open repository project, EphChat, and people promptly started flooding it with requests.
Does Firebase have a way to rate limit requests in the security rules? I assume there's a way to do it using the time of the request and the time of previously written data, but can't find anything in the documentation about how I would do this.
The current security rules are as follows.
{
"rules": {
"rooms": {
"$RoomId": {
"connections": {
".read": true,
".write": "auth.username == newData.child('FBUserId').val()"
},
"messages": {
"$any": {
".write": "!newData.exists() || root.child('rooms').child(newData.child('RoomId').val()).child('connections').hasChild(newData.child('FBUserId').val())",
".validate": "newData.hasChildren(['RoomId','FBUserId','userName','userId','message']) && newData.child('message').val().length >= 1",
".read": "root.child('rooms').child(data.child('RoomId').val()).child('connections').hasChild(data.child('FBUserId').val())"
}
},
"poll": {
".write": "auth.username == newData.child('FBUserId').val()",
".read": true
}
}
}
}
}
I would want to rate-limit writes (and reads?) to the db for the entire Rooms object, so only 1 request can be made per second (for example).
The trick is to keep an audit of the last time a user posted a message. Then you can enforce the time each message is posted based on the audit value:
{
"rules": {
// this stores the last message I sent so I can throttle them by timestamp
"last_message": {
"$user": {
// timestamp can't be deleted or I could just recreate it to bypass our throttle
".write": "newData.exists() && auth.uid === $user",
// the new value must be at least 5000 milliseconds after the last (no more than one message every five seconds)
// the new value must be before now (it will be since `now` is when it reaches the server unless I try to cheat)
".validate": "newData.isNumber() && newData.val() === now && (!data.exists() || newData.val() > data.val()+5000)"
}
},
"messages": {
"$message_id": {
// message must have a timestamp attribute and a sender attribute
".write": "newData.hasChildren(['timestamp', 'sender', 'message'])",
"sender": {
".validate": "newData.val() === auth.uid"
},
"timestamp": {
// in order to write a message, I must first make an entry in timestamp_index
// additionally, that message must be within 500ms of now, which means I can't
// just re-use the same one over and over, thus, we've effectively required messages
// to be 5 seconds apart
".validate": "newData.val() >= now - 500 && newData.val() === data.parent().parent().parent().child('last_message/'+auth.uid).val()"
},
"message": {
".validate": "newData.isString() && newData.val().length < 500"
},
"$other": {
".validate": false
}
}
}
}
}
See it in action in this fiddle. Here's the gist of what's in the fiddle:
var fb = new Firebase(URL);
var userId; // log in and store user.uid here
// run our create routine
createRecord(data, function (recordId, timestamp) {
console.log('created record ' + recordId + ' at time ' + new Date(timestamp));
});
// updates the last_message/ path and returns the current timestamp
function getTimestamp(next) {
var ref = fb.child('last_message/' + userId);
ref.set(Firebase.ServerValue.TIMESTAMP, function (err) {
if (err) { console.error(err); }
else {
ref.once('value', function (snap) {
next(snap.val());
});
}
});
}
function createRecord(data, next) {
getTimestamp(function (timestamp) {
// add the new timestamp to the record data
var data = {
sender: userId,
timestamp: timestamp,
message: 'hello world'
};
var ref = fb.child('messages').push(data, function (err) {
if (err) { console.error(err); }
else {
next(ref.name(), timestamp);
}
});
})
}
I don't have enough reputations to write in the comment, but I agree to Victor's comment. If you insert the fb.child('messages').push(...) into a loop (i.e. for (let i = 0; i < 100; i++) {...} ) then it would successfully push 60-80 meessages ( in that 500ms window frame.
Inspired by Kato's solution, I propose a modification to the rules as follow:
rules: {
users: {
"$uid": {
"timestamp": { // similar to Kato's answer
".write": "auth.uid === $uid && newData.exists()"
,".read": "auth.uid === $uid"
,".validate": "newData.hasChildren(['time', 'key'])"
,"time": {
".validate": "newData.isNumber() && newData.val() === now && (!data.exists() || newData.val() > data.val() + 1000)"
}
,"key": {
}
}
,"messages": {
"$key": { /// this key has to be the same is the key in timestamp (checked by .validate)
".write": "auth.uid === $uid && !data.exists()" ///only 'create' allow
,".validate": "newData.hasChildren(['message']) && $key === root.child('/users/' + $uid + '/timestamp/key').val()"
,"message": { ".validate": "newData.isString()" }
/// ...and any other datas such as 'time', 'to'....
}
}
}
}
}
The .js code is quite similar to Kato's solution, except that the getTimestamp would return {time: number, key: string} to the next callback. Then we would just have to ref.update({[key]: data})
This solution avoids the 500ms time-window, we don't have to worry that the client must be fast enough to push the message within 500ms. If multiple write requests are sent (spamming), they can only write into 1 single key in the messages. Optionally, the create-only rule in messages prevents that from happening.
I liked Kato's answer but it doesn't take into account a malicious user flooding the chat between the 500ms window simply using a for loop.
I propose this variant that eliminates the possibility:
{
"rules": {
"users": {
"$uid": {
"rateLimit": {
"lastMessage": {
// newData.exists() ensures newData is not null and prevents deleting node
// and $uid === auth.uid ensures the user writing this child node is the owner
".write": "newData.exists() && $uid === auth.uid",
// newData.val() === now ensures the value written is the current timestamp
// to avoid tricking the rules writting false values
// and (!data.exists() || newData.val() > data.val() + 5000)
// ensures no data exists currently in the node. Otherwise it checks if the
// data that will overwrite the node is a value higher than the current timestamp
// plus the value that will rate limit our messages expressed in milliseconds.
// In this case a value of 5000 means that we can only send a message if
// the last message we sent was more than 5 seconds ago
".validate": "newData.val() === now && (!data.exists() || newData.val() > data.val() + 5000)"
}
}
}
},
"messages": {
"$messageId": {
// This rule ensures that we write lastMessage node avoiding just sending the message without
// registering a new timestamp
".write": "newData.parent().parent().child('users').child(auth.uid).child('rateLimit').child('lastMessage').val() === now",
// This rule ensures that we have all the required message fields
".validate": "newData.hasChildren(['timestamp', 'uid', 'message'])",
"uid": {
// This rule ensures that the value written is the id of the message sender
".validate": "newData.val() === auth.uid"
},
"timestamp": {
// This rule ensures that the message timestamp can't be modified
".write": "!data.exists()",
// This rule ensures that the value written is the current timestamp
".validate": "newData.val() === now"
},
"message": {
// This rule ensures that the value written is a string
".validate": "newData.isString()"
},
"$other": {
// This rule ensures that we cant write other fields in the message other than
// the explicitly declared above
".validate": false
}
}
}
}
}
The code implementation uses atomic writes across multiple locations. If one validation fails, the operation doesn't complete and no operation is done in the database
function sendMessage(message) {
const database = firebase.database();
const pushId = database.ref().child("messages").push().key;
const userId = firebase.auth().currentUser.uid;
const timestampPlaceholder = firebase.database.ServerValue.TIMESTAMP;
let updates = {};
updates["messages/" + pushId] = {
uid: userId,
timestamp: timestampPlaceholder,
message: message,
};
updates[`users/${userId}/rateLimit/lastMessage`] = timestampPlaceholder;
database.ref().update(updates);
}
The existing answers use two database updates: (1) mark a timestamp, and (2) attach the marked timestamp to the actual write. Kato's answer requires 500ms time-window, while ChiNhan's requires remembering the next key.
There is a simpler way to do it in a single database update. The idea is to write multiple values to the database at once using the update() method. The security rules validates the written values so that the write does not exceed the quota. The quota is defined as a pair of values: quotaTimestamp and postCount. The postCount is the number of posts written within 1 minute of the quotaTimestamp. The security rules simply rejects the next write if the postCount exceeds a certain value. The postCount is reset when the quotaTimestamp is staler then 1 minute.
Here is how to post a new message:
function postMessage(user, message) {
const now = Date.now() + serverTimeOffset;
if (!user.quotaTimestamp || user.quotaTimestamp + 60 * 1000 < now) {
// Resets the quota when 1 minute has elapsed since the quotaTimestamp.
user.quotaTimestamp = database.ServerValue.TIMESTAMP;
user.postCount = 0;
}
user.postCount++;
const values = {};
const messageId = // generate unique id
values[`users/${user.uid}/quotaTimestamp`] = user.quotaTimestamp;
values[`users/${user.uid}/postCount`] = user.postCount;
values[`messages/${messageId}`] = {
sender: ...,
message: ...,
...
};
return this.db.database.ref().update(values);
}
The security rules to rate limit to at most 5 posts per minute:
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid && newData.child('postCount').val() <= 5",
"quotaTimestamp": {
// Only allow updating quotaTimestamp if it's staler than 1 minute.
".validate": "
newData.isNumber()
&& (newData.val() === now
? (data.val() + 60 * 1000 < now)
: (data.val() == newData.val()))"
},
"postCount": {
// Only allow postCount to be incremented by 1
// or reset to 1 when the quotaTimestamp is being refreshed.
".validate": "
newData.isNumber()
&& (data.exists()
? (data.val() + 1 === newData.val()
|| (newData.val() === 1
&& newData.parent().child('quotaTimestamp').val() === now))
: (newData.val() === 1))"
},
"$other": { ".validate": false }
}
},
"messages": {
...
}
}
}
Note: the serverTimeOffset should be maintained to avoid clock skew.
Related
here is my rules code, the code updates in unity every time the user gets +1 point. the rules need
to make it that points can only be updated by +1 (basically , currentPoints = newPoints +1).
in unity i would write it as:
if (currentPoints < newPoints) currentPoints++;
now how do i state my rules to stick to that so my code cant be hacked/edited by a hacker to give
them +1000 points at a time instead of +1 on the database.
{
"rules": {
"users": {
".read": "auth != null",
"$userId": {
".write": "$userId === auth.uid",
"points": {
".validate": "newData.isNumber()",
".write": "data.val() == null && newData.val() == 1 || newData.val() === data.val() + 1"},
"username": {
".validate": "newData.isString()"}
}
}
}
}
Looking at your current rules, you get caught out by cascading security rules:
{
"rules": {
"users": {
// any logged in user can read another user's data
".read": "auth != null",
"$userId": {
// a user can modify any of their own data
".write": "$userId === auth.uid",
"points": {
// asserts points is a number
".validate": "newData.isNumber()",
// this next rule is ignored when $userId === auth.uid - read/write rules cascade!
// if $userId !== auth.uid, ANYONE can write to this location as long as points is first set to 1 or is increased by 1
".write": "data.val() == null && newData.val() == 1 || newData.val() === data.val() + 1"
},
"username": {
// asserts username is a string
".validate": "newData.isString()"
}
}
}
}
}
With your current rules, as long as I am logged in, I can get the value of /users/someUserId/points, increase it by 1, and then set the new value to /users/someUserId/points. Arguably, the only person who should be able to increase a user's points is the user who owns that score.
To fix this, you need to remove the ".write" rule (which causes this bug) and move it's logic into ".validate":
{
"rules": {
"users": {
// any logged in user can read another user's data
".read": "auth != null",
"$userId": {
// a user can modify any of their own data
".write": "$userId === auth.uid",
"points": {
// asserts points is a number and is correctly increased by 1
".validate": "newData.isNumber() && ((data.val() == null && newData.val() == 1) || newData.val() === data.val() + 1)",
},
"username": {
// asserts username is a string
".validate": "newData.isString()"
}
}
}
}
}
Note: With the ".read": "auth != null" rule for /users, make sure that any private user data, e.g. emails, phone numbers, etc are not under /users - they should be moved into a different tree called /privateUserData (or similar).
Let's say my Firebase collection looks like:
{
"max":5
"things":{}
}
How would I use the value of max in my security rules to limit the number of things?
{
"rules": {
"things": {
".validate": "newData.val().length <= max"
}
}
}
Using existing properties is done using root or parent and is pretty straightforward.
{
"rules": {
"things": {
// assuming value is being stored as an integer
".validate": "newData.val() <= root.child('max')"
}
}
}
However, determining the number of records and enforcing this is a bit more complex than simply writing a security rule:
since there is no .length on an object, we need to store how many records exist
we need to update that number in a secure/real-time way
we need to know the number of the record we are adding relative to that counter
A Naive Approach
One poor-man's approach, assuming the limit is something small (e.g. 5 records), would be to simply enumerate them in the security rules:
{
"rules": {
"things": {
".write": "newData.hasChildren()", // is an object
"thing1": { ".validate": true },
"thing2": { ".validate": true },
"thing3": { ".validate": true },
"thing4": { ".validate": true },
"thing5": { ".validate": true },
"$other": { ".validate": false
}
}
}
A Real Example
A data structure like this works:
/max/<number>
/things_counter/<number>
/things/$record_id/{...data...}
Thus, each time a record is added, the counter must be incremented.
var fb = new Firebase(URL);
fb.child('thing_counter').transaction(function(curr) {
// security rules will fail this if it exceeds max
// we could also compare to max here and return undefined to cancel the trxn
return (curr||0)+1;
}, function(err, success, snap) {
// if the counter updates successfully, then write the record
if( err ) { throw err; }
else if( success ) {
var ref = fb.child('things').push({hello: 'world'}, function(err) {
if( err ) { throw err; }
console.log('created '+ref.name());
});
}
});
And each time a record is removed, the counter must be decremented.
var recordId = 'thing123';
var fb = new Firebase(URL);
fb.child('thing_counter').transaction(function(curr) {
if( curr === 0 ) { return undefined; } // cancel if no records exist
return (curr||0)-1;
}, function(err, success, snap) {
// if the counter updates successfully, then write the record
if( err ) { throw err; }
else if( success ) {
var ref = fb.child('things/'+recordId).remove(function(err) {
if( err ) { throw err; }
console.log('removed '+recordId);
});
}
});
Now on to the security rules:
{
"rules": {
"max": { ".write": false },
"thing_counter": {
".write": "newData.exists()", // no deletes
".validate": "newData.isNumber() && newData.val() >= 0 && newData.val() <= root.child('max').val()"
},
"things": {
".write": "root.child('thing_counter').val() < root.child('max').val()"
}
}
}
Note that this doesn't force a user to write to thing_counter before updating a record, so while suitable for limiting the number of records, it's not suitable for enforcing game rules or preventing cheats.
Other Resources and Thoughts
If you want game level security, check out this fiddle, which details how to create records with incremental ids, including security rules needed to enforce a counter. You could combine that with the rules above to enforce a max on the incremental ids and ensure the counter is updated before the record is written.
Also, make sure you're not over-thinking this and there is a legitimate use case for limiting the number of records, rather than just to satisfy a healthy dose of worry. This is a lot of complexity to simply enforce a poor man's quota on your data structures.
While I think there is still no available rule to do such thing, there is a sample cloud function available here that does that:
https://github.com/firebase/functions-samples/tree/master/limit-children
How to set the database rules with "allow to update but not allow to add new record"?
I tried to simulate below but doesn't work..
My Data:
{
users:{
-randomID001:{
email:user#email.com,
status:active
},
-randomID002:{
email:user2#email.com,
status:inactive
}
}
}
Simulate in the console:
{
"rules": {
".read": "auth != null",
"users":{
".indexOn": "email",
".write":"!newData.exists() && data.exists()"
}
}
}
This line of code show mean for allow to write if new record doesn't exist and existing record exist ??
".write":"!newData.exists() && data.exists()"
Below is the test data i submit:
{
"randomID001":{"status":"inactive"}
}
I got the below error:
Simulated update denied
Please advise.
You can use validate to check if email already exists in the database this will allow only update on the existing users.
"users":{
"$userId":{
".write":"auth!=null",
".validate":"newData.child('email').val()===data.child('email').val()"
}
Firebase Realtime Database Simulator allows you to test [READ, SET, UPDATE]
It looks like dealing with firebase object.
FIREBASE DB MODEL - here is yours
{
users:{
-randomID001:{
email:user#email.com,
status:active
},
-randomID002:{
email:user2#email.com,
status:inactive
}
}
}
Try with different Locations
/users/randomID001 // exists in your DB
/users/randomID002 // exists in your DB
/users/randomID003 // not exists in your DB
CREATE
// newData(json) exists and no randomID001,002 data => FALSE
// newData(json) exists and no randomID003 data => TRUE
".write": "newData.exists() && !data.exists()"
UPDATE
// newData(json) exists and target(randomID001,002) data exists => TRUE
// newData(json) exists and target(randomID003) data exists => FALSE
".write": "newData.exists() && data.exists()"
DELETE
//You are sending only HTTP DELETE SIGNAL, NOT JSON
//Target(randomID001,002) data exists => TRUE
//Target(randomID003) data exists => FALSE
".write":"!newData.exists() && data.exists()"
next example is allowing [create,update]
You can think like "CREATE || UPDATE || DELETE".
{
"rules": {
"users": {
"$uid":{
".write": "(newData.exists() && !data.exists()) || (newData.exists() && data.exists())"
}
}
}
}
Also validation rule is good idea to be secured.
VALIDATION
...
items: {
"$itemId": {
".validate": "newData.hasChildren(['name', 'message', 'timestamp'])",
"name": {
".validate": "newData.val().length > 0 && newData.val().length < 20"
},
"message": {
".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 50"
},
"timestamp": {
".validate": "newData.isNumber() && newData.val() == now"
}
}
...
For your code maintenance, Firebase bolt is good choice.
https://github.com/firebase/bolt/blob/master/docs/language.md
I launched my first open repository project, EphChat, and people promptly started flooding it with requests.
Does Firebase have a way to rate limit requests in the security rules? I assume there's a way to do it using the time of the request and the time of previously written data, but can't find anything in the documentation about how I would do this.
The current security rules are as follows.
{
"rules": {
"rooms": {
"$RoomId": {
"connections": {
".read": true,
".write": "auth.username == newData.child('FBUserId').val()"
},
"messages": {
"$any": {
".write": "!newData.exists() || root.child('rooms').child(newData.child('RoomId').val()).child('connections').hasChild(newData.child('FBUserId').val())",
".validate": "newData.hasChildren(['RoomId','FBUserId','userName','userId','message']) && newData.child('message').val().length >= 1",
".read": "root.child('rooms').child(data.child('RoomId').val()).child('connections').hasChild(data.child('FBUserId').val())"
}
},
"poll": {
".write": "auth.username == newData.child('FBUserId').val()",
".read": true
}
}
}
}
}
I would want to rate-limit writes (and reads?) to the db for the entire Rooms object, so only 1 request can be made per second (for example).
The trick is to keep an audit of the last time a user posted a message. Then you can enforce the time each message is posted based on the audit value:
{
"rules": {
// this stores the last message I sent so I can throttle them by timestamp
"last_message": {
"$user": {
// timestamp can't be deleted or I could just recreate it to bypass our throttle
".write": "newData.exists() && auth.uid === $user",
// the new value must be at least 5000 milliseconds after the last (no more than one message every five seconds)
// the new value must be before now (it will be since `now` is when it reaches the server unless I try to cheat)
".validate": "newData.isNumber() && newData.val() === now && (!data.exists() || newData.val() > data.val()+5000)"
}
},
"messages": {
"$message_id": {
// message must have a timestamp attribute and a sender attribute
".write": "newData.hasChildren(['timestamp', 'sender', 'message'])",
"sender": {
".validate": "newData.val() === auth.uid"
},
"timestamp": {
// in order to write a message, I must first make an entry in timestamp_index
// additionally, that message must be within 500ms of now, which means I can't
// just re-use the same one over and over, thus, we've effectively required messages
// to be 5 seconds apart
".validate": "newData.val() >= now - 500 && newData.val() === data.parent().parent().parent().child('last_message/'+auth.uid).val()"
},
"message": {
".validate": "newData.isString() && newData.val().length < 500"
},
"$other": {
".validate": false
}
}
}
}
}
See it in action in this fiddle. Here's the gist of what's in the fiddle:
var fb = new Firebase(URL);
var userId; // log in and store user.uid here
// run our create routine
createRecord(data, function (recordId, timestamp) {
console.log('created record ' + recordId + ' at time ' + new Date(timestamp));
});
// updates the last_message/ path and returns the current timestamp
function getTimestamp(next) {
var ref = fb.child('last_message/' + userId);
ref.set(Firebase.ServerValue.TIMESTAMP, function (err) {
if (err) { console.error(err); }
else {
ref.once('value', function (snap) {
next(snap.val());
});
}
});
}
function createRecord(data, next) {
getTimestamp(function (timestamp) {
// add the new timestamp to the record data
var data = {
sender: userId,
timestamp: timestamp,
message: 'hello world'
};
var ref = fb.child('messages').push(data, function (err) {
if (err) { console.error(err); }
else {
next(ref.name(), timestamp);
}
});
})
}
I don't have enough reputations to write in the comment, but I agree to Victor's comment. If you insert the fb.child('messages').push(...) into a loop (i.e. for (let i = 0; i < 100; i++) {...} ) then it would successfully push 60-80 meessages ( in that 500ms window frame.
Inspired by Kato's solution, I propose a modification to the rules as follow:
rules: {
users: {
"$uid": {
"timestamp": { // similar to Kato's answer
".write": "auth.uid === $uid && newData.exists()"
,".read": "auth.uid === $uid"
,".validate": "newData.hasChildren(['time', 'key'])"
,"time": {
".validate": "newData.isNumber() && newData.val() === now && (!data.exists() || newData.val() > data.val() + 1000)"
}
,"key": {
}
}
,"messages": {
"$key": { /// this key has to be the same is the key in timestamp (checked by .validate)
".write": "auth.uid === $uid && !data.exists()" ///only 'create' allow
,".validate": "newData.hasChildren(['message']) && $key === root.child('/users/' + $uid + '/timestamp/key').val()"
,"message": { ".validate": "newData.isString()" }
/// ...and any other datas such as 'time', 'to'....
}
}
}
}
}
The .js code is quite similar to Kato's solution, except that the getTimestamp would return {time: number, key: string} to the next callback. Then we would just have to ref.update({[key]: data})
This solution avoids the 500ms time-window, we don't have to worry that the client must be fast enough to push the message within 500ms. If multiple write requests are sent (spamming), they can only write into 1 single key in the messages. Optionally, the create-only rule in messages prevents that from happening.
I liked Kato's answer but it doesn't take into account a malicious user flooding the chat between the 500ms window simply using a for loop.
I propose this variant that eliminates the possibility:
{
"rules": {
"users": {
"$uid": {
"rateLimit": {
"lastMessage": {
// newData.exists() ensures newData is not null and prevents deleting node
// and $uid === auth.uid ensures the user writing this child node is the owner
".write": "newData.exists() && $uid === auth.uid",
// newData.val() === now ensures the value written is the current timestamp
// to avoid tricking the rules writting false values
// and (!data.exists() || newData.val() > data.val() + 5000)
// ensures no data exists currently in the node. Otherwise it checks if the
// data that will overwrite the node is a value higher than the current timestamp
// plus the value that will rate limit our messages expressed in milliseconds.
// In this case a value of 5000 means that we can only send a message if
// the last message we sent was more than 5 seconds ago
".validate": "newData.val() === now && (!data.exists() || newData.val() > data.val() + 5000)"
}
}
}
},
"messages": {
"$messageId": {
// This rule ensures that we write lastMessage node avoiding just sending the message without
// registering a new timestamp
".write": "newData.parent().parent().child('users').child(auth.uid).child('rateLimit').child('lastMessage').val() === now",
// This rule ensures that we have all the required message fields
".validate": "newData.hasChildren(['timestamp', 'uid', 'message'])",
"uid": {
// This rule ensures that the value written is the id of the message sender
".validate": "newData.val() === auth.uid"
},
"timestamp": {
// This rule ensures that the message timestamp can't be modified
".write": "!data.exists()",
// This rule ensures that the value written is the current timestamp
".validate": "newData.val() === now"
},
"message": {
// This rule ensures that the value written is a string
".validate": "newData.isString()"
},
"$other": {
// This rule ensures that we cant write other fields in the message other than
// the explicitly declared above
".validate": false
}
}
}
}
}
The code implementation uses atomic writes across multiple locations. If one validation fails, the operation doesn't complete and no operation is done in the database
function sendMessage(message) {
const database = firebase.database();
const pushId = database.ref().child("messages").push().key;
const userId = firebase.auth().currentUser.uid;
const timestampPlaceholder = firebase.database.ServerValue.TIMESTAMP;
let updates = {};
updates["messages/" + pushId] = {
uid: userId,
timestamp: timestampPlaceholder,
message: message,
};
updates[`users/${userId}/rateLimit/lastMessage`] = timestampPlaceholder;
database.ref().update(updates);
}
The existing answers use two database updates: (1) mark a timestamp, and (2) attach the marked timestamp to the actual write. Kato's answer requires 500ms time-window, while ChiNhan's requires remembering the next key.
There is a simpler way to do it in a single database update. The idea is to write multiple values to the database at once using the update() method. The security rules validates the written values so that the write does not exceed the quota. The quota is defined as a pair of values: quotaTimestamp and postCount. The postCount is the number of posts written within 1 minute of the quotaTimestamp. The security rules simply rejects the next write if the postCount exceeds a certain value. The postCount is reset when the quotaTimestamp is staler then 1 minute.
Here is how to post a new message:
function postMessage(user, message) {
const now = Date.now() + serverTimeOffset;
if (!user.quotaTimestamp || user.quotaTimestamp + 60 * 1000 < now) {
// Resets the quota when 1 minute has elapsed since the quotaTimestamp.
user.quotaTimestamp = database.ServerValue.TIMESTAMP;
user.postCount = 0;
}
user.postCount++;
const values = {};
const messageId = // generate unique id
values[`users/${user.uid}/quotaTimestamp`] = user.quotaTimestamp;
values[`users/${user.uid}/postCount`] = user.postCount;
values[`messages/${messageId}`] = {
sender: ...,
message: ...,
...
};
return this.db.database.ref().update(values);
}
The security rules to rate limit to at most 5 posts per minute:
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid && newData.child('postCount').val() <= 5",
"quotaTimestamp": {
// Only allow updating quotaTimestamp if it's staler than 1 minute.
".validate": "
newData.isNumber()
&& (newData.val() === now
? (data.val() + 60 * 1000 < now)
: (data.val() == newData.val()))"
},
"postCount": {
// Only allow postCount to be incremented by 1
// or reset to 1 when the quotaTimestamp is being refreshed.
".validate": "
newData.isNumber()
&& (data.exists()
? (data.val() + 1 === newData.val()
|| (newData.val() === 1
&& newData.parent().child('quotaTimestamp').val() === now))
: (newData.val() === 1))"
},
"$other": { ".validate": false }
}
},
"messages": {
...
}
}
}
Note: the serverTimeOffset should be maintained to avoid clock skew.
Documentation: https://www.firebase.com/docs/javascript/firebase/setpriority.html
Isn't there missing something important?
I am trying to update the priority of the group by some auth user. And i am able to do it only when i am authenticated as an owner of the group and i have no idea why and HOW TO GRANT ACCESS ONLY FOR UPDATING THE PRIORITY TO THE ALL AUTH USERS AS I DID WITH "membersCount".
// GROUPS
"groups": {
"$subCategoryId": {
"$groupId": {
// ONLY OWNER OF GROUP CAN UPDATE THE userId, name, hashTag, categoryId
//
// The first init has to have userId, name, categoryId
//
// Anyone can update the membersCount
".write": "newData.hasChildren(['userId','name','categoryId']) || !data.exists() || auth != null",
"userId": {
".validate": "auth.uid == newData.val()"
},
"name": {
".validate": "newData.val() != '' && newData.val().length < 30 && (data.parent().child('userId').val() == auth.uid || !data.parent().child('userId').exists())"
},
"hashTag": {
".validate": "(data.parent().child('userId').val() == auth.uid || !data.parent().child('userId').exists())"
},
"categoryId": {
".validate": "newData.val() != '' && root.child('categories').child(newData.val()).exists() && root.child('subcategories').child(newData.val()).child($subCategoryId).exists() && (data.parent().child('userId').val() == auth.uid || !data.parent().child('userId').exists())"
},
"membersCount": {
".write": "auth != null && newData.isNumber() && (data.val()+1 == newData.val() || data.val()-1 == newData.val() )"
}
}
}
},
When i tried update({ '.priority': 2}) i got:
Uncaught Error: update() does not currently support updating .priority.
The error message has nothing to do with security rules in this case. You can't update a priority using the update() command. You can do it by using setPriority(2).
If you want to reference priorities in your security rules, you can do that with getPriority.