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. In case you skipped the previous lesson, I’ll just quickly run through what I did.
00:17
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:30 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:39
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
, 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
. 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. 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:21
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:36
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:48
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:57
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.
02:08
You may recall that inside of the settings
file, I set the login redirect to be "books/library/"
. Well, there’s going to need to be a view there, so I’m setting the path up for that now.
02:22
And now here’s the views
file. It’s a little longer than things from before. First off, let me scroll down a little bit to show you the home view, the library()
method. This method’s pretty simple.
02:34 All it does is output some static HTML. This is pretty bad practice—you should be using template files inside of Django—but it’s easier to just show it to you this way.
02:45 What this homepage will do is show you who is logged in, provide a link to the REST API, and a link for logging out. All of that will be rather useful when I try to demo this shortly.
02:59
Secondly, I’m setting up a BookViewSet
. The only difference between this and the ArtifactViewSet
, is the newly added .permission_classes
attribute. .permission_classes
takes a list.
03:12
In this case, I’m passing in the IsAuthenticated
class. Let me just scroll up for a second. IsAuthenticated
comes from rest_framework.permissions
.
03:24
With this permission class in place, the BookViewSet
will only be visible to people who are authenticated via Django. Here I am back inside of the admin.
03:36
I’m going to go to the LOGOUT link and log out of the account. Now let me visit the books/library/
URL.
03:49
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:03
Now I’m going to 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:14 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 a permission denied.
04:33
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.
04:46
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.
04:59
Here’s IsAdminUser
, also part of the DRF permissions
library. Going back to the same books entry point, still not authenticated.
05:10
That makes sense, because I still haven’t logged in. Now visiting the login page, logging in as admin
,
05:22 and there’s my books API. So far, so good.
05:28 Let me go back, log out.
05:34
This time, I’m going to log in as indy
.
05:39
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, because the admin
account is both a superuser and staff it’s allowed in.
05:59
And because indy
is staff, he’s allowed in. If I logged in with marion
, it would forbid me, because she’s neither. So let’s make some changes to the code to fix that.
06:12
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:23
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 .is_superuser
property and return that as the value for .has_permission()
. 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.
06:51
I also have to change the .permission_classes
attribute on the BookViewSet
. Change that from IsAdminUser
to my newly created IsSuperUser
.
07:04
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!
07:15 Let me just go back, log out,
07:21
log in as admin
, go to the Books API, and there’s the listing. IsSuperUser
works! You can also chain permissions together.
07:33
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 going to make a change.
07:43
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.
07:54
What I’m going to do here is check whether or not the object itself, which in this case is a Book
, is restricted. This is happening on line 17. If the Book
does not have the .restricted
flag, then anyone is able to see this, because I’m always returning True
.
08:12
If you get to line 20, the Book
is restricted, and in this case, it checks whether or not the .username
is "indy"
.
08:19
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.
08:36
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. 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:01
I have already shot myself in the foot several times with this particular feature, accidentally opening up permissions when I didn’t mean to. Let me show you the change. Defining .has_object_permission()
inside of IsSuperUser
.
09:22
And setting the return value the same as .has_permission()
. So this makes sure that if the permission’s being checked at the global level—at the request level—you check whether or not it is superuser, as well as at the object level.
09:37
Finally, I’m going to update the permission classes, and I’m going to change this to chain the permissions together. You do this with the or operator (|
).
09:48
Now, when someone visits the view, it checks the IsIndy
permission. If that passes, they are allowed in. If it does not pass, then the IsSuperUser
permission is checked.
10:01
Back at the library homepage here, I’m just going to log out and log in as indy
.
10:10
Visit the Books API. And you’ll notice that everything here is listed. Now I’m going to visit a particular title, by changing the URL to look at book number 2
, which is a restricted book.
10:27
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. Let me log out and show you the problem.
10:44
Logging in as marion
, visiting the Books API, and there’s the problem. Notice that marion
can see everything, including the restricted books.
10:55 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 to the listing.
11:09
The listing is done through a separate mechanism through your queryset. To show that this is working, I’m going to visit the specific URL for "Tanis and You"
.
11:24
And because I’m logged in as marion
, I now get permission denied. So it’s working, just not the way you might expect. 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.
11:41 This is a little counter-intuitive 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:01
Here’s a subset of the views
class, just the ViewSet
. I’m going to change the queryset now to include a filter.
12:11
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. 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, and that filter only shows the non-restricted books.
12:43
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.
12:56 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. 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:19 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. That being said, you’ve got to be a little careful here or you’ll end up with some toes missing.
13:40 So, that’s the permission mechanisms. Next up, I’m going to do a deeper dive on serialization and show you how to do some fancier stuff.
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.
SeanmWalker on Aug. 23, 2021
Well this video was as clear as mud. I guess I will stick with the documentation on permissions.
Christopher Trudeau RP Team on Aug. 24, 2021
Hi Sean,
Sorry it wasn’t clear for you. Something I can help make better?
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+.
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!)
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.
DjangoUser on May 23, 2021
Hi, Thank you for the course. I have few doubts for the permissions section
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 ?
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!