Hello! In this lesson, we are exploring an integral part of building a secure API using the Django REST Framework (DRF): controlling access to your API by updating view permissions. Think of permissions like a classified vault; you wouldn’t want everyone to have access, right? Similarly, for your web application, you want to control who can view, modify, or delete specific data.
Our goal is to understand and implement permission_classes
and authentication_classes
for API views, which helps us define who can perform certain actions. This knowledge is critical to ensuring your application remains secure while providing the right level of access to your users. Let's unlock the secrets to API view security together!
Before we start, one important note. Now, our Todo
model has a user
as its field. However, we won't usually pass the user data when creating a Todo
instance. Instead, we will simply fetch the current authenticated user and set them as an owner. This means that we don't need user
to be present in the serializer fields. To reflect that, let's update the serializer:
Python1class TodoSerializer(serializers.ModelSerializer): 2 group = GroupSerializer(required=False) 3 tags = TagSerializer(many=True, required=False) 4 5 class Meta: 6 model = Todo 7 exclude = ['user'] # Do not include user in the fields
This way, we include all the fields, except 'user'
.
To get started, let’s talk about permission_classes
and authentication_classes
. Think of permission_classes
as the gatekeeper of your application — how friendly or restrictive the gatekeeper is determines who can pass through the gate.
In DRF, permission_classes
define rules about whether a specific user can perform a given action on your API endpoint. In TodoListCreate
, we use IsAuthenticatedOrReadOnly
. This allows any user to fetch data (GET
requests) but restricts modifying actions (POST
requests) to authenticated users. authentication_classes
act as proof of identity. We use TokenAuthentication
, meaning users need to supply a valid token to prove identity.
Here's how this setup works in practice with TodoListCreate
:
Python1from rest_framework import generics 2from rest_framework.permissions import IsAuthenticatedOrReadOnly 3from rest_framework.authentication import TokenAuthentication 4from .models import Todo 5from .serializers import TodoSerializer 6 7class TodoListCreate(generics.ListCreateAPIView): 8 queryset = Todo.objects.all() 9 serializer_class = TodoSerializer 10 permission_classes = [IsAuthenticatedOrReadOnly] 11 authentication_classes = [TokenAuthentication] 12 13 def perform_create(self, serializer): 14 serializer.save(user=self.request.user)
Our TodoListCreate
view allows any visitor to browse through the list of to-dos. However, only a logged-in (authenticated) user can create new to-dos. The view leverages permissions and authentication to secure the data.
The perform_create
method ensures that the user who creates a new to-do is set as its owner. When a POST
request is made, this method assigns user=self.request.user
to the new Todo
instance via serializer.save()
, automatically linking the to-do to the authenticated user, thereby ensuring secure ownership.
Managing ownership is essential. Suppose our user, Alice, creates a to-do item. We want to restrict others from modifying or deleting it. This is enforced through the re-defining logic of perform_update()
and perform_destroy()
.
Within TodoUpdate
, we ensure only the creator of the to-do (i.e., the owner) can update it:
Python1class TodoUpdate(generics.UpdateAPIView): 2 permission_classes = [IsAuthenticated] 3 authentication_classes = [TokenAuthentication] 4 5 def perform_update(self, serializer): 6 if self.request.user == serializer.instance.user: 7 serializer.save()
In TodoDelete
, deleting a to-do is allowed only by its owner:
Python1class TodoDelete(generics.DestroyAPIView): 2 permission_classes = [IsAuthenticated] 3 authentication_classes = [TokenAuthentication] 4 5 def perform_destroy(self, instance): 6 if self.request.user == instance.user: 7 instance.delete()
These checks ensure ownership control, like a locker where only the owner has the key. Note that in these views we set permission_classes
to IsAuthenticated
, which ensures only authenticated users can update or delete Todo
items.
Testing is a critical part of ensuring that our permissions are correctly configured. We'll walk through how you can write basic tests for your views in Django REST Framework (DRF) to validate our permission setup. Then, we'll suggest options on how to make them cover more scenarios.
Before writing the test methods, we configure our test environment:
Python1class TodoPermissionTests(APITestCase): 2 3 def setUp(self): 4 # Create users 5 self.user1 = User.objects.create_user(username='user1', password='pass123') 6 self.user2 = User.objects.create_user(username='user2', password='pass123') 7 8 # Create a todo entry by user1 9 self.todo = Todo.objects.create(task='Task 1', user=self.user1) 10 11 # Authentication URLs 12 self.login_url = reverse('login')
setUp()
method is a preparation step that runs before each test. Two users, user1
and user2
, are created, and a todo item is assigned to user1
. We also obtain the login
URL to use it later.
To handle authentication efficiently, we define a helper method authenticate_user
for logging in our test users:
Python1def authenticate_user(self, user): 2 response = self.client.post(self.login_url, {'username': user.username, 'password': 'pass123'}) 3 self.assertEqual(response.status_code, status.HTTP_200_OK) 4 token = response.data['token'] 5 self.client.credentials(HTTP_AUTHORIZATION='Token ' + token)
This function logs in the specified user using their credentials, checks if the response is successful (HTTP_200_OK
), extracts the token, and sets it in the authorization header for subsequent requests. Note that this function is not a test, it is a helper function that we will use in tests to authorize user.
Python1def test_todo_list_create_permission(self): 2 response = self.client.get(reverse('todo-list')) 3 self.assertEqual(response.status_code, status.HTTP_200_OK) 4 5 self.authenticate_user(self.user1) 6 todo_data = {'task': 'New Task', 'user': self.user1.id} 7 response = self.client.post(reverse('todo-list'), todo_data) 8 self.assertEqual(response.status_code, status.HTTP_201_CREATED)
With test_todo_list_create_permission
, we verify that unauthenticated users can perform GET
requests. After that, we perform login for user1
, and validate that the POST
request to create a new todo is successful with a HTTP_201_CREATED
status.
Enhancements for test_todo_list_create_permission
:
- Test invalid token scenarios where the token is expired or incorrect.
- Check if specific fields are validated correctly and respond with appropriate status codes.
- Validate response content, ensuring that the returned data matches the expected format.
Python1def test_todo_update_permission(self): 2 # Test unauthorized update 3 new_data = {'task': 'Updated Task'} 4 response = self.client.put(reverse('todo-update', kwargs={'pk': self.todo.pk}), new_data) 5 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) 6 7 # Test update by non-owner user 8 self.authenticate_user(self.user2) 9 response = self.client.put(reverse('todo-update', kwargs={'pk': self.todo.pk}), new_data) 10 self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) 11 12 # Test update by owner 13 self.authenticate_user(self.user1) 14 response = self.client.put(reverse('todo-update', kwargs={'pk': self.todo.pk}), new_data) 15 self.assertEqual(response.status_code, status.HTTP_200_OK)
In test_todo_update_permission
, we check three scenarios: unauthorized update returns HTTP_401_UNAUTHORIZED
, non-owner tries to update results in HTTP_403_FORBIDDEN
, and the owner is allowed an HTTP_200_OK
status for a successful update.
Enhancements for test_todo_update_permission
:
- Include checks for partial updates using
PATCH
requests. - Verify the behavior when attempting to update non-existent records, ensuring it returns
404 NOT FOUND
. - Test against updates with invalid payloads that should cause validation errors.
Python1def test_todo_delete_permission(self): 2 # Test unauthorized delete 3 response = self.client.delete(reverse('todo-delete', kwargs={'pk': self.todo.pk})) 4 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) 5 6 # Test delete by non-owner user 7 self.authenticate_user(self.user2) 8 response = self.client.delete(reverse('todo-delete', kwargs={'pk': self.todo.pk})) 9 self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) 10 11 # Test delete by owner 12 self.authenticate_user(self.user1) 13 response = self.client.delete(reverse('todo-delete', kwargs={'pk': self.todo.pk})) 14 self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
Similarly, test_todo_delete_permission
ensures that deleting a to-do requires proper authorization. Unauthorized deletions and deletions by a non-owner should fail with HTTP_401_UNAUTHORIZED
and HTTP_403_FORBIDDEN
respectively. The owner can successfully delete with HTTP_204_NO_CONTENT
.
Enhancements for test_todo_delete_permission
:
- Confirm that attempting to delete non-existent records results in a
404 NOT FOUND
. - Test system behavior when trying to delete data linked with non-removable relations.
These tests confirm that our permission logic is functioning as expected, safeguarding our application's data effectively.
In summary, we reviewed how view permissions and authentication in DRF are crucial for secure APIs. From allowing read-only access to securing updates with IsAuthenticated
, you've seen how these classes ensure the right users have the right access. You've also learned about ownership control via perform_update()
and perform_destroy()
, preventing unauthorized alterations.
Great job! Now, let's begin hands-on practice. You will apply what you've learned by implementing permissions and authentication mechanisms in your Django project. This practice session is intended to consolidate your understanding and build confidence in securing API views across various scenarios. Let’s start coding!