Как создать систему пользователей на основе прав доступа в Django?
Я хочу построить REST API, в котором пользователь может выполнять операции над объектами, основываясь на своих правах. Рассмотрим запись, которая представляет автомобиль - она содержит номер лицензии, тип автомобиля и дополнительную информацию. Также рассмотрим следующую систему пользователей:
- Owners - Кто владеет объектом
car
. Может изменять и удалять его. - Редакторы - Кто может изменять только свойства объекта.
- Просмотрщики - Могут только просматривать свойства объекта.
Каждая запись может содержать несколько владельцев/редакторов/просмотрщиков (пользователь, создавший объект, автоматически должен быть владельцем). Также владельцы могут добавлять или удалять редакторов/просмотрщиков.
Итак, в случае GET-запроса я хочу иметь возможность вернуть все объекты, на которые у пользователя есть разрешения, разделенные на эти три категории.
Итак, в моем приложении api
у меня есть следующий код:
Файл models.py
содержит:
class CarRecord(models.Model):
type = models.CharField(max_length=50)
license = models.CharField(max_length=50)
Файл serializers.py
содержит:
class CarRecordSerializer(serializers.ModelSerializer):
type = models.CharField(max_length=50)
license = models.CharField(max_length=50)
class Meta:
model = CarRecord
fields = ('__all__')
В view.py
у меня есть:
class CarRecordViews(APIView):
def get(self, request):
if not request.user.is_authenticated:
user = authenticate(username=request.data.username, password=request.data.password)
if user is not None:
return Response(data={"error": "invalid username/password"}, status=status.HTTP_401_UNAUTHORIZED)
# return all records of cars that user some type of permission for
Теперь я хочу получить все записи user
, которые у него есть разрешения на запрос (вместе с их типом разрешения). Я думал добавить три дополнительных поля под CarRecord
- каждое из них представляет собой список пользователей, которые содержат данный тип разрешения. Но я не уверен, что это "путь Django". Поэтому хотел сначала посоветоваться с SO.
Вот решение, которое я могу считать правильным
class CarRecord(models.Model):
date_created = models.DateTimeField(auto_now_add=True)
type = models.CharField(max_length=50)
license = models.CharField(max_length=50)
owners = models.ManyToManyField(User, related_name='car_owners')
creator = models.ForeignKey(User,on_delete=models.DO_NOTHING)
class CarRecordSerializer(serializers.ModelSerializer):
creator = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), required=False)
owners_details = UserSerializer(source='owners', many=True, read_only=True)
class Meta:
model = CarRecord
fields = '__all__'
def create(self, validated_data):
try:
new_owners = validated_data.pop('owners')
except:
new_owners = None
car_record = super().create(validated_data)
if new_owners:
for new_owner in new_owners:
car_record.owners.add(new_owner)
return car_record
В views.py
from rest_frameword import generics
from rest_framework import permissions
class CustomCarRecordPermissions(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method == 'GET':
return True
elif request.method == 'PUT' or request.method == 'PATCH':
return request.user == obj.creator or request.user in obj.owners.all()
elif request.method == 'DELETE':
return request.user == obj.creator
return False
class CarRecordListCreate(generics.ListCreateAPIView):
permission_classes = (IsAuthenticated, )
serializer_class = CarRecordSerializer
queryset = CarRecord.objects.all()
def post(self, request, *args, **kwargs):
request.data['creator'] = request.user.id
return super().create(request, *args, **kwargs)
class CarRecordDetailView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (CustomCarRecordPermissions, )
serializer_class = CarRecordSerializer
lookup_field = 'pk'
queryset = CarRecord.objects.all()
модели является самоочевидным; В сериализаторах CarRecord мы устанавливаем создателя как требуемое поле False и связанное поле первичного ключа, чтобы мы могли предоставить запрос идентификатора пользователя перед созданием, как показано в views.py post method.
В представлении Detail мы устанавливаем наше пользовательское разрешение; если запрос GET, мы разрешаем разрешения. Но если запрос PUT или PATCH, то разрешены владельцы и создатель. Но если это запрос на удаление, то разрешено только создателю.
Я думаю, что пакет django-rest-framework-guardian
подходит сюда. Этот пакет основан на django-guardian
.
django-guardian - это реализация объектных разрешений для Django, предоставляющая дополнительный бэкенд аутентификации.
Нет никаких изменений на вашем models.py
Вам следует изменить serializers.py
и views.py
.
Например, ваш сериализатор должен выглядеть следующим образом
from rest_framework_guardian.serializers import ObjectPermissionsAssignmentMixin
class CarRecordSerializer(ObjectPermissionsAssignmentMixin, serializers.ModelSerializer):
type = models.CharField(max_length=50)
license = models.CharField(max_length=50)
class Meta:
model = CarRecord
fields = ('__all__')
def get_permissions_map(self, created):
current_user = self.context['request'].user
readers = Group.objects.get(name='readers')
editors = Group.objects.get(name='editors')
owners = Group.objects.get(name='owners')
return {
'view_car_record': [current_user, readers, owners],
'change_car_record': [current_user, editors],
'delete_car_record': [current_user, owners]
}
и ваши представления должны выглядеть следующим образом:
from rest_framework_guardian import filters
class CarRecordModelViewSet(ModelViewSet):
queryset = CarRecord.objects.all()
serializer_class = CarRecordSerializer
filter_backends = [filters.ObjectPermissionsFilter]
Редактируйте settings.py
следующим образом:
INSTALLED_APPS = [
'rest_framework',
'guardian',
]
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"guardian.backends.ObjectPermissionBackend",
]
Вы также можете определить бэкенды фильтров глобально в настройках:
REST_FRAMEWORK = {
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework_guardian.filters.ObjectPermissionsFilter",
],
}
Не забудьте! Если вы определите ObjectPermissionsFilter
в settings.py, все ваши представления будут затронуты этим фильтром.
Если вы хотите ограничить post
запрос для каждого пользователя, вы должны реализовать класс пользовательских разрешений, например, так:
from rest_framework import permissions
class CustomObjectPermissions(permissions.DjangoObjectPermissions):
"""
Similar to `DjangoObjectPermissions`, but adding 'view' permissions.
"""
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
'HEAD': ['%(app_label)s.view_%(model_name)s'],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
Пройдите по этой ссылке, чтобы получить подробную информацию для CustomObjectPermissions
Вы можете написать разрешение class car owner user.
Ваша модель.
class CarRecord(models.Model):
date_created = models.DateTimeField()
type = models.CharField(max_length=50)
license = models.CharField(max_length=50)
owners = models.ManyToManyField(User, related_name='car_owners', verbose_name=('owners'), default=[]))
creator = models.ForeignKey(User,on_delete=models.DO_NOTHING)
Класс разрешения permission.py
from rest_framework.permissions import BasePermission,
from cars.models import CarRecord
class isCarAccess(BaseCommand):
def has_permission(self, request, view):
if request.method == 'OPTIONS':
return True
check_user = CarRecord.objects.filter(owners__in=[request.user])
return request.user is not None and request.user.is_authenticated and check_user
этот класс разрешения будет проверять, существует ли пользователь, аутентифицирован ли он, а также принадлежит ли пользователь к записи карты или нет.
И вы можете передать это разрешение в вашем представлении.
from .permission import isCarAccess
from .models import CarRecord
class CarRecordViews(APIView):
permission_classes = [isCarAccess]
def get(self, request):
car_record = CarRecord.objects.filter(owners__in=[request.user])
# return all records of cars that user some type of permission for
и ваш settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"oauth2_provider.contrib.rest_framework.OAuth2Authentication",
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
),
}
Надеюсь, это поможет вам.