Hello to all!
In projects based on Django, one often wants to use flexible access control at the level of records (objects) when different users have, or vice versa, do not have access to individual objects within the same model.
I want to tell you what kind of data access policy was required in our project, why a suitable ready-made system was not found and how a new access control system appeared at the record level.
For the most meticulous, the following are the details of the system, its internal logic and how to handle it.
For Jangi, there are already several entry-level access control systems. The most famous and stable systems such as Django-Guardian and Django-Authority .
The first of the systems, Django-Guardian , requires the creation of an untyped relationship (that is, records in the database) between the user and the object with which the user can interact. Each pair of user and object requires such a separate rights record. The number of these records in the database will be calculated as the product of the number of users and objects, the access rights to which are regulated in such a system.
It is easy to understand that in the case of an unlimited number of users and management objects in the project database, the number of rights records will grow quite quickly. Also, managing the deletion of these records when deleting users, a group change of rights records in the event of a change in the user's access area or moving an object from one user to another, made use of Django-Guardian in our project unlikely.
The second system, Django-Authority , tries to solve the problem of the first, establishing the relationship between the user and the managed object through common tags. Each such tag is an entry in the tag table associated with the user or control object. If tags with the same name are associated with a specific user and a specific object, we consider that this user has access to this control object.
The number of tag entries in this case will be substantially less and proportional to the sum of the number of users and objects, which is acceptable. However, in such a system will have to maintain a very unusual tag naming system. Practically, each such name will correspond to a certain "scope" (visibility for example). All objects belonging to this scope will have the corresponding tag, as well as users who have access to this scope.
The performance problem, which is not fundamentally solved in Django-Guardian, is quite tolerably solved in Django-Authority.
Unfortunately, the development of this system has been suspended for a long time. Integration with the admin control access according to the established relationship, it does not. We wanted to develop an application interface based on the admin panel, but using this system, we would still have to edit the admin panel anyway.
If you still have to edit the admin panel, why not make your own access control system?
Initially, our system was focused only on determining the visibility of objects, dividing their set "horizontally." The rights to control objects that were in the zone of visibility were distributed according to the traditional system of rights in Dzhanga, in accordance with their models (types) - “vertically”.
This separation worked until a certain point is quite acceptable, however, when it was necessary to distribute access "crosswise", it turned out that our system is too coarse. Really. Let we distribute access to objects of users. The user - the administrator of his group, may well edit and even delete a user record from this group. On the other hand, we would like users who are the administrators of their groups to be ordinary users of other groups. However, the admin has the same access to the records as soon as he sees them, no matter which group.
Thus, it became clear that “horizontally” it was necessary to control not only the visibility of objects, but also the entire spectrum of operations performed on them. Traditionally, 4 types of the most popular and commonly used actions on objects are defined, sometimes combined with the abbreviation CRUD (Create, Read, Update, Delete):
We need to regulate permissions on certain actions on subsets of objects. The most natural and effective way to manipulate specific subsets of objects in Jange is to use the QuerySet
. We will use it wherever we need to deal with a specific subset of objects.
However, QuerySet
does not describe one of the variants of the sets that we need: the set of all objects of this model, including all past and future objects . In fact, this set is determined by the model itself, and it is the only kind of set over which the “traditional” Django rights are defined. In fact: let's say we check permissions based on the QuerySet
. Having received an empty QuerySet
, we cannot be sure whether there are any objects in it due to the fact that we do not have enough rights to see any objects, or because the database has not yet formed such objects that we could would see.
Thus, we will define a set of objects over which rights are defined, either using the QuerySet
, defining a specific set of objects, or using a model, meaning all the objects of this model that have ever existed or were created in the future.
Actually, all of the above should be applied to the admin panel. It should show us a list of visible objects, allowing and forbidding them to add, edit or delete, depending on the rights set.
In order to change the behavior of already existing admins, it is necessary to make so that instead of (or in addition to) parts of their methods, code is called that takes into account the restrictions and permissions imposed by the new access control system. This is best done using the Mixin programming pattern , defining a class that, being at the top of the list of base classes, intercepts the method call from other base classes.
We still need to define rights not only over the subsets defined by the QuerySet
, but also over the set of all objects of a given type, defined by the model as such. Therefore, we will define the “traditional” Jungi rights model, based on Permission objects, as one of the possible ones , which may or may not be used in the project.
At first it seems that the best place to post information about the method of distribution of rights is the model. Our old system used the object manager for this, the thing in Jang, which serves to access the model objects and can be redefined if you insert it into the definition of the model class (the objects property).
However, this method, as it turned out, has several disadvantages.
First, the method of accessing the model object is a property not of the Django application (subsystem, which is often used unchanged from the installed package), but of the project as a whole. If the same application (package) is used in different projects, it is very likely that access to the model objects of this application will be defined in these projects differently.
Secondly, the definition of access rules can (and most often will) cross the boundaries of several applications (for example auth ). Being described in one of them, the definition may require unnecessary communication with another application (package).
Thus, the project should have its own, independent of individual applications, register of access rules for different objects of its applications (packages). This registry can be populated structured from different modules imported as models are used. Such a registry will contain the definition of access rules not only for its own models, but also models imported from all applications (packages) involved in the project.
Unsuccessful, too narrow, the solution to this problem led to unnecessary restrictions in existing packages.
Unlike other packages, we will describe the rights not with the help of some special models intended only for this purpose, but dynamically, with the help of the code applied to the models already existing in the project and the requests for them.
Fortunately, the code can be located not only in a method or function, but also in a lambda expression. We will use this method to describe the most obvious, simple, and frequently used access restriction rules.
Usually, access restrictions are made relative to the "current" user. It should not be forgotten, however, that the current user may not be the only element of the context for which access is restricted. It may well be that accessing will be affected by the “current venture”, the selected country, language, or any other factors that are relevant at the time of the decision to grant access.
Therefore, our code defining the access rules will receive as a context access restrictions, the entire request ( Request ). What exactly of this context is subject to restrictions, this code itself must decide.
The central class that determines the functionality of the system is the access manager - the managers.AccessManager
class. On the one hand, it allows you to register plug-in objects that define access restriction rules for various objects, and on the other hand, objects of this class are used to perform operations on determining rights regarding objects and sets when such a definition is required in the program.
Access restriction rules are created by constructing and registering plug-in objects.
The register/unregister_plugin(s)
manager class methods allow you to manipulate the plugin registry. No more than one plug-in for one model class is added to the registry. The register_plugins
method receives a dictionary in which the models serve as keys, and register_plugin
receives the model class and the plug-in object as separate parameters.
The helper class of the manager class get_default_plugin
returns the registered plugin by default, and plugin_for
searches for the plugin registered for the transferred class of the model. When searching for a plugin for a model, inheritance is taken into account, but classes that are not a model are excluded from the search. If the plugin for the model is not found, the default plugin is returned.
The predefined plug-in classes in the plugins
module include CompoundPlugin
to combine other plug-ins, plug-ins to dynamically define ApplyAblePlugin
and CheckAblePlugin
access restriction rules, and DjangoAccessPlugin
, which implements access restriction rules like the traditional ones, based on the analysis of django.contrib.auth.Permission
.
Dynamically defined attributes allow the AccessManager
methods check_something
and apply_something
, where something
is any valid name. This name serves as the name of an ability — the ability — that is requested from the system. For example, to obtain permissions for viewing (ability visible
), the methods check_visible
and appy_visible
.
The check_something
method gets the model and determines the ability limit on its relation, and the appy_something
method passes the QuerySet
and the method determines the limitations of our ability relative to the list of objects in this query.
The manager searches for the registered plug-in and requests from him, or from the plug-in by default, a similar method. The missing method means the permission of the requested actions with the specified ability with respect to all objects of the requested set. If a plugin is found, it checks.
Restricting access to the model as a whole is done using the plugin method with the check_
prefix. The model and the Request
object are passed to the method, determining the context of the rights check. If the method returns False, access is denied. To allow access, a dictionary is usually returned, which allows you to combine the returned values ​​when CompoundPlugin
processes them. This, somewhat unexpected, method of returning values ​​allows them to be used when requesting access to add check_appendable
: the fields whose names are mentioned in the returned combined dictionary are filled with values ​​taken from the dictionary from the newly created object.
Analysis of restriction of access to individual objects implies the imposition of filters on the QuerySet
query, leaving in it only those objects for which the specified access is allowed.
Such an overlay is performed by the plugin method with the prefix apply_
. The method is passed the QuerySet
and the Request
object, which defines the context of the rights check. The method imposes on the passed QuerySet
filters that limit the set of objects only to those that allow the specified access method for the specified context, and returns the filtered QuerySet
.
The system checks the following capabilities from the context in relation to the system objects:
appendable
- createvisible
- seechangeable
- changedeleteable
- deleteAt the same time, the ability of appendable
checked only for the model as a whole, using the check_appendable
method check_appendable
respectively, since checking for specific objects does not make sense: they have already been created.
The remaining abilities are tested both in relation to the model as a whole, and in relation to a specific list of objects. Total for standard checks, the following plugin methods are called if they are defined:
check_appendable
check_visible
apply_visible
check_changeable
apply_changeable
check_deleteable
apply_deleteable
Any application can construct an AccessManager
object and request that it check both standard and non-standard capabilities. To do this, the application requests a method with the prefix check_
or apply_
and the suffix corresponding to the requested ability.
If the method corresponding to the requested ability is not defined in the found plugin, it is considered available. The check_
method in this case returns an empty dictionary, and apply_
an unmodified QuerySet
.
The admin
module contains a special class AccessControlMixin
, which can be AccessControlMixin
to any class of standard jung admin admin. This class overrides the methods that participate in determining the order of access to objects, and restricts access in accordance with the rules of access restriction established for the project.
For constructing admins from scratch, the classes AccessModelAdmin
, AccessTabularInline
and AccessStackedInline
are also defined, which can be used in exactly the same way as their prototypes from Dzhangi. In essence, these classes are a pure combination of AccessControlMixin
and the corresponding class from Jangi.
To demonstrate the capabilities of the package, we use an example that is part of the package source code.
The example uses models from the standard package django.contrib.auth
, and also has its own additional application, someapp
, in which it defines two model classes:
SomeObject
managed from a separate ModelAdmin
, which refers to the editor_group
editors and many groups with read access - viewer_groups
SomeChild
which refers to SomeObject
and is controlled from InlineAdmin
The example defines the following access scheme:
Permission
are usedUser
object from each other, with the exception of a password and emailUser
's record about yourself is available to change, excluding the is_superuser
fieldPermission
rights are available only those that are relevant to this userSomeObject
objects and their SomeChild
subobjects SomeChild
available for reading to users of groups defined as viewer_groups
and for writing to users included in the editor_group
groupThe add access rule for a group also determines that when added, the group includes its creator. This is done to ensure that the newly created group is available to its creator after adding.
Thus, in the example, by using the functionality of the package and adding a minimum of additional code, a comfortable secure environment with controlled access is created, both to the individual functions of users and to the common space for them.
The functional definition of access restriction rules in the Django-Access package makes it easy to set complex arbitrary access control rules, avoiding the creation of additional entities that clutter up a project with code and the database with records.
Join the development of the project, look for bugs, create an issue. Pull requests are welcome.
Source: https://habr.com/ru/post/341496/
All Articles