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:

PermissionGrants
taskflow.add_taskCreating new Task rows
taskflow.change_taskEditing existing Task rows
taskflow.delete_taskDeleting Task rows
taskflow.view_taskViewing 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.

Note: These are correct, real Django permission patterns — but this website's own project doesn't run a live server, so none of the decorators or checks above execute on this page. Try them against your own TaskFlow views.
Next up: Assigning permissions to users one at a time doesn't scale once you have many users doing the same job — Groups & Permissions covers bundling permissions together for whole teams of users at once.