Гибкая система управления доступом на уровне объектов-записей
- суббота, 4 ноября 2017 г. в 03:12:44
Привет всем!
В проектах, основанных на Джанго, часто хочется использовать гибкое управление доступом на уровне записей (объектов), когда разные пользователи имеют, или наоборот, не имеют доступ к отдельным объектам в рамках одной и той же модели.
Я хочу рассказать, какая именно политика доступа к данным требовалась в нашем проекте, почему не нашлось подходящей готовой системы и как появилась новая система управления доступом на уровне записей.
Для наиболее дотошных, далее приведены детали устройства системы, ее внутренней логики и порядка обращения с ней.
Для Джанги уже есть несколько систем управления доступом на уровне записей. Наиболее известны и стабильны такие системы, как Django-Guardian и Django-Authority.
Первая из систем, Django-Guardian, требует создания нетипизированных отношений (то есть записей в БД) между пользователем и объектом, с которым пользователь может взаимодействовать. Каждая пара пользователя и объекта требует такой отдельной записи о правах. Количество этих записей в базе будет исчисляться, как произведение количества пользователей и объектов, права доступа к которым регулируются в такой системе.
Легко понять, что в случае неограниченно большого количества пользователей и объектов управления в базе проекта, количество записей о правах будет расти довольно быстро. Также, управление удалением этих записей при удалении пользователей, групповое изменение записей о правах в случае изменения области доступа пользователя или перемещения объекта из области видимости одного пользователя в область видимости другого, сделало использование Django-Guardian в нашем проекте маловероятным.
Вторая система, Django-Authority, пытается решить проблему первой, устанавливая взаимосвязь между пользователем и управляемым объектом посредством общих тегов. Каждый такой тег — это запись в таблице тегов, связанная с пользователем или объектом управления. Если теги с одним и тем же именем связаны с конкретным пользователем и конкретным объектом, мы считаем, что этот пользователь имеет доступ к этому объекту управления.
Количество записей о тегах в таком случае, будет существенно меньше и пропорционально сумме количества пользователей и объектов, что приемлемо. Однако, в такой системе придется вести весьма неординарную систему именования тегов. Практически, каждое такое имя будет соответствовать некоторой "области действия" (видимости например). Все объекты, принадлежащие к этой области действия, будут иметь соответствующий тег, так же как и пользователи, имеющие доступ к этой области.
Проблема производительности, принципиально не решаемая в Django-Guardian, вполне сносно решена в Django-Authority.
К сожалению, развитие этой системы приостановилось уже давно. Интеграции с админкой, контролирующей доступ согласно установленным отношениям, в ней так и нет. Мы хотели развивать интерфейс приложения на основе админки, но используя эту систему, нам все равно пришлось бы править админку так или иначе.
Раз уж все равно придется править админку, почему бы не сделать свою собственную систему управления доступом?
Первоначально, наша система была ориентирована только на определение видимости объектов, разделяя их множество "по горизонтали". Права управления объектами, попавшими в зону видимости, распределялись согласно традиционной системе прав в Джанге, в соответствие с их моделями (типами) — "по вертикали".
Такое разделение работало до определенного момента вполне приемлемо, однако когда потребовалось распределять доступ "перекрестно", обнаружилось, что наша система слишком груба. Действительно. Пусть мы распределяем доступ к объектам пользователей. Пользователь — админ своей группы, вполне может отредактировать и даже удалить запись о пользователе из этой группы. С другой стороны, мы бы хотели, чтобы пользователи, которые являются админами своих групп, могли быть рядовыми пользователями других групп. Однако, админ имеет одинаковый доступ к записям, как только видит их, не важно, в какой группе.
Таким образом, стало ясно, что "по горизонтали" нужно управлять не только видимостью объектов, но и всем спектром операций, производимых над ними. Традиционно, определено 4 вида наиболее популярных и общеупотребительных действий над объектами, объединенных иногда аббревиатурой CRUD (Create, Read, Update, Delete):
Нам требуется регулирование разрешений на некоторые действия над подмножествами объектов. Наиболее естественный и эффективный способ манипулировать конкретными подмножествами объектов в Джанге — это использовать QuerySet
. Мы будем использовать его везде, где нам потребуется иметь дело с конкретным подмножеством объектов.
Тем не менее, QuerySet
не описывает один из вариантов множеств, который нам потребуется: множество всех объектов данной модели, включая все прошлые и будущие объекты. Фактически, это множество определяется самой моделью, и это единственная разновидность множеств, над которой определены "традиционные" права Джанго. В самом деле: допустим, что мы проверяем права доступа на основе QuerySet
. Получив пустой QuerySet
, мы не можем быть уверены, нет ли в нем объектов из за того, что у нас недостаточно прав, чтобы видеть хоть какие-нибудь объекты, или из за того, что в базе пока не образовалось таких объектов, которые мы могли бы увидеть.
Таким образом, мы будем определять множество объектов, над которыми определены права, либо с помощью QuerySet
, определяя конкретное множество объектов, либо с помощью модели, имея в виду все объекты этой модели, когда-либо существовавшие, или созданные в будущем.
Собственно, все вышесказанное нужно применить к админке. Она должна показывать нам список видимых объектов, разрешая и запрещая их добавлять, редактировать или удалять в зависимости от установленных прав.
Для того, чтобы поменять поведение уже существующих админок, придется сделать так, чтобы вместо (или дополнительно к) части из их методов, вызывался код, учитывающий ограничения и разрешения, накладываемые новой системой управления доступом. Лучше всего это делается с применением шаблона программирования Mixin, определяя класс, который находясь в начале списка базовых классов, перехватывает вызов метода у других базовых классов.
Мы все равно должны определять права не только над подмножествами, определяемыми 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. Пулл реквесты приветствуются.