vesii
vesii

Reputation: 3128

How to have a permission-based user system in Django?

I want to build a REST API where user can do operations on objects, based on their permissions. Consider a record that represents a car - it contains the license number, the type of the car and extra information. Also consider the following user system:

Each record can contain multi owners/editors/viewers (The user who created the object should be automatically the owner). Also, owners can add or remove editors/viewers. In my head, I see it as a list of owners/editors/viewers.

So in case of a GET request, I want to be able to return all objects that the user has permissions for, separated into those three categories.

So under my api app, I have the following code:

The models.py file contains:

class CarRecord(models.Model):
    type = models.CharField(max_length=50)
    license = models.CharField(max_length=50)

The serializers.py file contains:

class CarRecordSerializer(serializers.ModelSerializer):
    type = models.CharField(max_length=50)
    license = models.CharField(max_length=50)

    class Meta:
        model = CarRecord
        fields = ('__all__')

In view.py I have:

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
            

Now, I want to get all the records of user that he has permissions to query (along with their permission type). I thought of adding a three extra fields under CarRecord - each one is a list of users that contains that permission type. But I'm not sure if it's the "Django way". So wanted to consult first with SO.

EDIT: I tried to add the following field to my CarRecord class:

owners = models.ManyToManyField(User, related_name='car_owners', verbose_name=('owners'), default=[])

Also I added:

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['username']

lass CarRecordSerializer(serializers.ModelSerializer):
    type = models.CharField(max_length=50)
    license = models.CharField(max_length=50)
    owners = UserSerializer(many=True)

    class Meta:
        model = CarRecord
        fields = ('__all__')

And the way I create the CarRecordSerializer instance is:

serializer = CarRecordSerializer(data=request.data)

But I get:

{
    "error": {
        "owners": [
            "This field is required."
        ]
    }
}

How to make it work? I guess is my problem is how to serialize a ManyToMany object?

EDIT2: My second attempt is:

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)

# ...

class CarRecordSerializer(serializers.ModelSerializer):
    date_created = serializers.DateTimeField(default=datetime.now(timezone.utc))
    type = models.CharField(max_length=50)
    license = models.CharField(max_length=50)
    creator = serializers.StringRelatedField()
    owners = serializers.PrimaryKeyRelatedField(many=True,read_only=True)

    class Meta:
        model = CarRecord
        fields = '__all__'

    def create(self, validated_data):
        self.owners = [self.context['creator']]
        record = CarRecord(**validated_data, creator=self.context['creator'])
        record.save()
        return record

# ... 

# In post method:
serializer = CarRecordSerializer(data=request.data, context={ 'creator': user })

But now, in GET method, I filter the owners list with the user and it can't find the objects:

> CarRecord.objects.filter(owners=user)
<QuerySet []>

Also, in the Admin section I see that all of the objects automatically have all the users in the owners/editors/viewers lists. Why is that? Owners should contain only the user that created the record and editors and viewers should be empty lists. In another query, owner can add additional owners/editors/viewers.

Upvotes: 5

Views: 781

Answers (3)

Rahul Raj
Rahul Raj

Reputation: 516

You can write permission class car owner user.

Your model.

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 class 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

this permission class will check that does user exists, user is authenticated and as well the user belongs to the card record or not.

And you can pass this permission in your view.

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

and your settings.py

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (  
       "oauth2_provider.contrib.rest_framework.OAuth2Authentication",
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.TokenAuthentication",
    ),
}   
            

Upvotes: 0

G. &#199;ay
G. &#199;ay

Reputation: 1

I think the django-rest-framework-guardian package fits here. This package is based on django-guardian.

django-guardian is an implementation of object permissions for Django providing an extra authentication backend.

There is no change on your models.py

You should change serializers.py and views.py. For example, your serializer should look like this

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]
        }

and your views should look like this:

from rest_framework_guardian import filters

class CarRecordModelViewSet(ModelViewSet):
    queryset = CarRecord.objects.all()
    serializer_class = CarRecordSerializer
    filter_backends = [filters.ObjectPermissionsFilter]
      

Edit settings.py like this:

INSTALLED_APPS = [
    'rest_framework',
    'guardian',
]

AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "guardian.backends.ObjectPermissionBackend",
]

You can define filter backends globally in your settings, too:

REST_FRAMEWORK = {    
    "DEFAULT_FILTER_BACKENDS": [
        "django_filters.rest_framework.DjangoFilterBackend",
        "rest_framework_guardian.filters.ObjectPermissionsFilter",
    ],
}

Don't forget! If you define the ObjectPermissionsFilter in the settings.py, your all views are affected by this filter.

If you want to restrict post request per user, you shoul implement custom permission class, like this:

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'],
    }

Check this link to get the detailed information for the CustomObjectPermissions

Upvotes: 0

Shishir Subedi
Shishir Subedi

Reputation: 629

Here is the solution I might think is the right one

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

In 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()
    
    

models is self explanatory; In CarRecord serializers we set creator as required False and primary key related field so that we can supply request user id before create as shown in views.py post method.

In Detail view we set our custom permission; If the request is GET we allow permissions. But if the request is PUT or PATCH the owners and the creator are allowed. But if it is a delete request only creator is allowed.

Upvotes: 1

Related Questions