Как настроить разрешения Django для конкретных экземпляров модели?

Рассмотрим простое приложение Django, содержащее центральную модель под названием Project. Другие ресурсы этого приложения всегда привязаны к определенной модели Project.

Примерный код:

class Project(models.Model):
    pass

class Page(models.Model):
    project = models.ForeignKey(Project)

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

Есть ли способ расширить или заменить систему авторизации Django для достижения чего-то подобного?

Я мог бы расширить модель пользователя Group, чтобы включить ссылку на Project и проверить как проект группы, так и ее разрешения. Но это не элегантно и не позволяет назначать разрешения отдельным пользователям.

Имеет ли смысл использовать такой проект, как django-guardian для получения прав доступа на уровне объекта? Мне кажется, что это довольно накладно для того, что не нуждается в такой детализации.

Мой ответ основан на a user should be able to have a view_page permission for one project instance, and don't have it for another instance.

По сути, вам придется перехватывать first user visit == first model instance, вы можете создать FirstVisit model, который будет перехватывать и сохранять каждый первый экземпляр, используя url, user.id и page.id, затем вы проверяете, существует ли он для каждого пользователя.

# model

class Project(models.Model):
   pass

class Page(models.Model):
    project = models.ForeignKey(Project)

class FirstVisit(models.Model):
    url = models.URLField()
    user = models.ForeignKey(User)
    page = models.ForeignKey(Page)


#views.py

def my_view(request):
   if not FisrtVisit.objects.filter(user=request.user.id, url=request.path, page=request.page.id).exists():
      # first time visit == first instance
      #your code...
      FisrtVisit(user=request.user, url=request.path, page=request.page.id).save()

на основе этого решения

Я предлагаю использовать Mac-адрес устройства (компьютера или смартфона) вместо url, используя getmac для максимальной проверки при первом посещении

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

Если вы не планируете использовать DRF, я бы добавил django-guardian

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

Допустим, вы создали их в области администратора и назначили права доступа, тогда вы можете сделать что-то вроде этого в представлении.

from django.core.exceptions import PermissionDenied

from .models import Project, Page

def my_page_view(request):
    page = Page.objects.get(pk=1)
    project = page.project
    if request.user.has_perm('view_project', project):
        raise PermissionDenied()
    else:
        # do something

Я действительно могу придумать удобный способ добиться этого таким образом, чтобы не повторять этот код в каждом представлении. Альтернативой может быть промежуточное ПО или декоратор. Но в любом случае, промежуточное ПО/декоратор должны знать, к какому экземпляру (Page) пользователь пытается получить доступ, который еще не определен. Можно написать декоратор, который принимает аргумент экземпляра, который затем передается в представление. Например, так (конечно, вы должны будете написать настоящий декоратор):

@has_project_permission(project=Page.objects.get(pk=1).project)
def my_page_view(request, project):
    pass

Это не совсем полный ответ, но я надеюсь, что он поможет вам в правильном направлении.

Меня не совсем устраивали предложенные (к счастью!) ответы, потому что они, казалось, вводили накладные расходы, либо по сложности, либо по обслуживанию. В частности, для django-guardian мне потребовался бы способ поддерживать разрешения на уровне объектов в актуальном состоянии, при этом потенциально страдая от (незначительной) потери производительности. То же самое справедливо и для динамически создаваемых разрешений; мне потребовался бы способ поддерживать их в актуальном состоянии и отклониться от стандартного способа определения разрешений (только) в моделях.

Но оба ответа подтолкнули меня к более детальному рассмотрению системы аутентификации и авторизации Django. Именно тогда я понял, что вполне реально расширить ее под свои нужды (как это часто бывает с Django)


Я решил эту проблему, введя новую модель, ProjectPermission, которая связывает Permission с проектом и может быть назначена пользователям и группам. Эта модель представляет тот факт, что пользователь или группа имеют разрешение на определенный проект.

Для использования этой модели я расширил ModelBackend и ввел параллельную проверку разрешений, has_project_perm, которая проверяет, есть ли у пользователя разрешение на определенный проект. Код в основном аналогичен пути по умолчанию has_perm, определенному в ModelBackend.

Используя проверку разрешений по умолчанию, has_project_perm будет возвращать True, если пользователь либо имеет разрешение для конкретного проекта, либо имеет разрешение по старинке (которое я назвал "глобальным"). Это позволяет мне назначать разрешения, действительные для всех проектов, не указывая их явно.

Наконец, я расширил свою пользовательскую модель пользователя для доступа к новой проверке разрешений, введя новый метод has_project_perm.


# models.py

from django.contrib import auth
from django.contrib.auth.models import AbstractUser, Group, Permission
from django.core.exceptions import PermissionDenied
from django.db import models

from showbase.users.models import User


class ProjectPermission(models.Model):
    """A permission that is valid for a specific project."""

    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    base_permission = models.ForeignKey(
        Permission, on_delete=models.CASCADE, related_name="project_permission"
    )
    users = models.ManyToManyField(User, related_name="user_project_permissions")
    groups = models.ManyToManyField(Group, related_name="project_permissions")

    class Meta:
        indexes = [models.Index(fields=["project", "base_permission"])]
        unique_together = ["project", "base_permission"]


def _user_has_project_perm(user, perm, project):
    """
    A backend can raise `PermissionDenied` to short-circuit permission checking.
    """
    for backend in auth.get_backends():
        if not hasattr(backend, "has_project_perm"):
            continue
        try:
            if backend.has_project_perm(user, perm, project):
                return True
        except PermissionDenied:
            return False
    return False


class User(AbstractUser):
    def has_project_perm(self, perm, project):
        """Return True if the user has the specified permission in a project."""
        # Active superusers have all permissions.
        if self.is_active and self.is_superuser:
            return True

        # Otherwise we need to check the backends.
        return _user_has_project_perm(self, perm, project)
# auth_backends.py

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import Permission


class ProjectBackend(ModelBackend):
    """A backend that understands project-specific authorization."""

    def _get_user_project_permissions(self, user_obj, project):
        return Permission.objects.filter(
            project_permission__users=user_obj, project_permission__project=project
        )

    def _get_group_project_permissions(self, user_obj, project):
        user_groups_field = get_user_model()._meta.get_field("groups")
        user_groups_query = (
            "project_permission__groups__%s" % user_groups_field.related_query_name()
        )
        return Permission.objects.filter(
            **{user_groups_query: user_obj}, project_permission__project=project
        )

    def _get_project_permissions(self, user_obj, project, from_name):
        if not user_obj.is_active or user_obj.is_anonymous:
            return set()

        perm_cache_name = f"_{from_name}_project_{project.pk}_perm_cache"
        if not hasattr(user_obj, perm_cache_name):
            if user_obj.is_superuser:
                perms = Permission.objects.all()
            else:
                perms = getattr(self, "_get_%s_project_permissions" % from_name)(
                    user_obj, project
                )
            perms = perms.values_list("content_type__app_label", "codename").order_by()
            setattr(
                user_obj, perm_cache_name, {"%s.%s" % (ct, name) for ct, name in perms}
            )
        return getattr(user_obj, perm_cache_name)

    def get_user_project_permissions(self, user_obj, project):
        return self._get_project_permissions(user_obj, project, "user")

    def get_group_project_permissions(self, user_obj, project):
        return self._get_project_permissions(user_obj, project, "group")

    def get_all_project_permissions(self, user_obj, project):
        return {
            *self.get_user_project_permissions(user_obj, project),
            *self.get_group_project_permissions(user_obj, project),
            *self.get_user_permissions(user_obj),
            *self.get_group_permissions(user_obj),
        }

    def has_project_perm(self, user_obj, perm, project):
        return perm in self.get_all_project_permissions(user_obj, project)
# settings.py

AUTHENTICATION_BACKENDS = ["django_project.projects.auth_backends.ProjectBackend"]
Вернуться на верх