Redesign an access control system
Another uncompleted article I never published, because being perfectionist should never be an excuse not to share 🤷‍♂️
Current situation
We only have two types of authenticated users at the moment wich are defined as “operator” and “admin” wich are basically users with ability to create operator accesses and run some tests on the app.
These operators can authenticate using an email and a password and are represented by a document stored in an “access” collection wich has a “role” property used by a firewall and other several times to segregate users accesses in the application logic.
At some point, this access document implemented a new “appointments” property so the operators can see the appointments they were assigned to.
The current system use token wich are validated, read, and passed to the app logic by the firewall.
Goals
- We want to implement an optionnal access to installers wich are only known as ressources in the application for now.
- We want to identify accesses based on user type and use the access object to gant permissions.
- We want to clarify the difference between an access and the different kind of users in our new data structure.
Phase 1 — Identity and access segregation.
At first, we will implement a new role to discriminate users and drive the firewall. To support this new role, we will also need to link the existing installer ressource with access and create a new operator ressource wich doesn’t fit with the access document anymore.
At this extent, we keep overloading the access structure with use case driven properties. This is not good, but will help to clarify codebase modifications and databases migrations while introducing a strong pattern of identity and access segregation.
- The admin users will have the role “admin” and will be linked to an operator (see why we force admins to exists in operators at phase 2)
- The installers may have-or not, an access to the application.
- The operators will always be linked to an access.
- We use a foreign key in the access structure to link with other entities when needed (operator or installer)
To implement this, we will only need to update the existing firewall with a way to load the installer or operator data to the user context it send to the app. This context will have a conditionnal shape at this point
We also have to load the new tokens with the proper user data regarding wich user it is.
Phase 2 — Agnostic access control
Now it is time to clean up the access data structure and make it agnostic of the application ressources and actors in order to build a sustainable access control system.
We have to know who have an access and to do what. This is possible implementing a data driven pattern where:
- “Grant”(or however we call it) will link the access to one specific ressource from its point of view without any need to overload its data structure with new fields as the application grow up.
- and “Permission” will define an access level to one specific entity. We will use a CRUD graph for that so we can improve from an entity level permission to a property level for future developments without generating new access control use case handled in business logic.
- For specific use cases, you may feel we won’t be able to define an access control logic using CRUD, you are right, but we will not use it, only create or delete a permission from a proper business event handler.
To avoid creating meaningless collections, we will prefer to make “Grant” a property of “Access”. The use of such an object could be ease by implementing linking functions as middlewares to data access layer in order to prevent inoperating links generation.
Permissions, otherwise, will be a dedicated collection with a one to many link to improve firewall and event handlers performances.
Using this structure, we can update the firewall so it reads the access and permission as easily as we do:
“Grant {grant.collection} {grant.entity}, authenticated using mail {email} and the relating password, the permission to {permission.action} {permission.collection} {permission.entity}”
This will be used to give installers read and update permissions on specific appointments but will also be used to centralize permissions and make it transparent for the IT admin. (for create rights, we should update the data structure
Phase 3 — Dealing with authentication
So now we have a sustainable access control for this project, we may want to use it again in other projects, and eventually make a service out of it (wich is easier due to the current nosql architecture). How do we ensure it will fit with predicatable needs ?
At some extent, we would probably need to support different ways to authenticate. This is possible by implementing authentication strategies wich will aggregate any authentication methods supported using dedicated logics and endpoints.
eg: “mail_and_password” strategy would only need to store an email and encrypted password, while other strategies may need to store token, encryption keys, IPs, localisation, …
We could implement this on the current system by adding a new parameter to login route in order to specify the authentication strategy.
An authentication strategy should implement:
- A login method wich will validate access and update, or create the token.
- An update method (eg to change password or api key)
- And eventually more in the future (retrieve, reset, expire, forbid,…)