Привет всем!


В проектах, основанных на Джанго, часто хочется использовать гибкое управление доступом на уровне записей (объектов), когда разные пользователи имеют, или наоборот, не имеют доступ к отдельным объектам в рамках одной и той же модели.


Я хочу рассказать, какая именно политика доступа к данным требовалась в нашем проекте, почему не нашлось подходящей готовой системы и как появилась новая система управления доступом на уровне записей.


Для наиболее дотошных, далее приведены детали устройства системы, ее внутренней логики и порядка обращения с ней.


Для тех, кому не терпится


Проект Django-Access


Существующие системы


Для Джанги уже есть несколько систем управления доступом на уровне записей. Наиболее известны и стабильны такие системы, как Django-Guardian и Django-Authority.


Django-Guardian


Первая из систем, Django-Guardian, требует создания нетипизированных отношений (то есть записей в БД) между пользователем и объектом, с которым пользователь может взаимодействовать. Каждая пара пользователя и объекта требует такой отдельной записи о правах. Количество этих записей в базе будет исчисляться, как произведение количества пользователей и объектов, права доступа к которым регулируются в такой системе.


Легко понять, что в случае неограниченно большого количества пользователей и объектов управления в базе проекта, количество записей о правах будет расти довольно быстро. Также, управление удалением этих записей при удалении пользователей, групповое изменение записей о правах в случае изменения области доступа пользователя или перемещения объекта из области видимости одного пользователя в область видимости другого, сделало использование Django-Guardian в нашем проекте маловероятным.


Django-Authority


Вторая система, Django-Authority, пытается решить проблему первой, устанавливая взаимосвязь между пользователем и управляемым объектом посредством общих тегов. Каждый такой тег — это запись в таблице тегов, связанная с пользователем или объектом управления. Если теги с одним и тем же именем связаны с конкретным пользователем и конкретным объектом, мы считаем, что этот пользователь имеет доступ к этому объекту управления.


Количество записей о тегах в таком случае, будет существенно меньше и пропорционально сумме количества пользователей и объектов, что приемлемо. Однако, в такой системе придется вести весьма неординарную систему именования тегов. Практически, каждое такое имя будет соответствовать некоторой "области действия" (видимости например). Все объекты, принадлежащие к этой области действия, будут иметь соответствующий тег, так же как и пользователи, имеющие доступ к этой области.


Проблема производительности, принципиально не решаемая в Django-Guardian, вполне сносно решена в Django-Authority.


К сожалению, развитие этой системы приостановилось уже давно. Интеграции с админкой, контролирующей доступ согласно установленным отношениям, в ней так и нет. Мы хотели развивать интерфейс приложения на основе админки, но используя эту систему, нам все равно пришлось бы править админку так или иначе.


Раз уж все равно придется править админку, почему бы не сделать свою собственную систему управления доступом?


Что требуется от новой системы


Какие права нужно распределять


Первоначально, наша система была ориентирована только на определение видимости объектов, разделяя их множество "по горизонтали". Права управления объектами, попавшими в зону видимости, распределялись согласно традиционной системе прав в Джанге, в соответствие с их моделями (типами) — "по вертикали".


Такое разделение работало до определенного момента вполне приемлемо, однако когда потребовалось распределять доступ "перекрестно", обнаружилось, что наша система слишком груба. Действительно. Пусть мы распределяем доступ к объектам пользователей. Пользователь — админ своей группы, вполне может отредактировать и даже удалить запись о пользователе из этой группы. С другой стороны, мы бы хотели, чтобы пользователи, которые являются админами своих групп, могли быть рядовыми пользователями других групп. Однако, админ имеет одинаковый доступ к записям, как только видит их, не важно, в какой группе.


Таким образом, стало ясно, что "по горизонтали" нужно управлять не только видимостью объектов, но и всем спектром операций, производимых над ними. Традиционно, определено 4 вида наиболее популярных и общеупотребительных действий над объектами, объединенных иногда аббревиатурой CRUD (Create, Read, Update, Delete):


  • создавать
  • видеть
  • изменять
  • удалять

Множества, над которыми определены права


Нам требуется регулирование разрешений на некоторые действия над подмножествами объектов. Наиболее естественный и эффективный способ манипулировать конкретными подмножествами объектов в Джанге — это использовать QuerySet. Мы будем использовать его везде, где нам потребуется иметь дело с конкретным подмножеством объектов.


Тем не менее, QuerySet не описывает один из вариантов множеств, который нам потребуется: множество всех объектов данной модели, включая все прошлые и будущие объекты. Фактически, это множество определяется самой моделью, и это единственная разновидность множеств, над которой определены "традиционные" права Джанго. В самом деле: допустим, что мы проверяем права доступа на основе QuerySet. Получив пустой QuerySet, мы не можем быть уверены, нет ли в нем объектов из за того, что у нас недостаточно прав, чтобы видеть хоть какие-нибудь объекты, или из за того, что в базе пока не образовалось таких объектов, которые мы могли бы увидеть.


Таким образом, мы будем определять множество объектов, над которыми определены права, либо с помощью QuerySet, определяя конкретное множество объектов, либо с помощью модели, имея в виду все объекты этой модели, когда-либо существовавшие, или созданные в будущем.


Что придется изменить


Админка


Собственно, все вышесказанное нужно применить к админке. Она должна показывать нам список видимых объектов, разрешая и запрещая их добавлять, редактировать или удалять в зависимости от установленных прав.


Для того, чтобы поменять поведение уже существующих админок, придется сделать так, чтобы вместо (или дополнительно к) части из их методов, вызывался код, учитывающий ограничения и разрешения, накладываемые новой системой управления доступом. Лучше всего это делается с применением шаблона программирования Mixin, определяя класс, который находясь в начале списка базовых классов, перехватывает вызов метода у других базовых классов.


Традиционная система Permission


Мы все равно должны определять права не только над подмножествами, определяемыми QuerySet, но и над множеством всех объектов данного типа, определяемым моделью как таковой. Поэтому мы определим "традиционную" модель прав Джанги, основанную на объектах Permission, как одну из возможных, которая может быть использована (а может и не быть использована) в проекте.


Где должны быть описаны права


Поначалу кажется, что наилучшим местом для размещения информации о способе распределения прав, является модель. Наша старая система использовала для этого менеджер объектов, ту штуку в Джанге, которая служит для доступа к объектам модели и может быть переопределена, если вставить ее в определение класса модели (свойство objects).


Однако у такого способа, как выяснилось, есть ряд недостатков.


Во-первых, способ доступа к объекту модели — это свойство не приложения Джанго (подсистемы, которая часто используется в неизменном виде из установленного пакета), а проекта в целом. Если одно и то же приложение (пакет) используется в разных проектах, весьма вероятно, что доступ к объектам моделей этого приложения будет определен в этих проектах по разному.


Во-вторых, определение правил доступа может (и чаще всего будет) пересекать границы нескольких приложений (например auth). Будучи описанным в одном из них, определение может потребовать ненужной связи с другим приложением (пакетом).


Таким образом, проект должен иметь свой, не зависящий от отдельных приложений, реестр правил доступа к разным объектам своих приложений (пакетов). Этот реестр может заполняться структурированно из разных модулей, импортируемых по мере использования моделей. Такой реестр будет содержать определение правил доступа не только для собственных моделей, но и моделей, импортированных из всех приложений (пакетов), задействованных в проекте.


Как описывать права


Неудачное, чересчур узкое, решение этой задачи привело к ненужным ограничениям в существующих пакетах.


В отличие от других пакетов, мы будем описывать права не с помощью каких-то специальных, предназначенных только для этой цели, моделей, а динамически, с помощью кода, применяемого к уже существующим в проекте моделям и запросам к ним.


К счастью, код может быть расположен не только в методе или функции, но и в лямбда-выражении. Мы воспользуемся этим способом для описания наиболее очевидных, простых и часто используемых правил ограничения доступа.


Контекст выполнения правил ограничения доступа


Обычно, ограничение доступа производится относительно "текущего" пользователя. Не следует забывать однако, что текущий пользователь может быть не единственным элементом контекста, для которого ограничивается доступ. Вполне может быть, что на получение доступа повлияет "текущее предприятие", выбранная страна, язык или любые другие факторы, актуальные в момент принятия решения о предоставлении доступа.


Поэтому наш код, определяющий правила доступа, будет получать в качестве контекста ограничения доступа, весь запрос (Request). Что именно из этого контекста является субъектом ограничений, должен решать сам этот код.


Структура классов системы


Менеджер доступа


Центральным классом, определяющим функционал системы, является менеджер доступа — класс managers.AccessManager. С одной стороны, он позволяет зарегистрировать объекты плагинов, определяющие правила ограничения доступа для различных объектов, а с другой стороны, объекты этого класса используются для выполнения операций по определению прав относительно объектов и множеств, когда такое определение требуется в программе.


Создание правил ограничения доступа


Правила ограничения доступа создаются путем конструирования и регистрации объектов плагинов.


Методы класса менеджера register/unregister_plugin(s) позволяют манипулировать реестром плагинов. В реестр добавляется не более одного плагина для одного класса модели. Метод register_plugins получает словарь, в котором ключами служат модели, а register_plugin получает класс модели и объект плагина как отдельные параметры.


Вспомогательный метод класса менеджера get_default_pluginвозвращает зарегистрированный плагин по умолчанию, а plugin_for ищет плагин, зарегистрированный для переданного класса модели. При поиске плагина для модели, учитывается наследование, но из поиска исключаются классы, не являющиеся моделью. Если плагин для модели не найден, возвращается плагин по умолчанию.


Предопределенные классы плагинов в модуле plugins включают в себя CompoundPlugin для комбинирования других плагинов, плагины для динамического определения правил ограничения доступа ApplyAblePlugin и CheckAblePlugin, а также DjangoAccessPlugin, реализующий правила ограничения доступа, подобные традиционным, основанные на анализе объектов django.contrib.auth.Permission.


Проверка ограничения доступа


Динамически определенные атрибуты позволяют вызвать у менеджера доступа AccessManager методы check_something и apply_something, где something — любое допустимое имя. Это имя служит именем способности — ability — которая запрашивается у системы. Например, для получения прав на просмотр (способность visible), запрашиваются методы check_visible и appy_visible.


Метод check_something получает модель и определяет ограничение способности в ее отношении, а методу appy_something передается QuerySet и метод определяет ограничения нашей способности относительно списка объектов в этом запросе.


Менеджер ищет зарегистрированный плагин и запрашивает у него, либо у плагина по умолчанию, аналогичный метод. Отсутствующий метод означает разрешение запрошенных действий с указанной способностью в отношение всех объектов запрошенного множества. Если плагин найден, он и осуществляет проверку.


Ограничение доступа к модели в целом


Ограничение доступа к модели в целом производится методом плагина с префиксом check_. Методу передается модель и объект Request, определяющий контекст проверки прав. Если метод возвращает False, доступ запрещен. Для разрешения доступа, обычно возвращается словарь, что позволяет комбинировать возвращенные значения, когда их обрабатывает CompoundPlugin. Такой, несколько неожиданный, способ возврата значений, позволяет использовать их при запросе доступа на добавление check_appendable: поля, имена которых упомянуты в возвращенном скомбинированном словаре, заполняются значениями, взятыми из словаря, у вновь создаваемого объекта.


Ограничение доступа к отдельным объектам


Анализ ограничения доступа к отдельным объектам подразумевает наложение на запрос QuerySet фильтров, оставляющих в нем только те объекты, для которых указанный доступ разрешен.


Такое наложение выполняется методом плагина с префиксом apply_. Методу передается QuerySet и объект Request, определяющий контекст проверки прав. Метод накладывает на переданный QuerySet фильтры, ограничивающие множество объектов только теми, которые допускают указанный способ доступа для указанного контекста, и возвращает отфильтрованный QuerySet.


Стандартные проверки


В системе осуществляется проверка следующих способностей со стороны контекста в отношении объектов системы:


  • appendable — создавать
  • visible — видеть
  • changeable — изменять
  • deleteable — удалять

При этом, способность appendable проверяется только в отношении модели в целом, методом check_appendable соответственно, поскольку проверка в отношении конкретных объектов не имеет смысла: они уже созданы.


Остальные способности проверяются как в отношение к модели в целом, так и в отношение к конкретному списку объектов. Итого, для стандартных проверок, вызываются следующие методы плагинов, если они определены:


  • check_appendable
  • check_visible
  • apply_visible
  • check_changeable
  • apply_changeable
  • check_deleteable
  • apply_deleteable

Нестандартные проверки


Любое приложение может сконструировать объект AccessManager и запросить у него проверку как стандартных, так и нестандартных способностей. Для этого, приложение запрашивает метод с префиксом check_ или apply_ и суффиксом, соответствующим запрошенной способности.


Если метод, соответствующий запрошенной способности, не определен в найденном плагине, она считается доступной. Метод check_ в этом случае, возвращает пустой словарь, а apply_ — неизмененный QuerySet.


Админка


Модуль admin содержит специальный класс AccessControlMixin, который можно подмешивать к любому классу стандартной джанговской админки. Этот класс переопределяет методы, которые участвуют в определении порядка доступа к объектам, и ограничивает доступ в соответствие с правилами ограничения доступа, установленными для проекта.


Для конструирования админок с нуля, также определены классы AccessModelAdmin, AccessTabularInline и AccessStackedInline, которые можно использовать в точности так же, как их прототипы из Джанги. По сути, эти классы являются чистой комбинацией AccessControlMixin и соответствующего класса из Джанги.


Пример


Для демонстрации возможностей пакета, воспользуемся примером, входящим составной частью в исходный код пакета.


Пример использует модели из стандартного пакета django.contrib.auth, а также имеет собственное дополнительное приложение someapp, в котором определяет два класса модели:


  • SomeObject управляемый из отдельного ModelAdmin, который ссылается на группу редакторов editor_group и множество групп, имеющих доступ на чтение — viewer_groups
  • SomeChild который ссылается на SomeObject и управляется из InlineAdmin

Пример определяет следующую схему доступа:


  • суперпользователь может все
  • используются обычные Permission Джанги
  • все пользователи могут читать, если разрешено, все характеристики объекта User друг у друга, за исключением пароля и электронной почты
  • запись User о себе самом доступна на изменение, исключая поле is_superuser
  • группы Group и права Permission доступны только те, которые имеют отношение к данному пользователю
  • объекты SomeObject и их подобъекты SomeChild доступны для чтения пользователям групп, определенных как viewer_groups и для записи пользователям, включенным в группу editor_group

Правило доступа на добавление для группы определяет также, что при добавлении, в группу входит ее создатель. Это делается для того, чтобы вновь созданная группа была доступна для ее создателя после добавления.


Таким образом, в примере, посредством использования функциональности пакета и добавления минимума дополнительного кода, создана комфортная безопасная среда с контролируемым доступом, как к отдельным функциям пользователей, так и к общему для них пространству.


Заключение


Функциональное определение правил ограничения доступа в пакете Django-Access позволяет легко устанавливать сложные произвольные правила разграничения доступа, избегая создания дополнительных сущностей, загромождающих проект кодом, а базу — записями.


Присоединяйтесь к развитию проекта, ищите баги, создавайте issue. Пулл реквесты приветствуются.

Комментарии (10)


  1. SerhiyRomanov
    03.11.2017 19:22

    Спасибо за Вашу работу.
    Если ли какие нибудь планы по поводу будущего функционала проекта?


    1. nnseva Автор
      03.11.2017 19:25

      да, прикручу tastypie обязательно — это требуется в проекте — возможно, и другие системы конструирования api. Посмотрим, как пойдет.


  1. ivlevdenis_ru
    03.11.2017 19:22

    Это же нужно при каждом изменении логики задания прав править код :(
    Жаль нет реализации ABAC для django…


    1. nnseva Автор
      03.11.2017 19:29

      не совсем понял. Это же зависит от кода правил, который вы сами и пишете. Если он достаточно гибок, то ничего править не придется. Приведите пример, пожалуйста.


      1. ivlevdenis_ru
        03.11.2017 20:03

        Да, как пример я привел отсутствие ABAC. Чего хочется?.. Да простого (это как сказать :)) создания динамических правил по атрибутам. Вот статья про разницу между ABAC и RBAC. Вот именно что не хочется писать код для задания привилегий.


        1. nnseva Автор
          03.11.2017 20:55

          Во первых, то, что называется «бизнес-правилами», в любом случае задается кодом, тем более, если вы хотите анализировать атрибуты для принятия решения о доступе.
          Во вторых, описанная система не ограничивает разработчика в сложности кода, регулирующего доступ.

          Так, в примере, доступ к объекту SomeObject зависит от того, принадлежит ли пользователь к группе редакторов или просмотрщиков. В данном случае, бизнес-правило было установлено таким образом.

          Система позволяет разработчику устанавливать практически любые, сколь угодно сложные, бизнес-правила для определения доступа к объектам. Фактически, задачей разработчика правил, является установление фильтра для списка объектов, на основании контекста, задаваемого объектом Request.

          Да, для установления бизнес-правил в нашей системе используется код.

          Однако, я прошу обратить внимание на DjangoAccessPlugin. Используя систему динамических правил, мы сделали плагин, реализующий обратно совместимый контроль доступа, с помощью 7 строк кода.

          Думаю, желающие смогут легко прикрутить код плагина, ориентирующегося на хранимые правила, подобные описанным в приведенной статье.

          upd да, если вы не поняли, приведенные в статье правила тоже являются кодом


  1. Sovetnikov
    04.11.2017 09:15

    Так уже есть Django-rules https://github.com/dfunckt/django-rules, который как раз предоставляет возможность управления доступами используя правила заданные в коде.
    rules + guardian дают гибкую связку для доступов, просто не надо в guardian задавать явный доступ на все имеющиеся объекты.


    Из статьи кажется, что ваш проект может управлять доступом на уровне полей модели, хотя это не так (поправьте, может ошибаюсь). В вашем примере, на странице проекта и в репозитории, вы используете стандартный рецепт с модифицированными get_list_display и get_fieldsets в UserAdmin именно для вашего случая:


        def get_fieldsets(self, request, obj=None):
            fieldsets = list(super(AccessUserAdmin, self).get_fieldsets(request, obj)) or []
            if request.user.is_superuser:
                return fieldsets
            if not obj:
                return fieldsets
            if obj.pk != request.user.pk:
                return self._fieldsets_exclude(fieldsets,['password', 'email'])
            return self._fieldsets_exclude(fieldsets,['is_superuser'])

    И зачем нужен AccessManager, когда джанго поддерживает в своём API проверку доступа на уровне объектов? Как в User.has_perm так и в has_perm для бэкендов аутентификации и авторизации (с версии Django 1.7).
    https://docs.djangoproject.com/en/1.11/ref/contrib/auth/#django.contrib.auth.models.User.has_perm
    https://docs.djangoproject.com/en/1.11/ref/contrib/auth/#django.contrib.auth.backends.ModelBackend.has_perm


    1. nnseva Автор
      04.11.2017 09:40

      Да, Rules — была бы отличной системой, если бы позволяла выполнять запросы, отфильтровывающие разрешенные объекты. К сожалению, Rules позволяет только протестировать конкретный объект. Это же касается has_perm, возвращающей результат только для конкретного объекта.

      Представьте себе базу, содержащую тысячу пользователей, каждый из которых управляет сотней объектов какой-нибудь модели. Вам нужно показывать пользователю только те объекты, которые он может видеть и которыми может управлять. Пользуясь Rules, вам придется получить весь список из 100000 объектов, а потом протестировать каждый из них на право доступа текущего пользователя. Это не эффективно, вы конечно, не будете так делать и вам придется писать фильтр, определяющий список доступных объектов — то есть, делать ровно то, что уже сделано в Access.

      Попробуйте интегрировать Rules в админку, вы увидите, что она — сама по себе — не позволяет эффективно ограничить списки объектов.

      Вы правы, Access (пока?) не позволяет контролировать доступ на уровне полей, такой контроль сделан в примере для того, чтобы обеспечить полноту защиты доступа при установленных с помощью Access правилах. Одновременно, пример показывает простоту интеграции кастомных админок и Access.

      upd да, спасибо, что напомнили про Rules, в ней реализовано несколько полезных техник Джанго, которые можно было бы внедрить и в Access.


  1. Sovetnikov
    04.11.2017 09:47
    +1

    Итого в вашем решении есть плюсы, которых нет в обозначенных готовых решениях:
    1. Реализован доступ «Видеть» с поддержкой в админке (чего я не нашел в своё время в уже готовых решениях).
    2. Унифицировано решение для фильтрации объектов в ModelAdmin.changelist_view на основе доступов (через ModelAdmin.get_query_set).

    Но настораживает ваше решение сделать свой слой управления доступами поверх стандартного механизма Джанго, механизм управления доступами Джанго подключается к вашему, а не наоборот. Это может привести к сложностями при использовании других готовых решений для Джанго… если стороннее приложение будет проверять доступ через has_perm Джанго, оно ведь пролетит мимо доступа заданного правилами в вашем решении?


    1. nnseva Автор
      04.11.2017 10:08

      вы правы, нужно пофиксить стандартный has_perm.

      Фактически, модифицированная админка устраняет исполнение всего кода, связанного с has_perm, из админки, но как вы правильно отметили, есть и другие пакеты. Спасибо!