Role based access control
Implementing RBAC with Biscuit
Role-based access control is a common authorization model where a set of permissions is assigned to a role, and a user or program can have one or more roles. This makes permissions more manageable than giving them to users directly: a role can be designed for a set of tasks, and can be given or taken back from the user depending on their duties, in one operation, instead of reviewing the user's entire set of rights. Changing the permissions of a role can also be done without going through all the users.
Let us imagine a space-faring package delivery company. Each member of the company has specific duties, represented by roles, that can perform specific actions.
We want to check if an operation is authorized, depending on the user requesting it. Typically, the user id would be carried in a fact like
user(0), in the first block of a Biscuit token. Each employee gets issued their own token.
From that user id, we would look up in the database the user's roles, and for each role the authorized operations, and load that as facts. We can then check that we have the rights to perform the operation:
Why are we loading data from the database and checking the rights here, while we could do all of that as part of a SQL query? After all, Datalog is doing similar work, joining facts like we would join tables.
We actually need to use both: a SQL query to load only the data we need, because requesting all the users and roles on every request would quickly overload the database. And we load them in Datalog because we can encode more complex rules with multiple nested joins and more specific patterns. Example: we could get an attenuated token that only delegates rights from a particular role of the original user.
Another question: why are we creating the
right() facts, instead of using the body of that rule directly in the allow policy?
Verifying inside the policy would work, but we would not get another benefit of Datalog here: we can use it to explore data. Try adding more
user() facts and see which rights are generated. Try to add rules to answer specific questions.
Example: write a rule to get the list of employees that are authorized to deliver a package.
Resource specific roles
We only adressed authorization per operations, but often roles are also linked to a resource, like an organization in a SaaS application, a team or project in a project management software. Users can get different roles depending on the resource they access, and they can also get global roles.
We have high priority packages that need special handling, so not everybody can deliver them. We will create different roles for normal and high priority packages. There are multiple ways this can be done, depending on your API and data model. You could have a generic role or role assignment with a "resource type" field, like this:
Or we could have roles defined per resource, and users are assigned those roles:
Or even different types of roles:
Let's use the second version, and see how data is fetched from the database:
You can explore the full example here:
Roles work great when the user structure is well defined and does not change much, but they grow in complexity as we support more use cases, temporary access, transversal roles, interns, contractors, audits...
Attenuation in Biscuit provides a good escape hatch to avoid that complexity. As an example, let's assume that, for pressing reasons, Leela has to let Bender deliver the package (usually we do not trust Bender). Do we add a new role just for him? Does Leela need to contact headquarters to create it and issue a new token for Bender, in the middle of traveling?
Leela can instead take her own token, attenuate it to allow the delivery of high priority packages for a limited time. She can even seal the token to avoid other attenuations. We would end up with the following:
Attenuating a token does not increase rights: if suddenly Leela loses the delivery role, the check of the attenuated token could succeed but authorization would fail both for Leela and Bender because the
right fact would not be generated.