Summary: The publication continues our series about the advantages and specifics of the Ruby on Rails framework. Protecting sensitive files and data requires a reliable approach to granting roles and permissions inside the Rails app. This article describes the best practices of our developers applying granular permissions to assign specific roles in Ruby applications.
Why do we need granular permissions?
Cybersecurity of web applications has been evolving over the years, raising the bar for organizations and applications to provide a sufficient level of security. Ruby on Rails is an efficient framework for building a scalable and secure web application.
Roles and granular permissions are crucial in restricting access to data and access. This way, you reasonably restrict access to the IT infrastructure, virtual networks, cloud systems, and sensitive information. Top Rails applications benefit from the security features of the Ruby on Rails framework.
Permissions provide mechanisms for restricting access to specific actions for different categories of users. In a nutshell, they determine areas that the current user can access. For instance, granular permissions determine whether the user can edit or watch the post or whether the user changes these settings. The granular permissions system answers the question: Which part of a system can I access as the user? What can I do with this access as the user?
Depending on the application’s type, we can install and use ready-made solutions like CanCanCan or Pundit, where the Policy implements all permissions. However, these options do not fit the applications allowing users to manage permissions or add custom roles. In the latter applications, we need a system that lets you dynamically change permissions without adjusting the code.
Below, we’ll describe our experience with designing granular permissions.
Requirements
Our task is to enhance the new application with the granular permissions system. These detailed permissions can differentiate between actions made with attributes of entities (from now on, resources).
Let’s delve into the core requirements:
- Multiple resources, each has specific attributes.
- Differentiation of access to individual attributes.
- Ability to add roles by users.
- Users can change permissions for roles.
Example resources: Company, Workplace, Employee, Report. Every resource has its attributes.
Operation examples: CRUD (create, read, update, destroy), custom (import, download, etc.). We’ll underline it again that different resources may take different operations.
One picture is worth a thousand words, so let’s move on to a diagram. Taking some inspiration from the permissions system on Salesforce and AWS, we came to the following structure of models and tables in the database:
The above structure is abstract and is not tied to the specific domain industry. We will give a few simplified examples from our Rails application. Remember that the roles, resources, and permissions can be anything.
User
So, there are many users in the system. The row in the table “users” matches the user.
Role
Each row in the roles table represents a role with a set of permissions. For example, our application has several standard roles: Admin, Employer, and Worker. They have the element company_id=null.
These roles have fixed permissions that users do not alter. But users can create custom roles in their companies, respectively, company_id means the role in the company.
To allow configuring standard roles, each company must create a default set of three roles with the corresponding company_id. The choice of option depends on business requirements.
UserRole
As the name suggests, this table is needed to connect the user and their role. In our application, one user can have several roles. Still, inside the application, they can choose a specific role when logging in, so we don’t have to worry about conflicting permissions in different roles.
Permission
We record a set of operations for each resource in the table, with all combinations and resource attributes to which the operation is applicable.
For example, there is a Report resource with two attributes: description and filed_on (filing date). These attributes can be read or updated, and the report can be created. Bear in mind that it is a very simplified example.
The following entries will be created in the permissions table:
- Permission(id=1, resource=Report, operation=read, field=description)
- Permission(id=2, resource=Report, operation=read, field=filed_on)
- Permission(id=3, resource=Report, operation=update,
Table of Contents
- Permission(id=4, resource=Report, operation=update, field=filed_on)
- Permission(id=5, resource=Report, operation=create, field=null)
Some operations may not be applied to attributes and cover the entire resource. In our example, this is create. This system allows you to define possible permissions flexibly for various operations on resources and their attributes.
RolePermission
This table links roles to permissions. If the role has permission for a specific action on a resource attribute, you’ll find a record with the appropriate role_id and permission_id in this table.
Let’s look at the following example. The Resource Report is already familiar to us. There’s a role Employer.
Let’s read the database to find permissions that are associated with this role through roles_permissions, and we get the following list:
- Permission(id=1, resource=Report, operation=read, field=description)
- Permission(id=2, resource=Report, operation=read, field=filed_on)
- Permission(id=3, resource=Report, operation=update, field=description)
These permissions allow the user to read the description and field_on of the report. Still, only the description can be edited because there is no filed_on edit permission entry.
How to use it?
Here are some simple code examples.
Creating new permissions:
permission = Permission.create(resource: 'report', operation: 'read', field: 'description')
Changing permissions for a role:
# adding
role.permissions << permission
# removing
role.permissions.delete(permission)
Checking permissions
This is the way to check if an operation is allowed on a specific resource:
current_role.permissions.exists?(resource: 'report', operation: 'create')
Checking if the operation is allowed on the resource attributes:
current_role.permissions.exists?(resource: 'report', operation: 'update', field: 'description')
Getting a list of all readable attributes of a resource:
attributes_list = current_role.permissions.where(resource: 'report', operation: 'read')
Creating a custom role:
role = current_company.roles.create(name: params[:name])
role.permissions << Permission.where(id: params[:permission_ids])
As we can see, the Granular permission scheme solves many issues.
- We can add more resources, permissions, and roles without code adjustments (via new entries in the corresponding tables).
- Adding custom user roles is easy since everything is stored in the database.
- It is easy to cache, for example, with the help of Rails.cache function. Permissions and roles are rarely changed.
Wrapping up
Of course, there is no “one fits all” all-purpose solution. When you set up the permission system, don’t forget to ensure its compatibility with future development plans. Otherwise, its structure will have to be changed once in a while. Our task is to create a sustainable permissions system that entails current requirements and development plans.
You may be interested in the following articles:
- Sidekiq Batches with Sub-Batches. Simple Way to Organize Code
- Optimize the Performance of Ruby Web Applications Through the Server Configuration
- Optimizing the Web App Scaling Process with Ruby on Rails