After implementing your Realtime Database, you’re going to want to set up some security so your users’ data is in safe hands.
It took me a long time to get my head around writing these rules. There’s nothing wrong with the official docs on the subject, but the details span over so many pages. Let’s make this a bit easier, shall we?
Before that though, I can’t recommend enough that you get this book whether you’re starting out or looking to dive deeper into the services of Firebase.
By the way, out of topic as it may be, I can’t help but to recommend this book, The Definitive Guide to Firebase by Laurence Moroney. It’s a strong pickup if you want to learn or polish your skills.
The 4 Basic Rules
I’m just going to rip this one off of the official docs.
Rule Types | |
---|---|
.read | Describes if and when data is allowed to be read by users. |
.write | Describes if and when data is allowed to be written. |
.validate | Defines what a correctly formatted value will look like, whether it has child attributes, and the data type. |
.indexOn | Specifies a child to index to support ordering and querying. |
Now that you know that, let’s move on to how to properly implement these rules.
Read and Write Rules Ins and Outs
I’ll be using this simple data to show you how read and write rules are implemented.
The Basics
{ "rules": { "one": { ".read": true, ".write": true } }
Setting a directory’s read or write to true will allow the user access to that location. False will do otherwise. The above code gives read and write access to one and all its children.
Read/Write Rules Access CANNOT be Revoked
{ "rules": { "one": { ".read": true, ".write": false, "two": { ".write": true } } } }
What catches out many is once you give read/write access to a parent node, setting read/write access to false on children node will do nothing and data can still be read/written there.
If you want children specific rules to be set, you either have to set access on the parent node to false so that it can be set true by its children, or just don’t declare the rule in the parent at all.
In the example above, write access to one is set to false, but it is set to true in two. Data can be written only within the two directory.
Built-in Variables
Predefined Variables | |
---|---|
now | The current time in milliseconds since Linux epoch. This works particularly well for validating timestamps created with the SDK’s firebase.database.ServerValue.TIMESTAMP. |
root | A RuleDataSnapshot representing the root path in the Firebase database as it exists before the attempted operation. |
newData | A RuleDataSnapshot representing the data as it would exist after the attempted operation. It includes the new data being written and existing data. |
data | A RuleDataSnapshot representing the data as it existed before the attempted operation. |
$ variables | A wildcard path used to represent ids and dynamic child keys. |
auth | Represents an authenticated user’s token payload. |
Again, I’m taking this from the official docs. You can access these variables in the rules to make logical operations for a more dynamic solution than true or false.
Using these Variables for Logical Operations
{ "rules": { ".read": "auth != null", ".write": "auth != null" } }
These are the default rules which only allow access if the user is authenticated.
Wildcards
{ "rules": { "users": { "$uid": { ".read": "$uid === auth.uid" } "montysUID": { ".read": "auth.uid == 'montysUID'", ".write": "auth.uid == 'montysUID'" } } } }
These variables can also be used as directories. In this case, the user can only write data in his/her own directory by matching the uid acquired by the auth to the uid represented by the path.
Any explicitly specified nodes will also override wildcard rules. In this case, most users would only be given read access to their own data but Monty is given read and write access to his data.
Click here if you want to see the full list of variables and logical operators for these security rules.
The Validate Rule Checks Incoming Data
{ "rules": { "foo": { ".validate": "newData.isString() && newData.val().length < 100" } } }
When there is incoming data, both the write and validate rules are checked. The key difference is that validate has access to the variable newData, or quite obviously, the incoming data. This means that you can pass incoming data through a set of rules and checks.
The above rule checks that the incoming data is a string of length less than 100.
IndexOn is for Sorting and Performance
The index rule is a special one that serves functions other than allowing or denying access. It has 2 purposes
- Improving the performance of your queries
- Optimising the sorting of your queries
You might be able to get away not specifying any indexes during development, but leaving this way will cause performance to degrade as your queries get larger.
As for sorting, you could do it either by orderByChild or orderByValue. Generally, you want to use orderByValue if the items you’re querying don’t have children.
OrderByChild
{ "rules": { "dinosaurs": { ".indexOn": ["height", "length"] } } }
Self-explanatory as it may be, the data returned by a query on dinosaurs will be sorted by height, followed by length.
OrderByValue
{ "rules": { "scores": { ".indexOn": ".value" } } }
If your items don’t have children then using orderByValue is a better option. In this case, a simple .value will do the trick to sort data by their value instead of their key.