Аутентификация DRF и Token с удаленными пользователями? - PullRequest
4 голосов
/ 21 мая 2019

Я использую пакет Django с именем django-safedelete , который позволяет удалять пользователей, не удаляя их из базы данных.

По сути, он добавляет атрибут delete в модельи запросы типа User.objects.all() не будут возвращать удаленные модели.

Вы по-прежнему можете запрашивать все объекты с помощью специального менеджера.Например, User.objects.all_with_deleted() вернет всех пользователей, включая удаленных.User.objects.deleted_only() вернет удаленные.

Это работает как положено, за исключением одного случая.Я использую Token Authentication для моих пользователей с Django Rest Framework 3.9, и в моих представлениях DRF я использую встроенное разрешение IsAuthenticated.

Код базового CBV, который я использую:

class MyView(APIView):

    permission_classes = (IsAuthenticated,)

    def get(self, request):
        return Response(status=HTTP_200_OK)

Код реализации DRF разрешения IsAuthenticated:

class IsAuthenticated(BasePermission):
    """
    Allows access only to authenticated users.
    """

    def has_permission(self, request, view):
        return bool(request.user and request.user.is_authenticated)

Проблема

Когда пользователь удаляется программным способом, он все еще может проходить аутентификацию, используяего токен.

Я ожидаю, что у пользователя будет 401 Несанкционированная ошибка, когда он удаляется программно.Что не так?

Ответы [ 2 ]

6 голосов
/ 21 мая 2019

DRF уже использует свойство is_active, чтобы определить, может ли пользователь пройти аутентификацию. Всякий раз, когда вы удаляете пользователя, просто установите одновременно is_active на False.

Для django-safedelete:

Поскольку вы используете django-safedelete, вам придется переопределить метод delete() для деактивации, а затем использовать super() для выполнения исходного поведения, что-то вроде:

class MyUserModel(SafeDeleteModel):
    _safedelete_policy = SOFT_DELETE
    my_field = models.TextField()

    def delete(self, *args, **kwargs):
        self.is_active = False
        super().delete(*args, **kwargs)

    def undelete(self, *args, **kwargs):
        self.is_active = True
        super().undelete(*args, **kwargs)

Обратите внимание, что это работает и с QuerySets, потому что менеджер для SafeDeleteModel переопределяет метод QuerySet delete(). (См .: https://github.com/makinacorpus/django-safedelete/blob/master/safedelete/queryset.py)

Преимущество этого решения заключается в том, что вам не нужно менять класс auth на каждом APIView, и любые приложения, использующие свойство is_active модели User, будут вести себя разумно. Кроме того, если вы не сделаете этого, то удалите также активные объекты, так что в этом нет особого смысла.

6 голосов
/ 21 мая 2019

Почему?

Если мы посмотрим на authenticate_credentials() метод DRF TokenAuthentication [исходный код] , мы увидим, что

def authenticate_credentials(self, key):
    model = self.get_model()
    try:
        token = model.objects.select_related('user').get(key=key)
    except model.DoesNotExist:
        raise exceptions.AuthenticationFailed(_('Invalid token.'))

    if not token.user.is_active:
        raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

    return (token.user, token)

Что означает, что он не отфильтровывает программно удаленные пользовательские экземпляры

Решение

Создайте класс Custom Authentication и подключение в соответствующем представлении

# authentication.py
from rest_framework.authentication import TokenAuthentication, exceptions, _


class CustomTokenAuthentication(TokenAuthentication):
    def authenticate_credentials(self, key):
        model = self.get_model()
        try:
            token = model.objects.select_related('user').get(key=key)
        except model.DoesNotExist:
            raise exceptions.AuthenticationFailed(_('Invalid token.'))

        <b>if not token.user.is_active or not token.user.deleted: # Here I added something new !!</b>
            raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

        return (token.user, token)

и просмотр в представлениях

# views.py
from rest_framework.views import APIView


class MyView(APIView):
    <b>authentication_classes = (CustomTokenAuthentication,)</b>
    permission_classes = (IsAuthenticated,)

    def get(self, request):
        return Response(status=HTTP_200_OK)
...