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.

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.

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.

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.

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

❌ 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.

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.

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.