Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Авторизация: Permissions & Groups
auth_permissions

Авторизация: Permissions & Groups

Permissions, Groups, decorators, mixins, object-level permissions

Авторизация: permissions, groups и доступ к объекту

После аутентификации приложение знает пользователя. Авторизация сопоставляет пользователя, действие и ресурс. Ошибка «у него есть change_post, значит он может изменить любой пост» возникает, когда model-level permission подменяет object-level правило.

Материал актуален для Django 5.2 LTS и 6.0.

#Четыре стандартных permission модели

Для каждой managed model Django создаёт codenames:

  • view_post;
  • add_post;
  • change_post;
  • delete_post.

Полное имя включает app label: blog.change_post. Permission связан с ContentType, поэтому один codename не гарантирует уникальность во всей базе:

from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from blog.models import Post content_type = ContentType.objects.get_for_model(Post) permission = Permission.objects.get( content_type=content_type, codename="change_post", )

Стандартный ModelBackend проверяет model-level permissions. user.has_perm("blog.change_post") не знает, принадлежит ли пользователю конкретный пост.

#Custom permission — глагол продукта

class Post(models.Model): # поля модели class Meta: permissions = [ ("publish_post", "Can publish post"), ("moderate_post", "Can moderate post"), ]

Проверка должна использовать ровно тот же codename:

request.user.has_perm("blog.publish_post")

Записи Permission создаются обработчиком post_migrate при выполнении migrate, а не самой командой makemigrations. Изменение Meta может породить migration state, но строка permission появляется после миграций. Поэтому bootstrap ролей запускают после migrate.

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

#Groups — назначения, а не вся доменная модель ролей

Group объединяет permissions, а пользователь наследует разрешения всех своих групп:

from django.contrib.auth.models import Group editors, _ = Group.objects.get_or_create(name="Editors") editors.permissions.add(permission) user.groups.add(editors)

Группы удобны для стабильных ролей вроде Editor/Moderator. Но проверка строки user.groups.filter(name="Editors") связывает бизнес-код с названием роли. Если действие уже описано permission, проверяйте permission; состав группы тогда можно менять без переписывания views.

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

#has_perm() и кэш экземпляра

if request.user.has_perm("blog.publish_post"): ... if request.user.has_perms([ "blog.change_post", "blog.publish_post", ]): ...

has_perms() требует все перечисленные permissions. Результат собирается authentication backends; custom backend может изменить поведение.

ModelBackend кэширует permissions на экземпляре пользователя после первой проверки. Если тест или admin-код добавил group/permission, прежний user может не увидеть изменение. Надёжный путь — получить пользователя заново из базы, а не очищать приватные _perm_cache вручную.

Активный superuser получает все model-level permissions через стандартный backend. Это не означает автоматический успех любой самописной проверки владельца: если политика должна разрешать superuser обход, укажите это явно и протестируйте.

#FBV: различайте unauthenticated и forbidden

from django.contrib.auth.decorators import permission_required from django.views.decorators.http import require_POST @require_POST @permission_required("blog.publish_post", raise_exception=True) def publish_post(request, pk): post = get_object_or_404(Post, pk=pk, status=Post.Status.PENDING) post.publish(by=request.user) return redirect("blog:post_detail", slug=post.slug)

raise_exception=True даёт 403 при отсутствии permission. Без него декоратор направляет на login URL, что может создать redirect loop для уже вошедшего пользователя без права. Для HTML-продукта явно решите: anonymous идёт на login, authenticated без права получает 403.

Изменение состояния выполняется только небезопасным HTTP-методом с CSRF, не GET-ссылкой. Метод publish() должен сохранять инварианты и аудит; один флаг во view быстро начинает дублироваться.

#CBV: PermissionRequiredMixin не заменяет область объектов

from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.views.generic import UpdateView class PostUpdateView( LoginRequiredMixin, PermissionRequiredMixin, UpdateView, ): permission_required = "blog.change_post" raise_exception = True model = Post form_class = PostForm def get_queryset(self): queryset = super().get_queryset() if self.request.user.is_superuser: return queryset return queryset.filter(author=self.request.user)

Mixin проверяет model-level permission, а get_queryset() ограничивает конкретные строки. Такое сочетание выражает политику «автор с общей возможностью редактирования — только свои; superuser — все». Если автору не нужен общий permission, уберите PermissionRequiredMixin и оставьте аутентификацию плюс ownership.

Не вызывайте self.get_object() много раз из разных mixin: это даёт повторные запросы и усложняет MRO. Ограниченный QuerySet обычно проще и не раскрывает существование чужого объекта.

#Object-level permission: три рабочих подхода

#1. Ownership в QuerySet

Для простого «только свои» достаточно:

post = get_object_or_404( Post, pk=pk, author=request.user, )

Это одновременно выборка и запрет. При multi-tenant сначала ограничивают tenant, затем владельца; ни один id из URL не должен обходить эту область.

#2. Явная policy-функция

Когда правило зависит от статуса, роли и участника ресурса, вынесите его в чистую функцию:

def can_edit_post(*, user, post) -> bool: if not user.is_authenticated or not user.is_active: return False if user.is_superuser: return True return post.author_id == user.pk and post.status == Post.Status.DRAFT

Policy легко тестировать таблицей сценариев. QuerySet всё равно ограничивают tenant/visibility до передачи объекта в policy.

#3. Object-permission backend

Django core передаёт obj в user.has_perm(perm, obj), но стандартный ModelBackend не хранит назначения на объекты. Сторонний django-guardian добавляет backend и таблицы object permissions:

INSTALLED_APPS += ["guardian"] AUTHENTICATION_BACKENDS = [ "django.contrib.auth.backends.ModelBackend", "guardian.backends.ObjectPermissionBackend", ]
from guardian.shortcuts import assign_perm, get_objects_for_user assign_perm("blog.change_post", user, post) allowed = user.has_perm("blog.change_post", post) posts = get_objects_for_user( user, "blog.change_post", klass=Post, )

Специальный model mixin для базовой работы Guardian не обязателен. Direct foreign-key permission models — отдельная оптимизация, которую добавляют по документации библиотеки и с миграциями.

Guardian не отменяет tenant scope и проверку действия. Также решите, должны ли глобальные model permissions автоматически давать доступ к каждому объекту: параметры shortcut влияют на результат.

#Удаление и массовые операции

Право на «удалить пост» не значит, что можно удалить любой переданный id. Сначала сформируйте разрешённый QuerySet, затем изменяйте строки. Массовая action должна проверить каждый объект или целиком фильтровать QuerySet политикой.

Проверка только в Model.delete() недостаточна: у модели нет request user, а QuerySet.delete() и cascade имеют другой путь. Лучше держать use case с явным actor и запретить обход на уровне вызывающего слоя; для особенно чувствительных данных добавляют database RLS как отдельную defense-in-depth политику.

#Bootstrap групп после migrate

Идемпотентная management command может задать роли:

from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType from django.core.management.base import BaseCommand from blog.models import Post class Command(BaseCommand): def handle(self, *args, **options): content_type = ContentType.objects.get_for_model(Post) permissions = Permission.objects.filter( content_type=content_type, codename__in={"view_post", "change_post", "publish_post"}, ) editor, _ = Group.objects.get_or_create(name="Editors") editor.permissions.set(permissions)

set() намеренно заменяет весь набор. Это правильно, если command — источник истины для системной группы, и опасно, если администраторы вручную добавляют права: следующий deploy их удалит. Выберите ownership конфигурации и зафиксируйте его в runbook.

Для критичных ролей тест проверяет точный набор codenames после bootstrap. Не ищите permissions только по codename и не предполагайте, что они существуют до migrate.

#Admin уже умеет управлять users/groups

django.contrib.auth регистрирует стандартные User и Group в admin. Повторная @admin.register(User) или Group без unregister() приводит к AlreadyRegistered.

Для custom user наследуйте UserAdmin, добавьте поля в fieldsets/add_fieldsets и зарегистрируйте свою модель один раз. Inline through-таблиц для groups/permissions обычно не нужен: стандартный UserAdmin уже предоставляет интерфейс.

#Шаблон не является security boundary

{% if perms.blog.publish_post %} <form method="post" action="{% url 'blog:publish' post.pk %}"> {% csrf_token %} <button type="submit">Опубликовать</button> </form> {% endif %}

Условие улучшает интерфейс, но endpoint обязан повторить проверку. Пользователь может отправить HTTP-запрос без кнопки.

#Legacy и актуальность

Permissions, Groups, decorators и mixins актуальны. Частые legacy-проблемы — permission lookup без content_type, несовпадающие codenames, проверка названия группы вместо действия и предположение, что model permission ограничивает владельца.

Custom permissions создаются после migrate через post_migrate. Старые инструкции «достаточно запустить makemigrations» описывают изменение model state, но не момент появления строк в таблице auth_permission.

#Документация

  • Permissions and authorization
  • Authentication backends
  • django-guardian documentation

Далее: Django Admin: Кастомизация