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
Related
I have a problem restricting access to the children of the object
The rules I need:
roles - read
-- UID
--- SUPUSR
---- settings = read only
--- store = write and read
My rules
"roles":{
".read":"auth != null",
".write":"root.child('roles/SUPUSR/').child(auth.uid).child('settings').child('pri_enabled').val() == 1 || root.child('roles/USERS/').child(auth.uid).child('settings').child('pri_enabled').val() == 1",
"settings":{
".read":"auth != null",
".write":false
}
If I leave it the way it is above, it inherits the "roles" rules for writing
Firebase Realtime Database Rules cascade, once you grant permission, you cannot revoke it. So if you allow write access on /roles, anyone can write to any child of /roles whether it's their own or someone else's data.
Other notes:
The current rules affect /roles and /roles/settings, which is too high in the database tree, you should be setting the rules of /roles/SUPUSR/someUserId, /roles/SUPUSR/someUserId/settings and so on.
The use of auth != null seems out of place. Should any logged in user be able to read any other user's roles? Should this only work for super users?
Some of the data would also make sense to be validated.
{
"rules": {
"roles": {
"SUPUSR": {
"$uid": {
// any data under /roles/SUPUSR/$uid is readable to logged in users
".read": "auth != null",
"nome": {
// only this user can update nome, it also must be a string
".write": "auth.uid === $uid",
".validate": "newData.isString()"
},
"role": {
// only this user can update role, and it must be one of a select number of string values
".write": "auth.uid === $uid",
".validate": "newData.isString() && newData.val().matches(/^(R&S|Admin|etc)$/)"
},
"store": {
".write": "root.child('roles/SUPUSR/').child(auth.uid).child('settings').child('pri_enabled').val() == 1 || root.child('roles/USERS/').child(auth.uid).child('settings').child('pri_enabled').val() == 1"
}
// any other keys are ".write": false, by default, which includes "settings"
}
}, // end /rules/roles/SUPUSR
"USERS": {
"$uid": {
...
}
}, // end /rules/roles/USERS
...
}, // end /rules/roles
...
}
}
What should be the firebase rules for comment on post which is similar to facebook.
There are two things:
first, only authenticated user can comment.
Second, only the user who has commented can delete the comment. The user who has commented his id is saved in username.
I strongly suggest using Firebase Bolt for writing/compiling Firebase Database Security rules. Data structure can get big and complicated. Using Bolt language you'll be able to easily write complex access and structure rules that can be re-used for other db patterns.
Your rules would look something like this:
path /comment/{postUid}/{commentUid} is Comment {
read() { true }
write() { isAuthor(this) || isAuthor(prior(this)) }
}
type Comment {
text : String,
username : String
}
isAuthor(value) { auth != null && value.username == auth.uid }
Pay attention to isAuthor(prior(this)) call. This is the way to make sure only author can delete a comment. prior function returns data as it was saved before current event (create, update or delete).
After using firebase-bolt tool to compile rules to JSON format you'll get:
{
"rules": {
"comment": {
"$postUid": {
"$commentUid": {
".validate": "newData.hasChildren(['text', 'username'])",
"text": {
".validate": "newData.isString()"
},
"username": {
".validate": "newData.isString()"
},
"$other": {
".validate": "false"
},
".read": "true",
".write": "auth != null && newData.child('username').val() == auth.uid || auth != null && data.child('username').val() == auth.uid"
}
}
}
}
}
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.
(Please see revised question and comment if further clarity is necessary)
Given this data structure
{
"-KWz2G9JKtwqt5Kn-pL7":true
}
How can I access the first value ( "-KWz2G9JKtwqt5Kn-pL7" ) from within the Firebase Validation Rules when I try to validate the newData?
Below please find my original question.
Background
Samples online show that the best way to connect Firebase entities is using an index where one collects id's that relate one entity within a node of another entity.
for example;
"groups":{
...
"members":{
"userid-1":true,
"userid-2":true
}
}
I want to publish an index to a node below another entity (queues/queue/tasks) with the following data structure and to validate that data structure with the set of rules described below:
{
"-KWz2G9JKtwqt5Kn-pL7":true
}
The data structure is a reference to a task entity (/tasks/task/tid) that I want to associate with a node of a separate entity.
I'm planning on putting those values into a Dictionary and adding it via setValue. When I do this Firebase should apply a validation rule. I want to verify that the identifier is one that exists elsewhere in the database.
My Index Will Look Like This
(where the identifiers relate to a set of task id's stored in another location)
"queues":{
....
"K24395498054-p23"{
"tasks": {
"-KWz2G9JKtwqt5Kn-pL7":true,
"-KWjewrkstwqt7Ln-pL3":true,
"-KWjewgqjdsllfsn-pL5":true
}
}
}
But I'm unclear on how to unpack the first value of the newData server variable within the rules when in this case, I have no child identifier.
Instead of sending a singular value, I am sending a dictionary to Firebase.
newData in this case should equal:
{
"-KWz2G9JKtwqt5Kn-pL7":true
}
Nota Bene - An interesting side note on this; I'm trying to create an
index but reading the setValue documentation. It states that the value
would be overwritten. How then can I save the dictionary without
overwriting the existing values?
Rules:
{
"rules": {
".read": "auth != null",
"queues": {
".write":"auth.provider != 'anonymous'",
"$qid": {
"members" : {
//unsure how to access the first value of the newData object without it having a label but the following shows what I am trying to accomplish
".validate":"root.child('tasks').hasChild('-KWz2G9JKtwqt5Kn-pL7')",
}
}
}
}
}
In this case I am confirming that the newData being added is a valid unique identifier that already exists as a task. In this rule the task will be associated with the queue entity.
If it helps please see my entity map below:
Alternatively
How can I create an index similar to what I am seeing online in your samples?:
To represent a set of groups that I might associate with a user....
"user_0" : {
....
"groups" : {
"group_id0":true,
"group_id1":true,
"group_id2":true
}
}
Following that, how can I validate that a group_id# actually exists under my group node elsewhere?
If I can get that answer, I may be able to extrapolate for my needs.
Update
Based on the accepted answer, I have been able to successfully apply the following rules to achieve the desired outcomes:
{
"rules": {
".read": "false",
".write": "(auth.provider != 'anonymous') && (auth != null)",
"presence": {
".read": "(auth.provider != 'anonymous') && (auth != null)",
".write": "(auth.provider != 'anonymous') && (auth != null)",
},
"queues": {
".read":"(auth != null)",
".write": "(auth.provider != 'anonymous') && (auth != null)",
"$qid": {
"tasks": {
"$taskid": {
".validate": "root.child('tasks').child($taskid).exists()"
}
}
}
},
"tasks": {
".read": "(auth != null)",
".write": "(auth.provider != 'anonymous') && (auth != null)",
"$taskid":{
"queues":{
"$qid": {
".validate":"root.child('queues').child($qid).exists()"
}
}
}
},
"users": {
".read": "(auth != null)",
".write": "(auth != null)",
"$userid":{
"groups":{
"$gid": {
".validate":"root.child('groups').child($gid).exists()"
}
},
"roles":{
"$rid": {
".validate":"root.child('roles').child($rid).exists()"
}
},
"metadata":{
".read": "(auth != null)",
".write": "(auth.provider != 'anonymous') && (auth != null)",
}
}
},
"roles": {
".read": "(auth != null)",
".write": "(auth != null)",
},
"groups":{
".read": "(auth != null)",
".write": "(auth != null)",
},
} //eof-rules
}
You need to add another level to your rules so that you can use a $ variable for the task ID:
{
"rules": {
".read": "auth != null",
"queues": {
".write": "auth.provider != 'anonymous'",
"$qid": {
"members": {
"$taskid": {
".validate": "root.child('tasks').hasChild($taskid)"
}
}
}
}
}
}
You can then use either set or update to add the task ID to the index:
firebase
.database()
.ref("queues/K24395498054-p23/members/-KWz2G9JKtwqt5Kn-pL7")
.set(true);
firebase
.database()
.ref("queues/K24395498054-p23/members")
.update({ "-KWz2G9JKtwqt5Kn-pL7": true });
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.