Lesson 3
Updating Views Permissions in Django REST Framework
Lesson Introduction

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!

Setup

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:

Python
1class 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'.

Understanding Permission and Authentication Classes

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.

Code Snippet Walkthrough: TodoListCreate

Here's how this setup works in practice with TodoListCreate:

Python
1from 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 Todo Update and 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:

Python
1class 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:

Python
1class 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.

Writing Tests for Views with Permissions: Setup

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:

Python
1class 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.

Writing Tests for Views with Permissions: Authentication

To handle authentication efficiently, we define a helper method authenticate_user for logging in our test users:

Python
1def 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.

Writing Tests for Views with Permissions: ListCreate
Python
1def 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.
Writing Tests for Views with Permissions: Update
Python
1def 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.
Writing Tests for Views with Permissions: Delete
Python
1def 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.

Lesson Summary and Practice Introduction

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!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.