Permissions & Decorators

Permissions in Django control what an authenticated user is allowed to do, and decorators like @login_required and @permission_required are wrappers you place above a view to enforce those rules — redirecting or blocking anyone who isn't allowed before the view ever runs.

Learn Permissions & Decorators in our free Django course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…

Part of the free Django course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

In this lesson you'll learn how to lock views down so only logged-in users reach them, how Django's built-in model permissions and groups work, and how to protect both function-based and class-based views cleanly.

What You'll Learn in This Lesson

1 The @login_required Decorator

The most common rule on a website is simply: you must be logged in to see this page . Django ships a decorator for exactly that. A decorator is a function that wraps another function to add behaviour before or after it runs, and you apply one by placing it on the line above a view with the @ symbol.

When you decorate a view with @login_required , Django checks request.user.is_authenticated first. If the user is signed in, the view runs normally. If not, Django redirects them to the login page defined by LOGIN_URL in your settings, attaching a next parameter so they come back afterward.

To really understand the decorator, it helps to see one written from scratch in plain Python. The example below builds a tiny login_required wrapper around a fake request, exactly like Django does under the hood.

2 Model Permissions, Groups & @permission_required

Being logged in is rarely enough — an editor should change posts while a regular reader should not. Django solves this with permissions . For every model you create, Django automatically generates four permissions: add , change , delete , and view .

Each permission has a string identifier in the form app_label.codename . For a Post model in a blog app, the change permission is blog.change_post . You can check whether a user holds it in code with user.has_perm('blog.change_post') , or enforce it on a view with the @permission_required decorator.

Assigning the same permissions to many users one at a time is tedious, so Django gives you groups . A group is a named bundle of permissions; add a user to the "Editors" group and they inherit every permission that group holds.

The runnable example models has_perm() with a simple user object, then shows how a permission check decides what an action returns.

3 Protecting Class-Based Views & user_passes_test

Decorators wrap plain functions, so they fit function-based views perfectly. Class-based views use inheritance instead, so Django provides mixins that add the same protection. A mixin is a small class you list as a parent to inject behaviour.

Use LoginRequiredMixin to require authentication, and PermissionRequiredMixin with a permission_required attribute to require a specific permission. The crucial rule: list the mixin before the base view class, so its check runs first.

For rules that don't map to a stored permission — say "only staff active before 2020" or "email ends in @company.com" — use the @user_passes_test decorator. You give it a function that receives the user and returns True to allow access or False to redirect to login.

🎯 Your Turn: Check the Permission

Fill in the blank so the function correctly checks whether the user is allowed to delete a post using the right method name.

Common Errors (And How to Fix Them)

❌ Adding parentheses to @login_required when you have no arguments... or forgetting them when you do

Bare @login_required works with no arguments, but to pass options you must call it: @login_required(login_url="/signin/") .

✅ Fix: use @login_required alone, or @login_required(login_url=...) with parentheses when overriding the redirect.

❌ PermissionDenied even though the user clearly has access

The permission string is wrong — it must be 'app_label.codename' , not just the codename. Writing 'change_post' instead of 'blog.change_post' never matches.

✅ Fix: always include the app label, e.g. @permission_required("blog.change_post") .

The mixin was listed after the base view class, so the base view handled the request first and the check never ran.

✅ Fix: put the mixin before the view base — class PostList(LoginRequiredMixin, ListView) , never the reverse.

📋 Quick Reference

Tool

What It Does

@login_required

Require a logged-in user on a function-based view

@permission_required("app.codename")

Require a specific model permission on a view

LoginRequiredMixin

Require authentication on a class-based view

PermissionRequiredMixin

Require a permission on a class-based view

@user_passes_test(fn)

Allow access only when a custom test function returns True

🏆 Mini Challenge: A Permission Gate

Build a small access gate that combines a login check and a permission check, returning the right message for guests, logged-in readers, and editors.

❓ Frequently Asked Questions

Lesson 18 complete — your views are locked down!

You now know how @login_required gates a view, how Django's add/change/delete/view permissions and groups work, how to enforce them with @permission_required and user.has_perm(), and how to protect class-based views with LoginRequiredMixin, PermissionRequiredMixin, and @user_passes_test.

🚀 Up next: Django REST Framework Intro — start building APIs on top of the foundation you've learned.

Practice quiz

What does the @login_required decorator require before a view runs?

  • That the user is authenticated
  • That the user is a superuser
  • That the request is a POST
  • That DEBUG is False

Answer: That the user is authenticated. @login_required checks request.user.is_authenticated and redirects to LOGIN_URL if not.

Which setting names the page that unauthenticated users are redirected to?

  • AUTH_URL
  • LOGIN_URL
  • REDIRECT_URL
  • LOGIN_REDIRECT

Answer: LOGIN_URL. LOGIN_URL in settings.py defines where @login_required sends logged-out visitors.

Which four permissions does Django create automatically for each model?

  • read, write, edit, remove
  • create, update, list, destroy
  • add, change, delete, view
  • get, post, put, patch

Answer: add, change, delete, view. Django generates add, change, delete, and view permissions for every model.

What is the correct form of a permission string for the Post model in the blog app?

  • change_post
  • post.change
  • Post.change
  • blog.change_post

Answer: blog.change_post. Permission strings are 'app_label.codename', e.g. blog.change_post.

Which method checks whether a user holds a permission in code?

  • user.has_perm('blog.change_post')
  • user.can('blog.change_post')
  • user.check_perm('blog.change_post')
  • user.permission('blog.change_post')

Answer: user.has_perm('blog.change_post'). user.has_perm('app.codename') returns True when the user has that permission.

Which decorator enforces a specific permission on a function-based view?

  • @login_required
  • @permission_required
  • @require_perm
  • @needs_permission

Answer: @permission_required. @permission_required('app.codename') gates a view on a stored permission.

What is a Django group?

  • A single user account
  • A database table
  • A named bundle of permissions
  • A URL pattern

Answer: A named bundle of permissions. A group is a named collection of permissions; members inherit all of them.

When listing mixins on a class-based view, where must LoginRequiredMixin go?

  • Anywhere in the list
  • After the base view
  • It is not needed
  • Before the base view class

Answer: Before the base view class. The mixin must come before the generic view so its check runs first.

By default, what does @permission_required return for a logged-in user who lacks the permission?

  • 403 Forbidden
  • 200 OK
  • 404 Not Found
  • A redirect to the homepage

Answer: 403 Forbidden. Without raise_exception=False, the user gets a 403 Forbidden response.

Which decorator allows access only when a custom test function returns True?

  • @permission_required
  • @login_required
  • @user_passes_test
  • @staff_member_required

Answer: @user_passes_test. @user_passes_test(fn) grants access only when the given test function returns True.

Continue this course