Recently I revisited a way to deal with permissions in an application. Miguel Grinberg's Flask Web Development book presented a way to do this with so-called "bit flags". I did not fully understand how this was implemented until last night. It's quite a clever way of dealing with permissions in an application. In order for me to understand how this works I needed to look more into "bit flags". So this post will cover my understanding of this interesting data structure.
Before going further I'm just going to emphasize that my understanding really hinges on two data sources. First being the Flask book and the second being this article from a Nasa site. But before I dive into the details let's just talk about the high-level concepts:
It probably seems weird to go into defining these things because we are all so intimately familiar with the concept of roles and permissions. They exist in nearly every application that we use on regular basis. But I still find it helpful to keep in mind that a role for a user in an application is comprised of many permissions. Discord is a great example of an application that is highly configurable through custom roles.
So how can we represent a users permissions in our application? The upshot is that with "bit flags" you can uniquely represent the users role with a single integer value. But let's think about this a little bit. Let's say that we define some permissions like so:
COMMENT = 1
WRITE = 2
EDIT = 3
ADMIN = 4
SUPER = 5
Awesome, we've defined 5 permissions in our application. And each one of them has an integer value assigned to it. Now we have to think about roles. How could we represent a "standard" user role in our application? Let's say that a standard user role allows commenting, writing and editing. There are a couple ways to think about that users role:
COMMENT + WRITE + EDIT = 6
. Therefore this
standard user role is represented by the value 6. This is what I said
we could do earlier, we could represent all permissions in the
application as a single integer value. (But this has issues)[1,2,3]
.How do we reverse this though? What if we needed to figure out which
permissions a user has? The addition method has a glaring problem. There
is more than one way to add the permissions such that the end result is
6
. I.e. COMMENT + SUPER = 6
and WRITE + ADMIN = 6
. So simple
addition looks like it's out (but we will find later all hope is not
lost).
With the list of permissions approach we can at least uniquely identify
the roles that user has, but it has a different problem. For every
standard user we need to store at least 3 values. It could be up to 5
values if the person is also an ADMIN
and a SUPER
. Add even more if
your application allows more, i.e. Discord and Jira. Everywhere you
need to pass the info about your users role, you would need to send
these 3 values. And in application that has a lot of users that can
become burdensome.
How can this be done better? This is where bit flags and bitwise operators come in. Let's change up the underlying representation of our permissions and dive into this approach:
COMMENT = 0b00000001 (binary) = 1
WRITE = 0b00000010 = 2
EDIT = 0b00000100 = 4
ADMIN = 0b00001000 = 8
SUPER = 0b10000000 = 128
What's different? Well we chose to represent the permissions in binary.
In other words, each permission is a power of 2. This has the solves the
issue with adding the values of the roles. Therefor each sum of the
permissions will end up unique. I.e. our standard role no longer conflicts
with the ADMIN
permission:
COMMENT + WRITE + EDIT = 7
So great, we've solved that issue. We can now store a single integer in our database for every user of our application, and it will uniquely represent the persons role. But we need to be able to check this persons role to customize their experience in our application. How can we do that? It turns out that we can actually get some more utility out of using binary numbers to represent roles. We can use bitwise operators as well.
The upshot here is that bitwise operations are very efficient operations computationally, they are also built-in to the foundation every programming language. Here's an example in Elixir of how we can combine the permissions for our standard user role:
use Bitwise
comment = 0b00000001
write = 0b00000010
edit = 0b00000100
standard_user = comment ||| write ||| edit
The |||
is called the bitwise OR operator, this is the particular
symbol that Elixir uses for "bitwise OR". It will be different in every
language. We can read this literally as the standard user can comment or
write or edit.
Now imagine that a standard user is going to edit a post that they created on our site. How would our application check that this person is allowed to do that?
iex(20)> standard_user &&& edit
4
What if the user is trying to access an admin-only dashboard?
iex(21)> standard_user &&& admin
0
What if the user is trying to do super-only action?
iex(22)> standard_user &&& super
0
This &&&
operator is the bitwise AND operator for Elixir. What you can
see from the outputs above is that if a user has the permission we are
testing, it returns the integer value of that permission. If a user does
not have the permission we get back 0
. This gives a very efficient and
convenient method of checking if a user is authorized for a given task
in an application.
It's worth revisiting the list of permissions we discussed earlier, and comparing the approach for how we would check a users permissions in that scenario. First and foremost we would need to create a list in memory:
>>> roles = [1, 2, 4]
Then we would need to check if the required role for a given action in the application is contained in that list:
>>> admin in [1, 2, 4]
To me this doesn't seem too much worse. I suppose a downside is that if your application grows and you need to add more permissions, you will need to adjust your database model to have a new column. Otherwise you might be able to make use of JSON column type, and store the list directly. Either scenario seems more complex than using the binary representation which involves storing a single integer value in your database. The point is that using a list has more tradeoffs than just performance and memory optimizations, some of which I probably have not even considered.
My next goals are to incorporate this into a database model, and see how to develop a model within Elixir and Phoenix that makes use of this type of a permissions scheme. I am not an expert on this method of permissions. I am merely reflecting what I have learned from these resources, which I recommend checking out:
If I have missed something or you would like to chat about anything in this post, please feel free to email me at wgwz@pm.me.