New project. Once again it was necessary to solve the problem with the division of rights. Once again I had to reinvent the wheel. So I thought, is it not easier to deal with this problem once and for all. I want to solve the problem “on paper” so that these principles can be used independently of technology.
The evolution of the system of separation of rights
Usually the division of rights evolves as follows:
First make the admin flag in the user table.
')
Then it turns out that in addition to admins there are other types of users. Add groups (predefined set). Rights and groups are tightly connected. The relationship between groups and users is one to many. This is often the case because the division of rights is associated with an organizational hierarchy.
Further, end users want to create groups themselves and distribute rights to them.
The next step is that some users need permissions beyond their group. There are such options:
- Modify the system so that you can issue rights directly to the user. If the base is relational, then get queries with union.
- You can create groups for each user with "extra" rights. With this approach, there will be a problem with mass editing. Suppose we have users who have rights A and B and a certain number of users (from the same organizational group), but who also have other rights X, Y, Z. At one point it was necessary to remove right A. To do this, it will be necessary to remove the right A from each specially created "extra" group.
To solve a problem with “extra” rights, sometimes group inheritance is done. This leaves the problem with mass editing. But there is the problem of resolving conflicts permissions / prohibitions. For example: in group A, some action is allowed, and in group B, which is inherited from it, the same action is prohibited.
From a technical point of view, there is an awkward moment - working with a "tree" structure in a relational database. For example, to get the list of rights: you first have to get all the parent groups, then you need to join the rights table. After receiving the entire list from the database, you will need to apply the conflict resolution rules to it. MySQl sometimes uses the “hack” with GROUP BY and ORDER for this, but this solution is not portable, since this GROUP BY behavior does not conform to the SQL specification.
Of course, these are solvable problems. You can figure out how to store roles, how to resolve conflicts. But will it be easy for end users to understand this logic?
It is also likely to have to face the problem of “synchronization” between the code and the base. For the rights to be editable - they need to be stored in the database. But the functions that are responsible for enforcing the separation of rights are stored in code.
How to do
Already seeing what may come out of this, we can draw the following conclusions:
Immediately write a system of separation of rights with the calculation of more than two groups (admins and non-admins).
The following subtlety - in my description there is a small substitution of concepts. The group (which reflects the belonging to the hierarchical structure in the real world) and the group (role) in the system of separation of rights is not always the same. Specially left, as he himself attacked this rake (I think I'm not the only one). It is necessary to separate the concepts of group and role. Then it will be possible to make the relationship between roles and users many to many.
At a ratio of many to many, the problem with “extra” rights and the inheritance of groups goes away. Extra rights are placed in separate roles that are added to the user, except for the “main” role.
Conflict resolution is also simplified: since the structure of rights is “flat”, then the rights themselves will be simple. For example, there may be such rules:
- everything is forbidden by default;
- permits and prohibitions can be issued;
- the ban outweighs any number of permissions;
- permission rules are added (logical or);
- rules of prohibition are added (logical or);
- then all rights of prohibitions are subtracted from all permissions, and all that remains is the desired set of rights that are allowed to the user.
Implementation
I will describe the implementation using the example of a web application that follows the MVC pattern. In such an application, rights can be differentiated at the controller level and / or at the model level. Also, the ACL must provide helpers for the view.
Basic Interface (Basic Module)
Required minimum for ACL operation.
The most basic function that should be in any ACL, answering the question whether this user has the right to this action with this resource:
can (user, 'edit', resource) # => true / false
The function to call in the controller is _ to check if the current user has access. It is assumed that the controller provides a method for obtaining the current user (CoC).
authorize ('edit', resource)
In case there is no access, control will be transferred to the error handler of unauthorized access. Depending on the PL, this can be done through exceptions or via callbacks.
It should be possible to specify your own way of handling unauthorized access errors. For example, if exceptions are used, then in the base controller class, you can specify a handler like this:
catch AccessDenied
redirect
end
It should be possible to set a check for all controllers at once. That it is not necessary to put authorize in each controller. Depending on the technology, this can be done either in the base class of the controller or using the software layer (middleware).
For the basic interface is not fundamentally device groups. This is done specifically so that it can be easily incorporated into existing projects with the goal of remaking them in pieces. The base interface can even work with the admin flag in the user table. To do this, you will need to implement a piece of code that is responsible for determining the rights of the user. For example:
detect (user)
if user.admin
allow ('all', 'post')
else
allow ('read', 'post')
end
end
Role module
It is assumed that the user model provides a method for obtaining user roles (CoC).
If the groups do not need to be stored in the database, then the rights are set using DSL.
allow ('admin', 'all', 'post')
Attribute module
Consider an example: the author can do all CRUD operations with articles. But he can only edit his articles, delete articles, and view drafts. Those. it turns out that now not all articles are the same. This division of rights is decided by the introduction of attributes for resources.
The first parameter is a draft or not (draft = true / false); the second is whether the author belongs (own).
Total:
# can create articles
allow ('author', 'create', 'post')
# can view all articles not drafts
allow ('author', 'read', 'post [draft = false]')
# can view, edit, delete their articles
allow ('author', ['read', 'update', 'delete'], 'post [own]')
To support these attributes, you will need to implement helpers. To work with the model instance (resource):
own (user, resource)
return user.id == resource.user_id
end
To work with the method of obtaining data (list) from the database.
own_filter (user, resource_query)
return resource_query.where ('user_id', user.id)
end
For models, you will need to implement a method that will call the necessary functions. Using this method might look like this:
Post.find (). With_permissions ('read', user)
Calling this function will call the rights sharing module. If you specify a user at the level of the rights sharing module, you will not need to specify a user in each call to this method.
More than one attribute can be applied to a resource, and the attribute conditions should be added according to the rules of logical and.
Storage in the database
The structure described is easily stored in the base.

User, Role - n to n; Role, Right - n to n; Right, Resource - n to 1; Right, Attribute - n to n; Right, Action - n to 1;
Setting initial values
The system must have default settings. In order not to depend on the base implementation, the same DSL can be used to set the initial values ​​(function allow).
Rights editing interface
Editing refers to the ability to create, delete roles and the ability to edit role rights. To create the right, you can select a resource, an action, and attributes from a predefined set. In order for the end user to work with this, they must have a human-readable look (and not a view of the costants that are used in the code).
Accordingly, in the database you need to store this predefined set and human-friendly designation of constants.
Form of creating / editing rights
There should be such fields:
- role (choice or given in context)
- resource selection
- depending on the selected resource, you can choose an action
- depending on the selected resource, you can select the attribute
- allow / deny
And the rights will be displayed like this:
<author> <may / cannot> <edit> <own> <articles>
Synchronization
Tasks to maintain synchronism between the code and the database, as well as to verify the integrity of the better to automate (write scripts), as they have to be done many times.
Collecting data from code
First you need to collect all the data on the code.
Resource collection
Resources can be models or controllers (they are routes or route). Models are easy to assemble; according to the MVC ideology, they must be kept separately (in a separate folder). When collecting routes, too, there should be no problems. In modern MVC frameworks, setting a route looks like this:
get ('/ posts /: id', handler)
Naturally, you can collect both types of resources or only one depending on the needs.
Attribute collection
This is also quite easy to do, because attributes are functions of a certain type that lie in a certain place. Also for attributes you will need to store information about which models they are suitable for.
Action collection
There are 4 standard actions for models (CRUD) and one action for the route (access). All other actions can be searched by text search code (by the function allow, authorize, etc.).
From code to base
After collecting the data on the code, you need to add them to the intermediate storage (file). Then in the same file it will be possible to add human-readable descriptions. It is also possible to circumvent the imperfection of the algorithm for collecting (searching) data by code. Suppose the algorithm does not find some data in the code (most likely actions). Then they can be added to the file by hand. At the next launch, the algorithm again will not find the same data and remove it from the file. To prevent this from happening, they must be marked as manually added so that the synchronization script does not try to delete them. The resulting file must be stored in the version control system along with the code.
When delivering updates or when deploying a system on a different hardware (deployment), you will need to run a synchronization script. The script reads the data from the file. Will make a check that there are no conflicts - the data that is still involved in the separation of rights has not been deleted. Then write new data to the database. Next will add new rights by default (if there is another file with default rights).
File format
During automatic data collection, “extra” data may be included in the file, which should not be displayed in the editing interface (no need to be added to the database). For such data, you need to enter a sign that they do not need to be saved to the database (for example, false instead of description).
{
resources: {
"get: / posts": false,
"model: post": "Articles",
"other"
},
attributes: {
"own": {
description: "Own",
resources: ["model: post", "model: comment"]
}
},
actions: {
"edit": "Edit",
"publish": {
description: "Publish",
resources: "model: post",
keep: true
}
}
}
Pseudo-groups, -resources
For convenience, you can create such pseudo-groups:
- all - all users
- authenticated - all logged in users
- non_authenticated - all non-logged users
And such a pseudo-resource:
Finishing touch
We must not forget to add helpers for testing.
What is not in the article
To readFields (parameters, properties) of models as attributes
Initially, I wanted to collect not only the functions (helpers) of attributes, but also all the fields of the models so that they could be compared with them (draft = false, amount> 100). But I realized that this is not a good idea and this is why:
- there will be a lot of them, and most of them will not take part in the division of rights, i.e. there will be a lot of trash
- if you allow to set complex dependencies, it may turn out that business logic will move from code to database.
Therefore, I decided to abandon the idea of ​​setting values ​​through the fields of models. If you need to set such a value, you will have to write helpers for attributes, i.e. for draft = true / false, do this:
is_draft (user, resource)
return resource.draft
end
is_not_draft (user, resource)
return! resource.draft
end
Separation of rights at the level of model fields
Those. some roles have access only to individual fields of the model. I left this question for later (maybe for the second article).
PS
The solution is only "on paper". During implementation, there may be pitfalls or better ideas. I publish an article so that hack readers can point out shortcomings in my approach and offer their ideas.
When writing an article I looked for inspiration here:
UPDI started responding to comments, thinking about the implementation and immediately found errors:
Bug work1. I suggested using routes as resources, but in fact, with this approach, you can solve only a very small set of tasks. This is the idea from the previous version of the article (draft). Maybe it is inspired by the term route-based acl. Or maybe an attempt to tighten the file system permissions system * nix on REST (it was a dead end).
Accordingly, this paragraph, about anything:
It should be possible to set a check for all controllers at once. That it is not necessary to put authorize in each controller. Depending on the technology, this can be done either in the base class of the controller or using the software layer (middleware).
Instead, you must be able to check that the authorize call is made in each controller. And if it is not done somewhere, then throw an exception.
It's so hard in every controller to make an authorize call. The situation can be alleviated for the classic REST, as it easily turns into CRUD models. Helpers can be made, but how to make them strongly depends on the implementation and on the PL.
** I will correct the article ** and remove the idea of ​​routes as resources, but for historical background I will leave here a list of all corrections.
2. There was also an idea about the interface for tracking where a user has this or that right and an interface for finding all users / groups that have this or that right.
3. I realized that I didn’t open the merits of this decision in the framework of MVC. It was necessary to explain that, with all the logic is stored in one place, and not smeared with a thin layer throughout the application.