11

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"
    }
  }
}
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Dan Kanze
  • 18,485
  • 28
  • 81
  • 134

2 Answers2

24

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.

Kato
  • 40,352
  • 6
  • 119
  • 149
  • This was very thorough - thank you for taking the time to walk me through this. Very helpful. Quick question on `$id >= 'rec'+root.child('incid/counter').val()` where `$id` is equal to the `counter` value found in your fiddle. If a user deletes an `$id` say record `3` we may be left with `1,2,4,5` at which point those rules would fall through right? How would I deal with this? – Dan Kanze Mar 26 '14 at 13:46
  • Hi Dan, yes those are two different strategies, one is for creating unique incremental records, the other is for enforcing a max. It would take a bit of scheming to combine the two, which I didn't do here. – Kato Mar 26 '14 at 16:50
  • I'm trying to imagine how it would be even possible to enforce both if `$id` wasn't an integer that matches the current `counter` value. What if the `.write` is on an object property that muiltiple records share or if every `$id` was random? Do you have any ideas that don't require `$id` to increment with `counter`? – Dan Kanze Mar 26 '14 at 18:15
  • This is the only idea for a fully client-side approach that I conjure. You could always spin up a node process in about 20 lines of code to monitor the path, count records, enforce the max, etc. – Kato Mar 26 '14 at 19:51
  • Yea if the `$id` wants to break away from that pattern I think that's what I'll have to do. – Dan Kanze Mar 26 '14 at 19:58
  • @Kato Something like root.child('somePath').numChildren() would be a great addition to the security structure assuming it could be fast. I'm working on an app where the # of things the user is allowed to have depends on how much they've paid. I'm handling it in some protected server side code - but now I've got security/validation logic in a few spots vs keeping it centralized in the Firebase Security Rules area. – Mike Pugh Mar 27 '14 at 19:25
  • @Kato Coming back to this a bit later I'm wondering if `.numChildren()` ever made it into the security API or is in the works? – Dan Kanze Jan 27 '15 at 01:55
  • Getting children without loading all records is still a very non-trivial issue in distributed, real-time computing. There is a REST API call available (shallow=true) that achieves a similar result, but it still takes a small performance hit if there are millions of children in the path, and nothing available for the SDK. – Kato Jan 27 '15 at 03:16
  • Amazing answers as always @Kato. I was wondering if you could recommend an approach for having a max of 12 items in my path **.ref(`connections/${myUid}/connection`)** – Nikos Jul 11 '20 at 19:04
4

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

Gonzalo
  • 3,674
  • 2
  • 26
  • 28
  • Thanks Gonzalo. This is a reall strict and powerfull solution. It is because one can put the logic and restrict his/her users to limits on client side , however with Cloud Functions it is so easy to catch anyone who tries to bypass the rules that you've provided on client side. Check it out https://firebase.google.com/docs/functions/ – katmanco Aug 27 '17 at 15:54