Manage your users' actions efficiently thanks to a robust, scalable system
Introduction When we talk about security in the context of an application, we immediately think of three aspects: Vulnerabilities (XSS…) Authentication Roles/permissions In this article, we will explore different ways to manage user access and actions within a hypothetical platform designed to offer client/prospect management as well as quote/invoice tracking. Our application allows SMEs to record billing information of their prospects or clients to easily reuse it for future quotes and invoices. Considering that in an SME there may be several users needing access to the platform, we want these users to have different permissions. We can identify the following access levels: Full admin: for the company owner Semi-restricted access: for HRs Our business owner should have full access to all the services offered by the platform. Our HR accounts should have full access to customers as well as quotes and invoices. At this stage, we identify two business needs requiring different treatments and introducing the notion of access restrictions. Different approaches The previously described constraints illustrate a level of complexity that can grow over time and according to the company’s future needs. To implement a robust and scalable permission management system, let’s first explore several ways to build a solid solution. Hardcode This method involves writing the authorization rules directly into the business logic. Let's imagine a simple structure to model our case. pub struct User { pub firstname: String, pub lastname: String, pub is_admin: bool, pub is_rh: bool } fn can_create_document(user: &User) -> bool { user.is_admin || user.is_rh } This approach may be suited for prototypes aimed at testing a feature’s feasibility but has many issues and should very rarely be used in production: No flexibility (a change requires a redeployment) Often duplicates rules Hard to test or trace RBAC (Role-Based Access Control) In the RBAC model, users are assigned one or more roles (e.g., Admin, HR, Sales). Each role is associated with a predefined set of permissions. pub struct Role { pub id: String, pub name: String } pub struct User { pub firstname: String, pub lastname: String, pub roles: Vec } fn can_create_document(user: &User) -> bool { user.roles.iter().any(|role| { role.name == "Admin" || role.name == "RH" }) } In our case, note that the name property may change, so it is better to rely on the role's id. This practice is interesting because it allows storing a set of roles in a persistent storage system, which we can modify. Adding a new role requires code changes to account for it in our permission management. Also, be very careful not to delete a role if it's referenced by id, as this would break permissions irreversibly. However, this practice has the advantage of having a simple model to conceptualize and manage if roles are well-defined. Drawbacks As mentioned, the essence of the RBAC model becomes inefficient in some cases: As the business evolves, the number of roles and business rules increases Temporary access for one person often requires creating a “temporary” role, which involves code changes ABAC (Attribute-Based Access Control) ABAC is a more granular approach, based on evaluating N dynamic attributes depending on your needs and business context. However, we generally see two attributes: The resource The action Let’s consider a set of user actions for our platform: create: allows creating a new document update: allows editing a document delete: allows deleting a document These actions apply to a given resource—here, document—and we scope our actions as :. Here is a Rust example: pub enum Permission { CreateDocument, UpdateDocument, DeleteDocument, } impl Permission { pub fn serialize(permission: &str) -> Option { match permission { "document:create" => Some(Permission::CreateDocument), "document:update" => Some(Permission::UpdateDocument), "document:delete" => Some(Permission::DeleteDocument), _ => None, } } } fn can_create_document(user: &User) -> bool { user.permissions.iter().find_map(|element| { if let Some(permission) = Permission::serialize(element) { permission == Permission::CreateDocument } else { false } }) } With this action-based approach, we get much finer control over user actions, but it takes longer to implement due to the number of permissions we must declare and manage. We can simplify the above code like this: pub enum Permission { ... } impl Permission { pub fn serialize(permission: &str) -> Option { ... } pub fn has(permissions: &Vec, target: Permission) -> bool { permissions.iter().find_map(|element| { match Permission::serialize(element) { Some(permission) if permission == target => Some(true),

Introduction
When we talk about security in the context of an application, we immediately think of three aspects:
- Vulnerabilities (XSS…)
- Authentication
- Roles/permissions
In this article, we will explore different ways to manage user access and actions within a hypothetical platform designed to offer client/prospect management as well as quote/invoice tracking.
Our application allows SMEs to record billing information of their prospects or clients to easily reuse it for future quotes and invoices.
Considering that in an SME there may be several users needing access to the platform, we want these users to have different permissions. We can identify the following access levels:
- Full admin: for the company owner
- Semi-restricted access: for HRs
Our business owner should have full access to all the services offered by the platform.
Our HR accounts should have full access to customers as well as quotes and invoices.
At this stage, we identify two business needs requiring different treatments and introducing the notion of access restrictions.
Different approaches
The previously described constraints illustrate a level of complexity that can grow over time and according to the company’s future needs.
To implement a robust and scalable permission management system, let’s first explore several ways to build a solid solution.
Hardcode
This method involves writing the authorization rules directly into the business logic.
Let's imagine a simple structure to model our case.
pub struct User {
pub firstname: String,
pub lastname: String,
pub is_admin: bool,
pub is_rh: bool
}
fn can_create_document(user: &User) -> bool {
user.is_admin || user.is_rh
}
This approach may be suited for prototypes aimed at testing a feature’s feasibility but has many issues and should very rarely be used in production:
- No flexibility (a change requires a redeployment)
- Often duplicates rules
- Hard to test or trace
RBAC (Role-Based Access Control)
In the RBAC model, users are assigned one or more roles (e.g., Admin, HR, Sales). Each role is associated with a predefined set of permissions.
pub struct Role {
pub id: String,
pub name: String
}
pub struct User {
pub firstname: String,
pub lastname: String,
pub roles: Vec<Role>
}
fn can_create_document(user: &User) -> bool {
user.roles.iter().any(|role| {
role.name == "Admin" || role.name == "RH"
})
}
In our case, note that the name
property may change, so it is better to rely on the role's id
.
This practice is interesting because it allows storing a set of roles in a persistent storage system, which we can modify.
Adding a new role requires code changes to account for it in our permission management. Also, be very careful not to delete a role if it's referenced by id
, as this would break permissions irreversibly.
However, this practice has the advantage of having a simple model to conceptualize and manage if roles are well-defined.
Drawbacks
As mentioned, the essence of the RBAC model becomes inefficient in some cases:
- As the business evolves, the number of roles and business rules increases
- Temporary access for one person often requires creating a “temporary” role, which involves code changes
ABAC (Attribute-Based Access Control)
ABAC is a more granular approach, based on evaluating N dynamic attributes depending on your needs and business context. However, we generally see two attributes:
- The resource
- The action
Let’s consider a set of user actions for our platform:
-
create
: allows creating a new document -
update
: allows editing a document -
delete
: allows deleting a document
These actions apply to a given resource—here, document
—and we scope our actions as
.
Here is a Rust example:
pub enum Permission {
CreateDocument,
UpdateDocument,
DeleteDocument,
}
impl Permission {
pub fn serialize(permission: &str) -> Option<Self> {
match permission {
"document:create" => Some(Permission::CreateDocument),
"document:update" => Some(Permission::UpdateDocument),
"document:delete" => Some(Permission::DeleteDocument),
_ => None,
}
}
}
fn can_create_document(user: &User) -> bool {
user.permissions.iter().find_map(|element| {
if let Some(permission) = Permission::serialize(element) {
permission == Permission::CreateDocument
} else {
false
}
})
}
With this action-based approach, we get much finer control over user actions, but it takes longer to implement due to the number of permissions we must declare and manage.
We can simplify the above code like this:
pub enum Permission { ... }
impl Permission {
pub fn serialize(permission: &str) -> Option<Permission> { ... }
pub fn has(permissions: &Vec<String>, target: Permission) -> bool {
permissions.iter().find_map(|element| {
match Permission::serialize(element) {
Some(permission) if permission == target => Some(true),
_ => None,
}
}).unwrap_or(false)
}
}
fn my_policy_handler(user: &User) -> bool {
Permission::has(&user.permissions, Permission::CreateDocument)
}
This method requires associating users with a large number of permissions, which gives optimal control but requires more setup.
What about roles?
We previously mentioned wanting two roles—Admin
and RH
—to delegate permissions to users.
We can now consider a hybrid approach combining ABAC and RBAC to leverage their respective strengths: atomic control and ease of use.
The hybrid approach
In this section, we’ll use the following data structures:
pub struct Role {
pub name: String,
pub permissions: Vec<String>
}
pub struct User {
pub username: String,
pub lastname: String,
pub roles: Vec<Role>
}
This approach “packages” our atomic actions into roles so that assigning a role automatically grants a set of permissions.
We can then create a function to check for a permission in the user’s roles:
pub enum Permission { ... }
pub struct User { ... }
impl User {
pub fn has_permission(&self, permission: Permission) -> bool {
self.roles.iter().any(|role| {
Permission::has(&role.permissions, permission).is_some()
});
}
}
fn my_policy_handler(user: &User) -> bool {
user.has_permission(Permission::CreateDocument)
}
Looks good, but I still can’t give a permission to one specific user without giving it to everyone with that role!
True—and that’s not a mistake. It’s the next improvement