Права и группы в Django: своя система доступа
Стандартных прав add/change/delete/view хватает для админки, но ломается на бизнес-логике. Разбираем, как построить свою систему доступа в Django 5.x без сторонних пакетов.

Стандартная система прав в Django создаёт для каждой модели четыре разрешения: add, change, delete, view. Этого хватает, пока доступ решается на уровне «может редактировать заказы вообще». Как только появляется «менеджер видит только свои заказы» или «редактор публикует, но не удаляет чужие черновики» — четырёх прав на модель не хватает, и в коде расползаются проверки вида if request.user.is_staff and request.user.groups.filter(...). Разберём, как строить доступ в Django 5.x штатными средствами: где заканчиваются встроенные права на модель и начинаются права на объект, как завести собственные разрешения и когда пора выносить логику в отдельный слой.
Речь про Django 5.x с приложениями django.contrib.auth и django.contrib.contenttypes в INSTALLED_APPS — это конфигурация по умолчанию. Сторонние пакеты вроде django-guardian здесь не рассматриваем: сначала стоит понять, что даёт коробка. Object-level права из коробки Django не хранит — это отдельная тема, её обозначим, но реализацию через сторонний бэкенд разворачивать не будем.
Что уже есть в коробке
При миграции для каждой модели Django создаёт четыре записи в таблице auth_permission. Проверка идёт через user.has_perm('app_label.codename'), где codename — это add_order, view_order и так далее. Группа (Group) — это просто набор прав; пользователь наследует все права всех своих групп плюс права, назначенные лично.
Ключевой момент, о который спотыкаются: суперпользователь всегда возвращает True для любого has_perm, даже если такого права не существует. Это удобно и опасно одновременно — на суперюзере вы никогда не заметите, что забыли выдать право обычному сотруднику.
Проверять доступ надо на пользователе без флагов is_superuser и is_staff. Иначе вы тестируете не систему прав, а обход системы прав.
Три подхода к своей системе доступа
Когда встроенных четырёх прав не хватает, есть три пути. У каждого своя цена.
| Подход | Что выигрываете | Чем платите |
|---|---|---|
| Кастомные permissions в Meta модели | Права видны в админке, работают через has_perm, хранятся в БД | Только глобальные, без привязки к объекту; при удалении права нужна миграция |
| Группы как роли + набор прав | Роль меняется в одном месте, пользователь просто в группе | Роль всё ещё глобальная: «менеджер» видит всех, а не своих |
| Свой слой проверки (функции/сервис) | Логика на объект: свои заказы, статус, владелец | Не отображается в админке, надо самому звать в каждой вьюхе |
Свои права: объявляем в модели
Дополнительные права, не связанные с CRUD, объявляются в Meta.permissions. Формат — список кортежей (codename, human_readable_name).
# models.py — Django 5.x
from django.db import models
from django.conf import settings
class Order(models.Model):
STATUS_CHOICES = [
("draft", "Черновик"),
("paid", "Оплачен"),
("shipped", "Отгружен"),
]
manager = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name="orders",
)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="draft")
total = models.DecimalField(max_digits=12, decimal_places=2)
class Meta:
permissions = [
# Разрешение сверх стандартных CRUD-прав
("can_refund_order", "Может оформлять возврат по заказу"),
("can_view_all_orders", "Видит заказы всех менеджеров"),
]
После этого нужна миграция — права создаются не автоматически при старте, а на migrate:
python manage.py makemigrations
python manage.py migrate
Теперь user.has_perm('shop.can_refund_order') вернёт корректное значение. Codename в базе хранится без префикса приложения, но в has_perm его передают с префиксом app_label.
Группы как роли
Раздавать права по одному каждому сотруднику — путь в ад при найме. Роль удобнее собрать в группу и назначать группу. Создать группы можно data-миграцией, чтобы они появлялись на любом окружении одинаково.
# Внутри data-миграции: RunPython
from django.contrib.auth.models import Group, Permission
def create_roles(apps, schema_editor):
# Берём модели через apps, а не импортом — так безопаснее в миграциях
manager, _ = Group.objects.get_or_create(name="Менеджер")
supervisor, _ = Group.objects.get_or_create(name="Старший менеджер")
# codename берём без префикса приложения
base_perms = Permission.objects.filter(
codename__in=["add_order", "change_order", "view_order"]
)
manager.permissions.set(base_perms)
extra = Permission.objects.filter(
codename__in=["can_refund_order", "can_view_all_orders"]
)
supervisor.permissions.set(list(base_perms) + list(extra))
Пользователю дальше назначают группу — правами он не управляет напрямую. Меняется набор прав роли — меняется у всех сразу.
Где встроенные права ломаются: доступ на объект
Право can_view_all_orders глобально: либо видишь все заказы, либо ни одного. «Менеджер видит только свои» так не выражается — это условие на конкретный объект, а Django из коробки object-level permissions не хранит.
Здесь начинается свой слой. Простой и предсказуемый вариант — функции проверки, которые принимают пользователя и объект и возвращают bool. Не прячьте эту логику по вьюхам — соберите в одном месте.
# permissions.py
def can_edit_order(user, order):
# Старший менеджер с глобальным правом — редактирует любой заказ
if user.has_perm("shop.can_view_all_orders"):
return True
# Обычный менеджер — только свой и только пока черновик
return order.manager_id == user.id and order.status == "draft"
Во вьюхе на классах это удобно повесить в один метод, а не размазывать по get/post:
# views.py — Django 5.x, CBV
from django.core.exceptions import PermissionDenied
from django.views.generic import UpdateView
from .models import Order
from .permissions import can_edit_order
class OrderUpdateView(UpdateView):
model = Order
fields = ["status", "total"]
def get_object(self, queryset=None):
order = super().get_object(queryset)
# Проверка на объект — до отдачи формы и до сохранения
if not can_edit_order(self.request.user, order):
raise PermissionDenied
return order
Отдельно фильтруйте выборку в get_queryset, иначе менеджер увидит чужие заказы в списке, даже если не сможет их открыть:
def get_queryset(self):
qs = super().get_queryset()
if self.request.user.has_perm("shop.can_view_all_orders"):
return qs
return qs.filter(manager=self.request.user)
Проверка прав и фильтрация queryset — разные задачи. Первая отвечает «можно ли трогать этот объект», вторая — «какие объекты вообще показывать». Забудете вторую — получите утечку данных в списках и API.
Кеш прав: почему изменение не подхватывается
Django кеширует права пользователя на время жизни объекта User в рамках запроса. Если вы в одном запросе выдали право и тут же проверяете has_perm на том же объекте — увидите старое значение. Сбросить кеш можно, перечитав пользователя из базы или удалив кеш-атрибуты. В обычном веб-запросе это не проблема (новый запрос — новый объект), но в тестах и командах управления ловится регулярно.
Когда что выбирать
- Глобальное право (может/не может в принципе) — кастомный permission в Meta и группа-роль. Пример: «доступ к разделу отчётов».
- Право на объект (только свои, только в статусе X) — свой слой проверки функциями. Встроенные права тут бессильны.
- Сложная матрица объект + роль + условие, которую нужно менять без деплоя — пора смотреть в сторону django-guardian или хранения политик в БД. Это выходит за рамки коробки.
Что делать дальше
Сначала закройте базовое: заведите группы-роли data-миграцией, чтобы они были одинаковыми на всех окружениях, и переведите сотрудников на группы вместо личных прав. Затем вынесите все object-level проверки в один модуль permissions.py и покройте их тестами на пользователе без is_superuser — именно там прячутся дыры. Если начнёте дублировать if user.has_perm(...) в шаблонах, посмотрите тег {% raw %}{{ perms }}{% endraw %} в шаблонном контексте. А когда матрица доступа перестанет умещаться в функции — читайте документацию по кастомным authentication backends и оценивайте django-guardian.
Защитник прав потребителей: Досудебные претензии на возврат товара со ссылками на ЗоЗПП.
Спросить за 15 ₽Источники: Django documentation: Using the Django authentication system — Permissions and authorization, Django documentation: Customizing authentication — Custom permissions
Частые вопросы
Почему has_perm возвращает True даже для несуществующего права?
Кастомные права не появились после добавления в Meta — что не так?
У меня другая версия Django, код подойдёт?
Как ограничить доступ так, чтобы менеджер видел только свои заявки?
Изменил права в коде, а has_perm показывает старое значение — почему?
Стоит ли сразу ставить django-guardian?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту. Instagram, Facebook и WhatsApp принадлежат компании Meta Platforms Inc., признанной в России экстремистской организацией; её деятельность на территории Российской Федерации запрещена.