Roles and permission: the right way
Authentication and authorizazion
Many applications need an authorization system that allows different users to do different things. There are many ways to implement such system; in this post we’ll describe our preferred strategy.
Let’s start by distinguishing authentication and authorization.
- Authentication is the part that identify users, e.g. checks username and password to make sure the user is valid.
- Authorization checks that the user is actually allowed to do something (logging in into backend, listing users, creating posts). Authorization rules can quickly grow as new features are added, so it’s a good idea to setup a neat authorization system.
Based on our experience, we suggest to use a system composed of permissions and roles.
By “permissions” we mean all activities which are subjected to an authorization check. Think about creating a new post. Let’s have some basic guidelines:
- permissions are hardcoded, because they reflect real actions that the current user may or may not do
- the final answer to a permission check can only be either a yes or no, based on whatever domain logic we need.
The quickest (but not reccomended) option is to place the check immediately before the code that performs the task. This may be acceptable if the check is very simple, e.g. if only one specific administrator user can perform the task. But it’s better to delegate the check to a separate service which knows which users are allowed to perform each task. More about that later.
In Symfony there are Voter classes, which are really flexible permission checkers. Each Voter identifies a subject (in our example, a Post) and one or more attributes (the available tasks on the subject, e.g. “create”). Each Voter is automatically invoked when the system requires a permission check on a supported subject/attribute combination. In the Voter we define the actual code that checks if the permission must be granted or not. We can have multiple voters for each combination, so that each class to remains small and simple.
Voters are really powerful and definitely reccomended if you have more than 2 different set of permissions in your application.
Now that we have defined the permissions and a way to delegate the check, let’s focus on the actual check. If you have very few users you may be tempted to assign permissions directly to users, i.e. keep a list of allowed usernames and tasks. But as the system grows the best option is to use the intermediate concept of roles.
By roles we mean a set of permissions: nothing more, nothing left. This definition is very important: too often roles (sometimes called groups) are based on real people groups, like MANAGERS, DEVELOPERS, CUSTOMERS. But it is probably best to define roles from the application point of view. If there is a set of permissions which enable to add / modify / delete posts we should group them into a ROLE_POST_MANAGER. If there a set of permissions which allows to read but not change support tickets, we may want to create a ROLE_SUPPORT_VIEWER.
Each role should be very specific and with a self-explaining name, but some users may need to do multiple things in the application. So of course we should be able to assign multiple roles to a user.
Also, we can have hierarchical roles: roles that includes other roles. The most common instance of this is a ROLE_ADMIN which usually includes all others, because – let’s face it – there is always someone which is allowed to do everything in the application. When we find ourselves assigning the exact same set of roles to many users, it’s time to think about creating a common parent role.
Putting it all together
If you have ROLES and PERMISSIONS in place, the permission check becomes very easy: it’s only a matter of checking if current user (or any given user) is granted the required role. After that, you only need to assign / remove roles from users.
In Symfony roles are natively supported in the security system, including the hierarchical feature. Jusr remember to use isGranted instead of hasRole: the first method supports hierarchical role checking (i.e. if one of user’s roles includes the required role, it’s ok) while the second just checks if user has been specifically assigned that role.
By using ROLES and PERMISSIONS we keep our authorization system very flexible and powerful. We also take care to delegate authorization checks to dedicated classes so the code in our main controllers is short and easy to follow.