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.
- add_post — may create new Post rows.
- change_post — may edit existing Post rows.
- delete_post — may remove Post rows.
- view_post — may view Post rows.
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
- Previous: User Authentication
- Next: Django REST Framework Intro