Limit number of records that can be written to a path (reference other paths in security rules)
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