📜 ⬆️ ⬇️

Flexible access control at the record object level

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 those who can not wait


Django-Access Project


Existing Systems


For Jangi, there are already several entry-level access control systems. The most famous and stable systems such as Django-Guardian and Django-Authority .


Django-guardian


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.


Django-authority


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?


What is required from the new system


What rights need to be distributed


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):



Sets over which rights are defined


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.


What will have to change


Admin panel


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.


Traditional Permission System


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.


Where rights should be described


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.


How to describe rights


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.


Context of the execution of 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.


Class structure of the system


Access manager


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.


Creating access restriction rules


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 .


Access restriction check


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


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.


Restricting access to individual objects


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 .


Standard checks


The system checks the following capabilities from the context in relation to the system objects:



At 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:



Custom checks


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 .


Admin panel


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.


Example


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:



The example defines the following access scheme:



The 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.


Conclusion


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