Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Limit number of records that can be written to a path (reference other paths in security rules)

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"
    }
  }
}
like image 236
Dan Kanze Avatar asked Mar 25 '14 19:03

Dan Kanze


2 Answers

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.

like image 107
Kato Avatar answered Oct 31 '22 13:10

Kato


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

like image 25
Gonzalo Avatar answered Oct 31 '22 11:10

Gonzalo