8. Django REST API: Permissions and Ownership
Django REST API – Permissions and Ownership

In this post, we will improve the security of our Django REST API by adding permissions and ownership rules.
Authentication tells us who the user is, but permissions decide what that user is allowed to access or modify.



 

Authentication vs permissions
In the previous post, we added authentication to protect our API.

Authentication answers this question:

Who is making the request?

Permissions answer a different question:

Is this user allowed to perform this action?

For example, a user may be logged in, but that does not mean they should be able to edit another user's projects or tasks.

 

The ownership problem
At the moment, our API uses querysets like this:

queryset = Project.objects.all()

This means every authenticated user can see all projects in the database.

User A can see User A projects
User A can also see User B projects
User B can see User A projects

For a real application, this is usually a problem.

Each user should only access the data that belongs to them.

User A → Only sees User A projects
User B → Only sees User B projects

 

Adding an owner field
To control ownership, we need to connect each project to a user.

Open config/projects/models.py and update the Project model.

from django.db import models
from django.contrib.auth.models import User


class Project(models.Model):
    owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='projects')
    name = models.CharField(max_length=100)
    description = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name


class Task(models.Model):
    project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='tasks')
    title = models.CharField(max_length=100)
    description = models.TextField(blank=True)
    completed = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

Now each project belongs to one user.

One User    → Many Projects
One Project → Many Tasks

Because tasks belong to projects, a task is indirectly owned by the same user who owns the project.

 

Creating and applying migrations
After changing the model, create a new migration.

python manage.py makemigrations

If you already have existing projects in the database, Django may ask for a default owner value for existing rows.
You can manually use the value 1, it will associate to the previously created superuser.

During development, if you do not need to keep the old data, you can delete the database and migrations and start again. In a real project, you should handle this carefully with a proper data migration.

Then apply the migration:

python manage.py migrate

Now the Project table includes an owner field.

 

Updating the serializer
Open config/projects/serializers.py.

We want the API to show the project owner, but we do not want the client to manually choose the owner when creating a project.

The owner should be automatically assigned from the authenticated user.

from rest_framework import serializers
from .models import Project, Task


class ProjectSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')

    class Meta:
        model = Project
        fields = '__all__'


class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = '__all__'

The ReadOnlyField means the owner will appear in the API response, but cannot be submitted or changed directly by the client.

API shows owner username
Client cannot manually set owner
ViewSet assigns owner automatically

 

Filtering projects by owner
Now we need to update the ProjectViewSet.

Instead of returning all projects, we return only the projects that belong to the authenticated user.

from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated

from .models import Project, Task
from .serializers import ProjectSerializer, TaskSerializer


class ProjectViewSet(viewsets.ModelViewSet):
    serializer_class = ProjectSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Project.objects.filter(owner=self.request.user)

The get_queryset method allows us to customize which objects are returned by the API.

Project.objects.filter(owner=self.request.user)

This ensures that each authenticated user only sees their own projects.

 

Assigning the owner automatically
When a user creates a new project, we want Django REST Framework to automatically set the project owner.

We can do this with the perform_create method.

class ProjectViewSet(viewsets.ModelViewSet):
    serializer_class = ProjectSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Project.objects.filter(owner=self.request.user)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

Now the user does not need to send the owner field in the request body.

Example request:

{
    "name": "Personal Website",
    "description": "Build a portfolio website"
}

The API automatically saves the project with the current authenticated user as the owner.

 

Filtering tasks by project owner
Tasks do not have a direct owner field, but each task belongs to a project.

So, to protect tasks, we filter tasks through the project owner.

class TaskViewSet(viewsets.ModelViewSet):
    serializer_class = TaskSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Task.objects.filter(project__owner=self.request.user)

The double underscore syntax allows us to filter through relationships.

Task.objects.filter(project__owner=self.request.user)

This means:

Only return tasks where the related project belongs to the current user.

 

Preventing users from creating tasks in other users' projects
There is still an important problem.

A user could try to create a task and manually send the ID of a project that belongs to another user.

{
    "project": 5,
    "title": "Unauthorized task",
    "description": "This project may belong to another user",
    "completed": false
}

To prevent this, we should validate that the selected project belongs to the current user.

One simple way is to override perform_create in the TaskViewSet.

from rest_framework.exceptions import PermissionDenied


class TaskViewSet(viewsets.ModelViewSet):
    serializer_class = TaskSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Task.objects.filter(project__owner=self.request.user)

    def perform_create(self, serializer):
        project = serializer.validated_data['project']

        if project.owner != self.request.user:
            raise PermissionDenied('You cannot create tasks for this project.')

        serializer.save()

Now the API checks ownership before creating the task.

If project belongs to current user → Create task
If project belongs to another user → Reject request

 

Complete views.py file
At this stage, your config/projects/views.py file should look like this:

from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.exceptions import PermissionDenied

from .models import Project, Task
from .serializers import ProjectSerializer, TaskSerializer


class ProjectViewSet(viewsets.ModelViewSet):
    serializer_class = ProjectSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Project.objects.filter(owner=self.request.user)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)


class TaskViewSet(viewsets.ModelViewSet):
    serializer_class = TaskSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Task.objects.filter(project__owner=self.request.user)

    def perform_create(self, serializer):
        project = serializer.validated_data['project']

        if project.owner != self.request.user:
            raise PermissionDenied('You cannot create tasks for this project.')

        serializer.save()

This gives us a safer API where users can only access and create resources related to their own data.

 

Testing with different users
To test ownership, create at least two different users.

You can create users through the Django Admin Panel:

Admin: http://localhost:8000/admin/

Then log in as each user and create different projects.

User A creates Project A
User B creates Project B

User A should only see Project A
User B should only see Project B

This is one of the most important security checks in APIs with user-owned data.

 

Object-level permissions
Django REST Framework also supports object-level permissions.

Object-level permissions are useful when we want to check access for a specific object.

Can this user access this specific project?
Can this user update this specific task?
Can this user delete this specific object?

In our current setup, filtering the queryset already prevents users from accessing objects that do not belong to them.

However, for more complex APIs, custom permission classes can be a better option.

 

Creating a custom permission
Inside the projects app, create a new file called permissions.py.

projects/
    __init__.py
    admin.py
    apps.py
    migrations/
    models.py
    permissions.py
    serializers.py
    tests.py
    urls.py
    views.py

Then add this custom permission:

from rest_framework import permissions


class IsProjectOwner(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.owner == request.user

This permission checks whether the current user owns the project object.

For tasks, the ownership check would go through the related project:

class IsTaskProjectOwner(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.project.owner == request.user

Custom permissions become useful when your API needs more specific access rules.

 

Why filtering is still important
Even when using custom permissions, filtering the queryset is still important.

If we only use object-level permissions but return all objects in the queryset, users may still see lists containing data that does not belong to them.

Use queryset filtering to control list results.
Use object permissions to control access to individual objects.

For user-owned data, both concepts are important.

 

Testing protected task creation
Try creating a task using a project ID that belongs to another user.

{
    "project": 2,
    "title": "Invalid task",
    "description": "Trying to use another user's project",
    "completed": false
}

If the project does not belong to the authenticated user, the API should reject the request.

{
    "detail": "You cannot create tasks for this project."
}

This confirms that ownership rules are working correctly.

 

Recommended approach
For this project, a clean and practical approach is:

Projects have a direct owner field
Tasks belong to projects
Project queryset is filtered by owner
Task queryset is filtered by project owner
Project owner is assigned automatically
Task creation validates project ownership

This keeps the API simple while protecting user data properly.

 

What we have learned
In this post, we added ownership rules to our Django REST API.

Authentication identifies the user
Permissions control what the user can do
Projects can belong to users
Querysets can be filtered by request.user
perform_create can assign the owner automatically
Tasks can be filtered through project ownership
PermissionDenied can reject invalid actions
Custom permission classes can handle object-level access rules

Our API is now much safer because users can only access their own projects and tasks.

 

What comes next?
In the next post, we will make our API easier to use by adding filtering, search, and ordering.

This will allow clients to request more specific data through query parameters.

Django REST API: Filtering, Search and Ordering