Locked learning resources

Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Locked learning resources

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

DRF Permissions

00:00 In the previous lesson, I gave a quick review of the Django authentication module. In this lesson, I’m going to use the accounts created in the previous lesson to show you how DRF permissions work.

00:12 In case you skipped the previous lesson, I’ll just quickly run through what I did. I installed the django.contrib.auth module. I added a login page and I created two accounts, one called indy with the staff parameter set to True and one called marion.

00:29 Okay, the background’s in place now, I’ve got some accounts so that I can show you who can do what with different permissions. Now I’m going to add another app for books.

00:38 This will be where I demonstrate the permissions. If you’re writing the code yourself while you’re following along, you’re going to need to change the settings file to add books to the INSTALLED_APPS.

00:48 Change the urls file to include books.urls, and then create the files that I’m going to show you for your models, serializers, views, and urls.

00:58 Like before, you’ll need to create your own admin file or pick one from the supplementing materials if you wish to use the admin to create your data.

01:07 You’ll need to run makemigrations and migrate as necessary on the models and then use the admin or a loaddata command to get data in so that you can play with the REST content.

01:20 Here’s the model file for my newly created books app. I’m creating an ORM class called Book that has two fields: title, the title of the book, and whether or not the book is restricted, which is a Boolean.

01:35 Similar to the artifacts, I’m going to create a BookSerializer. It’s the minimum possible serializer, taking everything in the Book model and serializing it.

01:47 Inside of the book app’s urls file, I’m going to do two things. First, I’m going to register a router for the BookViewSet, which I’ll be defining shortly.

01:56 That router is defined on line 8 and then included on line 11. And then on line 12, I’m also adding a view, which is the home view for this area. You may recall that inside of the settings file, I set the login redirect to be books/library.

02:14 Well, there’s going to need to be a view there, so I’m setting the path up for that now.

02:21 And now here’s the views file. It’s a little longer than things from before. First off, down at the bottom, is a view called library().

02:30 This function is pretty simple. All it does is render the library.html template. Do note though, that the view is wrapped with the @login_required decorator.

02:39 In a second, I’ll show you the HTML that gets rendered, but what this page does is show you who is logged in, provide a link to the REST API and a button for logging out.

02:49 All of that will be rather useful when I try to demo this shortly. The second thing in this file is the BookViewSet. The only difference between this and the ArtifactViewSet you saw earlier,

03:02 is the newly added .permission_classes attribute. .permission_classes takes a list. In this case, I’m passing in the IsAuthenticated class.

03:13 IsAuthenticated comes from the rest_framework.permissions module. With this permission class in place, the BookViewSet will only be visible to people who are authenticated via Django.

03:28 And this is library.html that gets rendered through the library view I just showed you. It shows you who’s logged in, has a link to the book listing API, and finally, a form for logging out.

03:43 You used to be able to use just a link to log out of Django, but Django 5 has changed it, so you must use a post. This has to do with security stuff. The logout view can be looked up by using the url tag since that view got registered with the other account management views.

04:01 And since this is a form, you need the csrf_token tag inside of it.

04:09 Here I am back inside of the admin. I’m gonna go to the logout link and log out of the account. Now let me visit the books/library/ url.

04:23 The library page home view requires you to be logged in As a result, it redirected me to the login page, which I created inside of the templates/ directory before.

04:36 Now I’m gonna log in as indy, and here I am at that Library homepage. It shows me that I’m logged in as indy and provides a link to the Books API.

04:46 Let me visit that… and here’s the result, The listing of books. Going back to the Library page, logging out, and then going straight to the books listing and you’ll notice what comes back from the REST API is permission denied.

05:05 I’m not logged in, so it doesn’t let me see the list. So far so good. IsAuthenticated stops anyone who isn’t authenticated from seeing your REST API.

05:17 Let’s experiment a little bit. I want to change the permission class. Right now it’s IsAuthenticated. Let’s change that to something else.

05:30 Here’s IsAdminUser, also part of the DRF permissions library. Going back to the same books entry point, still not authenticated.

05:42 That makes sense because I still haven’t logged in. Now visiting the login page, logging in as admin,

05:53 and there’s my books API. So far so good. Let me go back, log out. This time I’m going to log in as indy.

06:11 Here I am as indy, visiting the books API. Uh oh. That’s not quite what I had planned. It turns out that DRF’s IsAdminUser really means is staff.

06:24 Because the admin account is both a super user and staff, it’s allowed in. And because indy is staff, he’s allowed in. If I logged in with marion, it would forbid me because she’s neither.

06:38 So let’s make some changes to the code to fix that. I’m back inside of views.py, and now I’m going to add my own custom permission class. This is called IsSuperUser, and it inherits from the BasePermission class.

06:54 It has one method which is .has_permission(), and inside of that I’m going to look at the request object, the .user who’s associated with the request object, and I’m going to check the IsSuperUser property and return that as the value for .has_permission().

07:10 Now, if the user is a superuser, they’ll be allowed in and if they’re not, they won’t. So in this case, indy who’s only staff should be denied.

07:21 I also have to change the .permission_classes attribute on the BookViewSet. Change that from IsAdminUser to my newly created IsSuperUser.

07:34 Here’s the web browser again, I’m still logged in as indy going to the Books API and indy no longer has permission. That’s good! Let me just go back, log out,

07:51 log in as admin, go to the Books API and there’s the listing. IsSuperUser works! You can also chain permissions together.

08:03 Let’s add another custom permission, this time checking whether or not you’re indy. Same as before, inheriting from BasePermission, but this time I’m gonna make a change.

08:13 I’m going to override the .has_object_permission() method. This allows me to specify permissions at the object level rather than at the global level.

08:23 What I’m going to do here is check whether or not the object itself, which in this case is a Book, is restricted. So this is happening on line 17.

08:34 If the book does not have the .restricted flag, then anyone is able to see this because I’m always returning True. If you get to line 20, the Book is restricted and in this case it checks whether or not the username is indy.

08:48 If it is, indy is allowed to see the restricted books, everyone else is not. The other change that I have to make is inside of IsSuperUser because I’m going to combine these two permissions. By default inside of BasePermission, permission is granted.

09:06 So if I chain IsIndy with IsSuperUser and I don’t change the .has_object_permission() for IsSuperUser, it will always be granted, which is something you have to be very careful with.

09:18 Personally, if I were designing the framework, I would make permission denied be the default rather than permission accepted. So you have to be very, very cautious when you play with this stuff inside of the DRF.

09:29 I have already shot myself in the foot several times with this particular feature accidentally opening up permissions when I didn’t mean to.

09:39 Let me show you the change. Defining .has_object_permission() inside of IsSuperUser.

09:52 and setting the return value the same as .has_permission(). So this makes sure that if the permissions being checked at the global level–at the request level–you check whether or not it IsSuperUser as well as at the object level.

10:07 Finally, I’m gonna update the permission classes and I’m gonna change this to chain the permissions together. You do this with the OR operator (|)..

10:17 Now when someone visits the view it checks the IsIndy permission. If that passes, they’re allowed in. If it does not pass, then the IsSuperUser permission is checked.

10:31 Back at the library homepage here. I’m just gonna log out and log in as indy.

10:40 Visit the Books API. And you’ll notice that everything here is listed. Now I’m gonna visit a particular title by changing the URL to look at book number 2, which is a restricted book.

10:56 You’ll notice that indy can see this. This is the expected behavior. I’m sure you can tell by the way I’m demonstrating this, that there’s a but coming.

11:07 Let me log out and show you the problem.

11:14 Logging in is marion, visiting the Books API, and there’s the problem. Notice that marion can see everything including the restricted books.

11:25 So what happened here? Well, it is actually working, it’s just that the listing and editing are different concepts and the permissions only apply to the editing, not the listing.

11:38 The listing is done through a separate mechanism through your queryset. To show that this is working. I’m gonna visit the specific URL for "Tanis and You".

11:53 Because I’m logged in as marion, I now get permission denied. So it’s working, just not the way you might expect.

12:03 So you’ve seen that the .permission_classes only affects whether or not you can actually touch the object. Specifically, it doesn’t affect the listing.

12:11 This is a little counterintuitive and unfortunately allows you to accidentally expose things through your API that you may not have intended. In order to have the behavior that I want here, so that restricted books don’t show up in the listing, I have to modify not just the permission classes, but also the queryset.

12:30 Here’s a subset of the views class, just the ViewSet. I’m gonna change the queryset now to include a filter.

12:40 Inside of the queryset you can get at the .request object and you can decide who the request is for. In this case, I’m checking whether or not the user is staff.

12:50 If the user is staff, then all books go back no matter what. Essentially ignoring the restricted field. Otherwise, so users who are not staff or not logged in–you will get a filtered version of the queryset.

13:07 And that filter only shows the non-restricted books.

13:13 Still logged in as marion, clicking on the Books API. And now my book listing no longer has "Tanis and You", because it’s restricted.

13:25 So you’ve learned how to use permissions. More importantly, you’ve learned how to be very careful with permissions. First off, default permissions allow entry rather than deny them.

13:35 That can be a problem if you mess things up. Secondly, permissions don’t apply filters. So if you want to make sure that only things with permissions show up inside of your listings, you have to do more than just set the permission.

13:49 You also have to change your queryset. There’s a lot of power here and I always hesitate when I criticize somebody who’s written open source software because more power to them, we’re all using it, and that’s fantastic.

14:02 That being said, you gotta be a little careful here or you’ll end up with some toes missing.

14:09 So that’s the permission mechanisms. Next up, I’m gonna do a deeper dive on serialization and show you how to do some fancier stuff.

Avatar image for DjangoUser

DjangoUser on May 23, 2021

Hi, Thank you for the course. I have few doubts for the permissions section

  1. When we have checked the permission at object level with has_permission method inside isSuperUser why do we need to check it again at the object level? Is there a specific precedence for it ?

  2. Does permission classes apply restriction at read level (single object) and queryset needs to be used for list level filtering ? Should not they be restricting at the list level as well because global permissions will kick in?

Please let me know, thanks!

Avatar image for Christopher Trudeau

Christopher Trudeau RP Team on May 23, 2021

Hi testuk08test,

I’m with you, this is less than intuitive and honestly any time I write this kind of code I make sure to test extensively, as the defaults can mess you up.

IIRC, what is happening here is that in the first case – just IsSuperUser – you never get around to calling the has_object_permission() method, because it is all or nothing, super-user or not.

When you’re doing the IsIndy chained with IsSuper user, the has_object_permission() method on IsIndy is called, if that fails, it chains to the same method on IsSuper user. The problem is BasePermission provides a default implementation of has_object_permission() that returns true. In the first case you never get to this call, so there isn’t a problem. In the chained case, you can get to this call, and if you don’t overwrite it you’ll be exposing the object.

The safest thing would be to always implement both methods so you know what it is doing.

As to your second question, permission classes only get triggered when viewing an object, not when viewing an object listing. This is likely due to an optimization – if you’ve got a long list of data coming back you want to manage stuff on the database side, rather than invoking a call for every single object. Whatever their reason for implementing it this way, they’ve separated a single object get/put from a listing. To truly restrict access you need both the object permissions and a change to the query set listing.

DRF documentation is decent for the most part, but I find the permission stuff a bit light. If you’re writing code where this is important make sure to test a lot, otherwise you might expose data that you didn’t intend to.

Avatar image for SeanmWalker

SeanmWalker on Aug. 23, 2021

Well this video was as clear as mud. I guess I will stick with the documentation on permissions.

Avatar image for Christopher Trudeau

Christopher Trudeau RP Team on Aug. 24, 2021

Hi Sean,

Sorry it wasn’t clear for you. Something I can help make better?

Avatar image for Thierry Gagné

Thierry Gagné on Oct. 10, 2024

I just wanted to say that since Django 5 (Dec 2023), this video no longer works.

Reading online, it seems that the issue is that the old approach used a GET method which could be unsafe and Django now requires a POST: forum.djangoproject.com/t/deprecation-of-get-method-for-logoutview/25533

I was not able to find a nice and easy fix for implementing this with the course material. I was able to make a new template with a logout button, but then I was not able to redirect to the course’s custom login page. At best, I could be redirected to the Django Admin’s login page. And if I accessed the /books/library page without being logged in, the page would print out an error.

For the future, please write alternative steps for Django 5+.

Avatar image for jwil

jwil on Nov. 16, 2024

I had the same issue as Thierry. I had the following solution:

from django.contrib.auth import logout

Then added this function:

def logout_view(request):
        logout(request)
        return HttpResponse("You are logged out.", content_type="text/html; charset=utf-8")

and finally, added to the urlpatterns list in books/urls.py:

path('logout/', views.logout_view),

I could certainly add an output variable that had more than “You are logged out” or look more closely at implementing a redirect, but it does seem to do the trick for now. (thanks to Django docs!)

Avatar image for Christopher Trudeau

Christopher Trudeau RP Team on Nov. 17, 2024

Hey folks,

Sorry I meant to comment on this and it slipped my mind. This is a Django 5 thing rather than something specific to the DRF. The approach jwil takes solves the problem but isn’t Django’s recommended mechanism. They changed this to requiring a post to get around a security issue and needing a CSRF token. For playing around, writing your own view works fine.

We’re in the process of updating the course to show the new mechanism. In the meantime, this Stackoverflow post covers how to use a form instead:

stackoverflow.com/questions/77690729/django-built-in-logout-view-method-not-allowed-get-users-logout

Become a Member to join the conversation.