Managing User Permissions in an Angular Application
Modern applications usually displays only what is visible to the user based on their role. For example, a guest user may read stories but can’t write comments on Medium. Or another example, an authorized user can write and remove drafts. But that user is not allowed to see or remove somebody’s else drafts. And that’s pretty reasonable, isn’t it? :)
It all sounds cool but sometimes managing such accessibility may become a nightmare. You probably have written or seen code like this before:
Later, this code is spread over the application and becomes a big problem when you need to add a new role in the app or change permissions of existing role. Eventually you need to change some of *ngIf
checks or in the worst case, change them all.
In this story, I want to share an alternative way to implement permission management by using a neat library which is called CASL.It makes managing user permissions much simpler, and allows to rewrite the previous example to:
First time you’ve heard about CASL? You may want to read “What is CASL?”.
Update: CASL 4.0 is released, see “CASL 4.0. What’s inside?”
Demo application
To illustrate how to use CASL, I decided to use well known Todo appication with small additions:
- you can assign tasks to users (“me” represents current user)
- you can switch between roles
There are two roles in application:
- “member” (i.e., regular user). Can read all Todos and CRUD (i.e., create, read, update, delete) only on assigned to him
- “admin”. Can CRUD any Todo
This logic is defined by using AbilityBuilder
class which allows to define user permissions by using declarative function calls:
In order to understand this function, we need to dive a bit deeper into CASL details. To do this, let’s go through each line of defineAbilitiesFor(role)
function.
AbilityBuilder.extract()
creates an instance of AbilityBuilder
and extracts rules
array and its can
, cannot
methods. With help of can
(or cannot
which is not used here), it’s possible to define user permissions. can
(and cannot
) both accepts 4 arguments, 2 of which are required: action name and subject name.
Action name represents a user’s action in application (in most cases it will be standard CRUD) and subject name represents model name which this rule is defined for.
Let’s take a look a the code again.
If role
argument of defineAbilitiesFor(role)
equals admin
, then user can do anything in the system (i.e., super admin rights). This is specified by can('manage', 'all')
statement. As I said before the first argument is an action name, manage
is a special keyword which represents any action in the system. And the second argument is a subject name, in this case it’s all
, which is a reserved alias for any subject name. So, if user has role admin
he can perform any action on any model.
In case user is not an admin, he can read
any model (this is specified by can('read', 'all')
) and can do any action on instances of Todo
model where model’s assignee
property equals to me
(i.e., can('manage', 'Todo', { assignee: 'me' })
).
As you can see, the last permission declaration has 3 arguments. The third arguments represents conditions
object. In this case, it clarifies which instances of Todo
user is allowed to manage.
To understand it clearly, let’s look at this example:
Here I created a separate class Todo
which represents a task to be done. This class accepts an object which must contain 2 fields: task’s title and who should work on this task. Later, I create 2 instances of Todo
class: 1 that represent task assigned to me
(remember conditions object?) and another one which is assigned to John
. I also create an Ability
instance with permissions for user with member
role.
Finally, I check permissions on these 2 todos. As you can see ability.can("read", myTodo)
and ability.can("close", myTodo)
returns true
, this is because early we defined that users who are not admins can do anything with tasks assigned to them (i.e., can('manage', 'Todo', { assignee: 'me' })
). Due to exactly the same reason, ability.can("close", johnTodo)
returns false
because we can only read his todo not close (i.e., can('read', 'all')
).
Do you see how closely explanation in console.log
reflects the actual check? They are almost identical (except that first one just a string and the last one do a complex permission check :)):
console.log(
"can read my todo?",
ability.can("read", myTodo)
)
Of course, now some of you may have questions like these:
- What other restrictions can be enforced by
conditions
object? You can learn about this in documentation here. - Shall I always define a class for all my models on UI? The short answer is NO but if you don’t use classes (and I think majority of devs don’t use them on frontend), you need to let CASL know how to detect subject name based on the object instance passed in
ability.can(action, subject)
. You can learn about this here.
CASL and Angular
CASL is shipped together with complementary package for Angular 2+. So, you can quickly add it into your Angular app.
Let’s start from installation
npm i @casl/ability @casl/angular
# or
yarn add @casl/ability @casl/angular
Later, include AbilityModule
in your AppModule
(use AbilityModule.forRoot()
if you include it in root AppModule
):
@casl/angular
provides the empty instance of Ability
by default. Here I specified own provider which defines Ability
based on provided rules (i.e., permissions). Alternatevely it’s possible to update rules of provided by default instance
Afterwards, you can inject Ability
instance in your component to check permissions or update them:
Alternatevely you can use can
pipe in templates for simple checks (more details here). So, you can write code like this in Angular’s templates:
With that, we have a really nice way to manage permissions in Angular app.
The full example of the Angular based app with CASL can be found on github or codesandbox:
If you like CASL, please star it on github and share the article with your friends :)
Looking for more?
- Read CASL documentation
- Read README file of @casl/angular package
- Join Gitter channel to discuss CASL, suggest features or ask for help
Interested in integration with another framework?
CASL has complementary packages for major frontend frameworks, check the articles below: