Первоначально опубликовано в блоге Software Development Hub.
Вячеслав Буханцов, генеральный директор Software Development Hub, архитектор программного обеспечения, эксперт в построении высоконагруженных CRM/ERP систем, серверных и веб-решений, делится своим подходом к созданию пользовательских прав доступа на Django.
Фреймворк Django имеет встроенный механизм разрешений, который подойдет для большинства простых проектов. Я могу сказать, что это идеальное решение для встроенного интерфейса администратора. Но что если права доступа к проекту должны быть реализованы более сложным способом?
В этой статье я поделюсь опытом нашей команды и приведу несколько примеров использования пользовательского механизма прав доступа. Наш пример не будет мешать встроенным правам доступа Django. К статье прилагается полный проект Django с реализацией. Для упрощения представления кода я намеренно удалил строки, относящиеся к активному кэшированию запросов и оптимизации производительности.
Демонстрационный проект на GitHub также будет полезен для изучения начинающим разработчикам.
Наши методы работы с группами и разрешениями Django
Мы начали использовать первую версию описанного подхода, начиная с версии Django 1.2, на проекте под названием Billing platform for ISP. Требованием проекта с точки зрения безопасности было разграничение прав доступа пользователей между различными компаниями. Приведенное ниже резюме дает более подробную информацию:
- В системе может быть создано несколько компаний
- Существует только одна запись для пользователя (т.е. пользователь имеет один логин и пароль во всех компаниях)
- Права доступа в разных компаниях независимы, например, пользователь A в компании A является менеджером и может изменять данные клиента A (принадлежит компании A), а пользователь A в компании B имеет доступ ReadOnly к клиенту B (принадлежит клиенту B).
- Как можно понять из описания требований выше, нарушение прав доступа должно быть выполнено на уровне объекта.
Для наглядности приведу краткое описание моделей:
class Company(models.Model):
name = models.CharField(max_length=255)
class Meta:
db_table = 'company'
class Customer(models.Model):
company = models.ForeignKey(Company, on_delete=models.PROTECT)
name = models.CharField(max_length=255)
class Meta:
db_table = 'customer'
В проекте эти модели для удобства разделены по разным модулям. Примем, что описание разрешения будет состоять из двух частей: названия модуля и описания действия пользователя, которым разрешение будет управлять:
Модуль |
Действие |
Описание |
компания |
читать |
Просмотр объекта Компания |
компания |
создать |
Создание объекта Компания |
компания |
обновить |
Изменение объекта Компания |
компания |
удалить |
Удаление объекта Компания |
клиент |
читать |
Просмотр объекта Клиент |
клиент |
создать |
Создание объекта Customer |
заказчик |
обновить |
Изменение объекта Заказчик |
клиент |
удалить |
Удаление объекта Клиент |
клиент |
проверить |
Верификация объекта Клиент |
клиент |
активировать |
Активация объекта Customer |
Согласно таблице, некоторые действия являются «классическим» CRUD, но только для отображения; как реальные бизнес-приложения, так и система разрешений более сложны; для модуля Customer были добавлены две дополнительные функции: верификация и активация.
Таким образом, при построении механизма прав мы не ограничиваемся моделью CRUD, но сможем контролировать действия пользователей с необходимой гранулярностью.
Для упрощения кода права будут назначаться только группам. В реальных проектах иногда требуется выдавать права на отдельные действия на уровне пользователя.
Двухуровневая структура прав доступа
Двухуровневая структура прав будет описана следующими моделями:
class AccessModule(models.Model):
name = models.CharField(max_length=128)
slug = models.CharField(max_length=128, unique=True)
class Meta:
db_table = 'access_module'
class AccessPermission(models.Model):
module = models.ForeignKey(AccessModule, on_delete=models.PROTECT)
name = models.CharField(max_length=128)
slug = models.CharField(max_length=128)
groups = models.ManyToManyField(Group)
class Meta:
db_table = 'access_permission'
unique_together = (('module', 'slug'),)
Модель AccessPermission имеет гиперссылку на группу. Таким образом, определяется список групп, которым разрешено данное разрешение.
В проекте ERP-платформы такие модели могут быть представлены следующим интерфейсом конфигурации:
В результате представление разделов и прав доступа по группам пользователей выглядит гибким и удобным с точки зрения функциональности. Необходимо лишь добавить модель, которая будет описывать, к какой из групп принадлежит пользователь в разных компаниях:
class AccessGroup(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
group = models.ForeignKey(Group)
company = models.ForeignKey(Company)
class Meta:
db_table = 'access_group'
unique_together = ('user', 'group', 'company')
Объект HttpRequest присутствует во многих контекстах: его можно найти в View, он может быть передан в контекст шаблона. Информация об авторизованном пользователе также доступна в request.user
Поэтому было бы удобно добавить доступ к проверке прав через Request. Для этого было разработано промежуточное ПО, это ленивый объект к Acl.
Например, можно использовать следующую конструкцию:
def get_queryset(self):
qs = super().get_queryset()
qs = qs.filter(company__in=self.request.acl.get_companies('customer', 'view'))
return qs
В приведенном примере показана фильтрация списка клиентов только для тех компаний, на которые у пользователя есть права.
Настоятельно рекомендуется просмотреть пример CustomerListView из проекта.
В этой статье я не затронул тему активного кэширования запросов по правам. Как видно из кода, добавить кэширование несложно, если составить ключ кэширования следующим образом:
::
В моих следующих работах будет выполнен пример и описание использования данного движка прав в сочетании с Django Rest Framework, а также будет представлено автодокументирование прав в связке с drf-spectacular.
Читайте также:
Как работает SDH в военное время? Отвечает Вячеслав Буханцов, генеральный директор