Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 59

Testing Authorization Rules

Recommended resources

Hello! In our last lesson, we built a solid foundation by writing tests for your application's authentication flow, ensuring users can register and log in reliably. Now that we know who the user is, we need to control what they are allowed to do.

Today, we shift our focus from authentication to authorization. This lesson is dedicated to testing the rules that enforce data privacy between users. For your Task Manager project, this is one of the most important features: you must guarantee that a user can only see and manage their own tasks, and not anyone else's. Failing to do so creates a major security vulnerability.

By the end of this 60-minute lesson, you will be able to write tests that verify your authorization rules, ensuring each user's data is kept private and secure.

1. The Threat: Broken Object-Level Authorization (BOLA)

Before we write any tests, let's understand the problem we're trying to solve. The most common authorization failure in applications like our Task Manager is called Broken Object-Level Authorization (BOLA).

Imagine User A is logged in and views their task at the URL /tasks/15/. What stops them from simply typing /tasks/16/ into their browser to see a task that might belong to User B? If nothing prevents this, your application has a BOLA vulnerability. Our tests must prove that our application is not vulnerable.

The following article provides an excellent overview of this security risk and how to approach testing for it.

Testing API Security on a Django DRF Backend

This article, 'Testing API Security on a Django DRF Backend', clearly explains the BOLA vulnerability. We will focus on its definition and the testing strategy it proposes.

Please read the first section, 'API1:2023 — Broken Object Level Authorization (BOLA)'. Focus on understanding the risk and the manual and automated testing approaches. Don't worry about the DRF-specific 'Fixing the Issue' code for now; we're focused on the testing concept.

As the article highlights, our testing strategy must cover two fundamental scenarios:

  1. Positive Case: A user can access and modify the data they own.
  2. Negative Case: A user cannot access or modify data owned by another user.

Our tests will programmatically simulate these scenarios.


2. Setting Up the Test Environment

To test authorization, we need a setup that mimics a multi-user environment. In our test file (e.g., tests/test_views.py or a new tests/test_authorization.py), we'll expand our setUp method to create the necessary users and objects.

Here’s how you would set up the test class:

# tests/test_authorization.py
from django.test import TestCase, Client
from django.contrib.auth import get_user_model
from django.urls import reverse

# Assuming you have a 'Task' model
from tasks.models import Task 

User = get_user_model()

class AuthorizationTests(TestCase):
    def setUp(self):
        # Create User A
        self.user_a = User.objects.create_user(
            username='userA', 
            password='password123'
        )
        
        # Create User B
        self.user_b = User.objects.create_user(
            username='userB', 
            password='password123'
        )
        
        # Create a task owned by User A
        self.task_a = Task.objects.create(
            title='Task for User A', 
            user=self.user_a
        )
        
        # Create a task owned by User B
        self.task_b = Task.objects.create(
            title='Task for User B',
            user=self.user_b
        )
        
        # Create a client instance
        self.client = Client()

This setup gives us two distinct users and a task that belongs to each one, creating the perfect scenario to test our privacy rules.


3. Testing the Task List View

First, let's ensure the main task list page only shows tasks belonging to the currently logged-in user. In your ListView, you would typically filter the queryset like this: Task.objects.filter(user=self.request.user). Our test will verify this filter works correctly.

The MDN Web Docs provide a great example of this testing pattern, which we will adapt. In their example, they test that a user only sees books they have borrowed. We will do the same for tasks.

Django Tutorial Part 10: Testing a Django web application

The Django documentation on the Mozilla Developer Network (MDN) has a superb section on testing views. It demonstrates exactly how to test that a list view only contains items belonging to the logged-in user.

Read the section titled 'Views that are restricted to logged in users'. Pay close attention to the LoanedBookInstancesByUserListViewTest class. See how it sets up multiple users and books, and then how the test_only_borrowed_books_in_list method logs in a user and checks the contents of the list.

Here is the test, applying that same logic to our Task Manager:

# In the AuthorizationTests class
    def test_task_list_shows_only_own_tasks(self):
        # ARRANGE: Log in as User A
        self.client.login(username='userA', password='password123')

        # ACT: Request the task list page
        response = self.client.get(reverse('task-list')) # Assuming 'task-list' is the URL name

        # ASSERT:
        # The page loads successfully
        self.assertEqual(response.status_code, 200)

        # The context contains User A's task
        self.assertContains(response, self.task_a.title)
        
        # The context DOES NOT contain User B's task
        self.assertNotContains(response, self.task_b.title)

        # A more robust check on the queryset in the context
        tasks_in_context = response.context['task_list']
        self.assertEqual(len(tasks_in_context), 1)
        self.assertEqual(tasks_in_context[0].user, self.user_a)

This test proves that userA can see their own task but is completely unaware of userB's task on the list page.


4. Testing Detail, Update, and Delete Views

Now for the BOLA test. We need to check that a user cannot access the detail, update, or delete pages for an object they don't own, even if they know the URL.

When a user tries to access a page they aren't authorized to see, your application should respond with an HTTP error code. Typically, this will be:

  • 403 Forbidden: The user is authenticated, but does not have permission.
  • 404 Not Found: The application pretends the object doesn't exist to avoid leaking information.

Both are valid responses. Django's generic views with LoginRequiredMixin and a custom UserPassesTestMixin often result in a 403, which is what we'll test for.

Django REST Framework HTTP 403 Forbidden Error
This image shows a 403 Forbidden error. This is the kind of response we expect when a user tries to access something they don't own.

Let's write a test to confirm this behavior.

# In the AuthorizationTests class
    def test_user_cannot_view_other_users_task_detail(self):
        # ARRANGE: Log in as User A
        self.client.login(username='userA', password='password123')

        # ACT: Try to access the detail page for User B's task
        url = reverse('task-detail', kwargs={'pk': self.task_b.pk})
        response = self.client.get(url)

        # ASSERT: Access is denied
        self.assertEqual(response.status_code, 403) # Or 404, depending on your implementation

This simple test is incredibly powerful. It directly checks for the BOLA vulnerability. You should write similar tests for your UpdateView and DeleteView URLs.

The following video, though focused on Django REST Framework, provides a great walkthrough of the logic for setting up tests with multiple users and checking for correct data access.

Django REST Framework - Permissions and Testing Permissions

This video on testing permissions in DRF visually demonstrates the testing strategy we're using. The underlying logic is the same, even though the syntax is for an API.

Watch from 04:31 to 12:38. Focus on these key concepts: The setUp method creating multiple users and orders (our 'tasks'). The test function that logs in one user. The assertion that the API response only contains data belonging to that logged-in user. This mirrors the logic of our test_task_list_shows_only_own_tasks.

Test your understanding!

You have written test_user_cannot_view_other_users_task_detail, which sends a GET request. How would you adapt this test to verify that User A cannot delete User B's task? The delete URL is typically a POST request.

Show answer

The test would be almost identical, but you would use self.client.post() instead of self.client.get().

def test_user_cannot_delete_other_users_task(self):
    # ARRANGE: Log in as User A
    self.client.login(username='userA', password='password123')

    # ACT: Try to POST to the delete confirmation page for User B's task
    url = reverse('task-delete', kwargs={'pk': self.task_b.pk})
    response = self.client.post(url)

    # ASSERT:
    # 1. Access is denied (403 Forbidden)
    self.assertEqual(response.status_code, 403)
    
    # 2. Crucially, User B's task still exists in the database
    self.assertTrue(Task.objects.filter(pk=self.task_b.pk).exists())

The second assertion is vital—it confirms that the unauthorized POST request did not result in a data change.


Conclusion

Congratulations! You have now learned how to write tests for one of the most important security features of a multi-user application. By verifying your authorization rules, you can confidently build your Task Manager portfolio project knowing that your users' data will be private and secure.

Key Takeaways:

  • Authorization vs. Authentication: Authentication confirms who a user is; authorization determines what they can do.
  • Test for BOLA: Always test to ensure a user cannot access or modify objects they don't own by manipulating URLs.
  • Create a Multi-User Environment: Your test setup should include at least two users and objects owned by each to simulate real-world conditions.
  • Test Both Positive and Negative Cases:
    • Verify that users can see their own data in list views.
    • Verify that users cannot see, update, or delete another user's data, checking for a 403 Forbidden or 404 Not Found response.
  • Assert Database State: When testing POST requests for unauthorized actions (like delete), always assert that the database was not changed.

Preview of the Next Lesson:

We have now concluded our module on testing. You have the skills to ensure your application's core logic, forms, authentication, and authorization are all working as expected. In the next module, "Enhancing the Application," we will shift our focus from backend logic to the user experience. Our first lesson will be on integrating a CSS framework like Bootstrap to style your application and make it look professional.