Authorization & Permissions
User Authentication answered "who is this?" — Django confirms a username and password match, and hands back a logged-in user. That's only half the security story. The other half is authorization: once you know who someone is, what are they actually allowed to do? A logged-in TaskFlow user shouldn't necessarily be able to delete any task in the system, or archive someone else's project. This lesson covers Django's permission system, which is how that "what are they allowed to do" question gets answered.
Authentication vs. Authorization
It's easy to blur these two together, so it's worth stating the difference plainly: authentication
proves identity, authorization decides access. Django keeps them as genuinely separate concerns —
request.user tells you who is logged in, and the permission system, covered here, is a
completely independent layer that decides what that user can do once identified.
Permissions Django Creates Automatically
For every model you define, Django automatically creates four permissions the moment you run
migrations — one each for adding, changing, deleting, and viewing instances of that model. Each
permission is named <app_label>.<action>_<modelname>. For TaskFlow's
Task model, that means:
| Permission | Grants |
|---|---|
taskflow.add_task | Creating new Task rows |
taskflow.change_task | Editing existing Task rows |
taskflow.delete_task | Deleting Task rows |
taskflow.view_task | Viewing Task rows |
The same four are created automatically for Project and Tag as well — you
never have to define them yourself. They exist whether or not you ever explicitly use them, and
they're what the Django admin checks internally before letting a staff user edit or delete something.
Checking a Permission with @permission_required
The simplest way to protect a whole view is the permission_required decorator — it
checks the permission before the view body runs at all, and redirects (or returns a 403) if the
logged-in user doesn't have it:
Example: Protecting the Delete-Task View
# taskflow/views.py
from django.contrib.auth.decorators import permission_required
from django.shortcuts import get_object_or_404, redirect
@permission_required('taskflow.delete_task', raise_exception=True)
def delete_task(request, task_id):
task = get_object_or_404(Task, pk=task_id)
task.delete()
return redirect('task-list')
With raise_exception=True, a user missing that permission gets a clean 403 Forbidden
page instead of being silently redirected to a login page — usually the clearer choice for a user
who is already logged in but simply lacks the right to perform this action.
Checking a Permission Manually with has_perm()
Sometimes you need the permission check to be a condition, not a hard gate on the whole view — for
example, deciding whether to render a "Delete" button in a template at all. Every user instance has
a has_perm() method for exactly this:
Example: Conditionally Showing a Delete Button
# taskflow/views.py
def task_detail(request, task_id):
task = get_object_or_404(Task, pk=task_id)
can_delete = request.user.has_perm('taskflow.delete_task')
return render(request, 'taskflow/task_detail.html', {
'task': task,
'can_delete': can_delete,
})
The can_delete flag can then drive an {% if %} block in the template to
show or hide the delete button — the permission itself is still enforced properly inside
delete_task, so hiding the button is a usability nicety, not the actual security boundary.
Custom Permissions Beyond Add / Change / Delete / View
The four automatic permissions cover ordinary CRUD, but not every meaningful action in an app is a
create, update, or delete. Archiving a project in TaskFlow is a good example — it's neither exactly
a "change" nor a "delete," and deserves its own permission. Custom permissions are declared in a
model's Meta class:
Example: A Custom can_archive_project Permission
# taskflow/models.py
class Project(models.Model):
name = models.CharField(max_length=150)
description = models.TextField(blank=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
permissions = [
('can_archive_project', 'Can archive a project'),
]
After migrating, taskflow.can_archive_project exists as a real permission you can check
with either @permission_required or has_perm(), exactly the same way as the
automatic ones — Django doesn't treat custom permissions any differently once they're created.
Object-Level Checks: "Your Own Tasks Only"
There's a category of rule the permission system above simply doesn't express: taskflow.change_task
answers "can this user edit tasks in general," not "can this user edit this specific task."
A regular TaskFlow member should be able to edit their own tasks but not someone else's, even though
both are the same model and the same permission. This is called an object-level check, and for a
straightforward ownership rule like this, plain Python inside the view is often clearer than reaching
for a third-party object-permission library:
Example: Enforcing Task Ownership in a View
# taskflow/views.py
from django.core.exceptions import PermissionDenied
def edit_task(request, task_id):
task = get_object_or_404(Task, pk=task_id)
if task.project.owner != request.user:
raise PermissionDenied
# ... proceed with the edit form for the task's owner
Raising PermissionDenied is what triggers Django's standard 403 Forbidden response, the
same outcome as a failed @permission_required check. The rule itself — comparing
task.project.owner against request.user — is plain Python, not a special
Django API, which is exactly why it's easy to read and easy to trust.