Object level authorization in a Django Rest Framework viewset - django

I'm creating an API with Django Rest Framework (DRF) and wondering where I should handle object level authorization.
So far I've created an Organization model and a custom user model with email being the unique identifier instead of username. Organizations and users are currently connected through a many-to-many field.
What I'd like to do is make sure that when a user hits my API they're only able to do the standard CRUD actions on the models that are linked to the users respective organization. As an example here's my current UserViewSet where I've overriden then get_queryset method to filter the User queryset to only return other users who are apart of the same organizations as the user calling the API:
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
def get_queryset(self):
User = get_user_model()
user = self.request.user
organizations = user.organization.all()
return User.objects.filter(organization__in=organizations)
What's considered best practice to extend these limitations to other viewset actions? For example, if I want to make sure that the user can only create and add other users into the organizations they're linked to, should I override the create method in the viewset and perform a validation there that the organization which was passed in the request data is the same as the organization that the user calling the API belongs to?
My gut feeling is that I'd end up breaking DRY this way, because I'd be overriding all the viewset actions and repeating nearly identical overrides. Is this intuition wrong? I guess I could separate the "validations" into a separate services.py file and call them in the overriden actions. Should I instead offload these checks to custom permissions? Or should I disregard the views completely and put the validation into the serializers?

Actually, you need different tools for different DRF CRUD actions. Personaly, I love to use rules package
name=XXX-list : /XXX/
list : permissions through get_queryset() filtering
create : permissions through rules and payload validation with serializer
name=XXX-detail : /XXX/{id}
retrieve : permissions through get_queryset() filtering
partial_update, update and destroy : permissions through rules and get_queryset() filtering
You will probably need to write a custom DjangoObjectPermission class to integrate rules

Related

How to associate “owner” with default User model?

I’m trying to teach myself Django and the Django Rest Framework, but I'm having a hard time understanding how object ownership is defined.
I want to apply the custom “IsOwnerOrReadOnly” permissions given in the DRF documentation with the default User model. My goal: Users will be able to PUT/PATCH/DELETE the information in their own account, but won't be able to change any other users’ information.
My user serializer:
class UserSerializer(serializers.ModelSerializer):
rides = serializers.PrimaryKeyRelatedField(many = True, queryset = Ride.objects.all())
class Meta:
model = User
fields = [
'pk',
'username',
'first_name',
'last_name',
'email',
'password',
'rides'
]
write_only_fields = ['password']
read_only_fields = ['pk', 'username']
The IsOwnerOrReadOnly code:
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user
The problem is with the line return obj.owner == request.user. It always returns AttributeError: 'User' object has no attribute 'owner’. Why is the "owner" of a user not the user itself? How can I say “owner = this object” so that my permissions classes work?
Follow-up question: I would like to eventually extend the default User model to add more fields. Would my permissions still work if I made a new User model by inheriting from AbstractBaseUser? (I just started learning Django, so I'm not entirely sure what the process for creating a new User model would be. But I thought it might be good to mention in case that would change how I define permissions.)
You're getting the error because there's no such abstract concept as owner for all models. You will have to define the relationship between the object and the user in the model and apply that in the permission for each model.
In this case, you want to be sure that the user is trying to modify his own user object so you can just do this:
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj == request.user
So in essence, this will only work for the User model and you will have to extend it to work for other models based on the relationship of the model with the user.
You may want to define different permission classes for each resource than trying to put all the logic in the same permission class.
As for your question on extending the user model. This page in the docs explains the different methods of extending the existing user model which is basically by either extending from the AbstractUser or AbstractbaseUser. The Django permissions framework will still work as it is implemenmted against those base classes.
Bear in mind however that DRF permission classes are different from Django permissions. You can Django permissions to implement the logic inside the permission classes but they can be implemented without the permissions.
EDIT FROM COMMENT
If you have a Ride model with owner, you can do this to combine them.
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
if isinstance(obj, Ride):
return obj.owner == request.user
return obj == request.user
Ownership
Say there is a Model A. To define ownership, you need to create a field in Model A that will point to User model. You can do this easily. Make sure you use django.contrib.auth.get_user_model for same.
Getting a list of owned objects from API
When it comes to getting a list of objects owned by the authenticated user (request.user), you are looking at creating a filter. To do same there are two ways:
Create a filter class and use in it generic views. This is more extensible. I have done same in drfaddons.filters.IsOwnerFilterBackend. Here's the source code.
Override def filter_queryset(self, queryset) in each API Class.
Ensuring in Retrieve/Update/Delete
This time you want to make sure that permission check is being applied. Here, you will want to implement has_object_permission and has_permission from permissions.BasePermission which you're doing.
In general, you'll want to use both filtering and permission check.
Implementing across the project
To implement this across the whole project, you need to set your filter and permission as default one in settings.py's REST_FRAMEWORK configuration. You'll also need to ensure that an owner field (with same name preferably) is present in each model. (Check Abstract model).
I have done same in all my project and hence, I have created a package of the same. Check DRF Addons. You can also install it via pip: pip install drfaddons. It will do all of the above said task.

Django REST auth - users stored in external service

I've been wondering the best way to handle the case where a Django is used in a service-oriented architecture, so individual Django applications do not maintain their own user stores. One service maintains the user store, and others must call it in order to retrieve user information.
So far example, here is how I was thinking of building a custom authentication class in Django REST to handle this:
class SOAAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
token = request.get_token_from_auth_header()
if not remote_auth_service.is_token_valid(token):
raise AuthFailed('Token is invalid')
user_properties = remote_users_service.get_user(token):
# user_properties is a dict of properties
if not user_properties:
raise AuthFailed('User does not exist.')
user = MyCustomUserClass(**user_properties)
return (user, 'soa')
So no user info would get persisted in the Django application's database, and permission classes could interrogate the instance of MyCustomUserClass to figure out what groups a user belongs to.
Would this approach work for simple group-based authorization? My think is that I don't need object-level permissions, so there's no need to create rows for my users in the Django database.

Django restrict views by User permissions which are editable in the Admin

I have a Django application where I need to restrict specific Views to subset of Users. I also want to be bale to edit which Users have this permission via the Django Admin. So in the admin I would like to be able to see all users and have a check box which can be checked to give permission to see this specific Views.
I believe the way to approach to this is to a permissions decorator on the Views in question:
from django.contrib.auth.decorators import permission_required
#login_required
#permission_required('user.can_view_restricted', login_url='/accounts/login/')
def Restrictedview(request, template_name='restricted.html'):
...
# restricted stuff
Now I know I need to define this permission (in permissions.py?), and register it with the Admin. I am unsure of how to do this and how to properly associate the permission with a specific User instance. Should this be an extra field on 'User', or a separate model to hold model to hole Users and Permissions?
You can read in details about django permissions in the docs
https://docs.djangoproject.com/en/dev/topics/auth/default/#permissions-and-authorization
Basically Django permissions use the Permission model, which is found at django.contrib.auth.models, but for most applications you don't need to directly import or use that model.
By default Django creates 3 default permissions for any model you have in your app. If you have a model named MyModel in an app named myapp, then Django will create create_mymodel, change_mymodel, and delete_mymodel permissions by default.
You can check if the user has a certain permission by calling
user.has_perm('myapp.create_mymodel')
if you're checking for the create permission for example. Or, like you did, you can use the decorator
permission_required('myapp.create_mymodel')
In addition to the default permissions provided by django, you can define custom permissions on your models by specifying the permissions attribute in the Meta class of your model like this:
class MyModel(models.Model):
[...]
class Meta:
permissions = (
("can_deliver_pizzas", "Can deliver pizzas"),
)
More on defining custom permissions here: https://docs.djangoproject.com/en/dev/ref/models/options/#permissions
By default, permissions can be easily edited for every user using the admin interface. Just visit a certain user's page and there will be a field named User Permissions with a list of all permissions in your project, from which you can add or remove permissions for your particular user.

Exposing a many to many field using Django REST Framework?

I'm creating an event following/signup system that uses Django REST Framework and can't figure out how to properly set this up.
In my events model I have:
followers = models.ManyToManyField(get_user_model(), related_name='following')
Ideally, an authenticated user could use a POST or PATCH to add or remove themselves from the followers record for a given event. Though I'm not really sure what the best way to do that would be.
My current thinking would be to create a serializer that only exposes the followers field, then create an APIView using that serializer with login in the get and post/patch methods to add or remove the specific user.
I'm getting the feeling that this is over-complicating things though. Is there an easier way to do this?
What do you think about using a 'through' model for the M2M relation?
I mean:
class Follower(...:
user = FK user
event = FK event
...
followers = models.ManyToManyField(get_user_model(), through=Follower ...)
...
In this case you are able to create quickly a model serializer and a generic view for the model Follower. In order to add or remove a user to an event you just send POST or DELETE requests to this resource

Django REST Framework: restrict data records access to the users created them

I'm trying to figure out, what is the best way to manage model access permissions with Django.
I have a table of items which belong to the users created them. All the items a managed via a RESTful API. When using this API I want to limit access to the items created by a given user.
Do I have to create several tables or is it possible to achieve the same with just one table?
If I have to use multiple tables, how do I correlate API requests with a particular table?
Ok, I found a way to do it via both API and admin. This basically resembles Rob's idea.
First of all, every time I create a new user via admin panel, I need to append a user to my items:
class MyAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
if getattr(obj, 'user', None) is None:
obj.user = request.user
obj.save()
admin.site.register(MyItem, MyAdmin)
Then when accessing my model, I just filter by user (which is btw a foreign key to django.contrib.auth.models.User):
MyItem.objects.filter(user=request.user)
Finally to make it work with Django REST Framework, I need to add a couple of methods to My custom ModelViewSet:
class MyItemViewSet(viewsets.ModelViewSet):
model = MyItem
serializer_class = MyItemSerializer
def get_queryset(self):
return MyItem.objects.filter(user=self.request.user)
def pre_save(self, obj):
obj.user = self.request.user
I've used documentation and (lots) trial and error to figure this out.
if you want generic object-level permissions then you can use a permission backend like django-guardian, here is examples for integrating with django-restframework
You could have a created_by field on your objects. Then compare the django user to that.
It's a little difficult help further without a concrete example.
Using django-rest-framework and django-rest-auth, I had to perform additional steps as described in the answer by Lucas Weyne to this question.
Here's what he suggested that worked for me.
Make sure to include the authorization token in the request:
curl -H "Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b" <url>
This is key because otherwise the server doesn't know who the user is.
Add this to your viewset definition:
permission_classes = [permissions.IsAuthenticated, ]
This is important if the query is user dependent.