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.
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
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.
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
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
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.
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.
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.
Inside of the
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.
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/". Well, there’s going to need to be a view there, so I’m setting the path up for that now.
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.
Secondly, I’m setting up a
BookViewSet. The only difference between this and the
ArtifactViewSet, is the newly added
.permission_classes takes a list.
In this case, I’m passing in the
IsAuthenticated class. Let me just scroll up for a second.
IsAuthenticated comes from
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.
I’m going to go to the LOGOUT link and log out of the account. Now let me visit the
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.
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.
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.
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.
IsAdminUser, also part of the DRF
permissions library. Going back to the same books entry point, still not authenticated.
That makes sense, because I still haven’t logged in. Now visiting the login page, logging in as
05:22 and there’s my books API. So far, so good.
05:28 Let me go back, log out.
This time, I’m going to log in as
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.
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.
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
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.
I also have to change the
.permission_classes attribute on the
BookViewSet. Change that from
IsAdminUser to my newly created
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,
log in as
admin, go to the Books API, and there’s the listing.
IsSuperUser works! You can also chain permissions together.
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.
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.
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
If you get to line 20, the
Book is restricted, and in this case, it checks whether or not the
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.
So if I chain
IsSuperUser and I don’t change the
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.
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
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.
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 (
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.
Back at the library homepage here, I’m just going to log out and log in as
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.
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.
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.
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".
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.
Here’s a subset of the
views class, just the
ViewSet. I’m going to change the queryset now to include a filter.
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.
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.
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.
Well this video was as clear as mud. I guess I will stick with the documentation on permissions.
Sorry it wasn’t clear for you. Something I can help make better?
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!