Very dynamic rules

I want to start off, by saying that we do not suggest that you have to do this, but that it is possible. Continue…

In Flamelink, users are assigned to Permission Groups, each with a unique ID. These permission groups map to certain permissions in the app. A permission group could, for instance, be configured to allow only “view” access for schemas, but full CRUD access for content. We can make use of these permission groups to dynamically restrict access on the database level.

Bare with me, this might get nasty. We’ll first look at how we can enforce “view” permissions on your content types, but the same technique can be used for any of the other CRUD actions.

{

"rules": {

"flamelink": {

".read": "auth != null",

".write": "auth != null",

"environments": {

"$environment": {

"content": {

"$contentType": {

"$locale": {

".read": "auth != null && root.child('flamelink').child('permissions').child(root.child('flamelink').child('users').child(auth.uid).child('permissions').val() + '').child('content').child($environment).child($contentType).child('view').val() === true"

}

}

}

}

}

}

}

}

Wow! What the heck?! Okay, let’s break that down because the idea is simple, the syntax not so much. I promise it will make sense.

The idea: Get the user’s permission group and check if that permission group is set up to allow “view” permissions for the particular content.

The syntax: The rule is made up of two parts: getting the permission group ID and then checking the permission configuration for that group.

root

.child('flamelink')

.child('users')

.child(auth.uid)

.child('permissions')

.val() + ''

This code starts at the root of your database and drills down to flamelink.users.<uid>.permissions , where <uid> is the user ID of the user trying to access the DB. The value of this database field is an integer, so we cast it to a string with + '' so that we can use it in the next part of our rule.

root

.child('flamelink')

.child('permissions')

.child(<our-previous-query>)

.child('content')

.child($environment)

.child($contentType)

.child('view')

.val() === true

Again, we start at the root of the DB and drill down until we get to the actual permission group’s configuration: flamelink.permissions.<user-permission-group>.content.<environment>.<content-type>.view .

Each permission group configuration consists of the following 4 boolean properties that map to a standard CRUD config:

{

create: true,

delete: false,

update: true,

view: true

}

To check for any of the other permissions, simply replace “view” with “update”, “delete” or “create”.

You might have also noticed the auth != null part at the beginning of the rule. That is to ensure we’re still checking that the user is logged in, otherwise, all our hard work would be undone by someone simply not logged in.

That is it for the ".read" rule. The ".write" rule is similar to our reads, but more complex because we need to also take into account what the user is trying to do to the data to determine whether we should check the create, update or delete config.

We’re brave developers, so let’s continue.

{

".write": "auth !== null &&

((!data.exists() &&

root

.child('flamelink')

.child('permissions')

.child(

root

.child('flamelink')

.child('users')

.child(auth.uid)

.child('permissions')

.val() + ''

)

.child('content')

.child($environment)

.child($contentType)

.child('create')

.val() === true) ||

(!newData.exists() &&

root

.child('flamelink')

.child('permissions')

.child(

root

.child('flamelink')

.child('users')

.child(auth.uid)

.child('permissions')

.val() + ''

)

.child('content')

.child($environment)

.child($contentType)

.child('delete')

.val() === true) ||

(data.exists() && newData.exists() &&

root

.child('flamelink')

.child('permissions')

.child(

root

.child('flamelink')

.child('users')

.child(auth.uid)

.child('permissions')

.val()

)

.child('content')

.child($environment)

.child($contentType)

.child('update')

.val() === true))"

}

Now that we’ve ripped off the bandage, what is happening here?

Apart from the auth != null check for logged in users, there are 3 distinct parts to our rule, each dealing with a different action (create, delete and update).

For our create action we make use of Firebase’s data.exist() method to check if no data currently exist for the particular content. That is how we know someone is trying to add new data.

For our delete action, we use the newData.exists() method to check if new data would not exist. If the user’s action would result in no new data, we know they’re trying to delete something.

For our last update action, we combine the data.exists() and newData.exists() methods to determine that a user is trying to change existing data to something else.

That was not so bad, was it?

For a full example of how this can be applied, see this gist.

This approach is not without its limitations. Since Flamelink is an evergreen and always-evolving product, new features are constantly added which could result in new nodes added to the database. If you tie down the database so much that we cannot make the necessary updates to your database structure, you won’t have access to the shiny new features. You can get around this by combining the UID specific rule we looked at earlier with this dynamic setup and ensure that if the user currently logged in is the owner of the project any writes can be made to the database. This would ensure that when new features are rolled out and the owner logged into the project, the necessary DB structure changes are applied.