Django queryset permissions - python

I am building a quite complex Django application to be used on top of and email scanning service. The Django application is written using Python 3.5+
This application primarily uses Django Rest Framework to handle communication with the frontend in the browser.
The issue that I am currently having is that I try to implement the concept of a System Administrator, Domain Administrator and Application User
The System Administrator is basically the "normal" django superuser and is therefore capable of doing everything and see every record in the system.
The Domain Administrator is user who manages one or more email domains. I keep track of this using a Many2Many relationship between the users and the domains. The idea would then be to predefine a filter, so that the log of messages processed, will be automatically filtered to show only messages where the sender domain or the recipient domain equal a domain in the list of domains that the given user is assigned to.
The same would be true for blacklisting/whitelisting policies.
If the Domain Administrator is not assigned to any domains, then no data is shown.
The Application User is basically any authenticated user with one or more domains assigned to them, using the same Many2Many relationship as the Domain Administrator. If no domains are assigned, then no data is shown.
I have found some other solution here on Stackoverflow on making the request.user available to the QuerySet in the ModelManager, but that does not seem like the correct way to handle it.
I have looked at django-guardian, django-authority and django-permissions, but none of them seem to be affecting the QuerySet or the resulting list of objects.
Does anyone have a suggestion for Django package/addon that can be used to handle this or maybe an idea for how this could be handled?

I'm the author of django-cancan library https://github.com/pgorecki/django-cancan which strives to solve the exact problem you are describing.
The philosophy is as following: first, you determine per-user abilities, then in a view, you can check user abilities for a given object, model, or you can retrieve a queryset based on those abilities.
The declaration part looks like this:
def declare_abilities(user, ability):
if not user.is_authenticated:
# Allow anonymous users to view only published articles
ability.can('view', Article, published=True)
else:
# logged in user can view any article...
ability.can('view', Article)
# ... and change his own
ability.can('change', Article, author=user)
# ... and add new ones
ability.can('add', Article)
if user.is_superuser:
# Allow superuser to view and change any article
ability.can('view', Article)
ability.can('change', Article)
Then you can you can check for abilites on a per-object level:
def article_detail_view(request, pk):
article = Article.objects.get(pk=pk)
if request.ability.can('view', article):
...
or on a model level:
def article_create_view(request, pk):
if request.ability.can('add', Article):
...
or get a queryset with accessible objects:
def another_list_view(request, pk):
articles = request.ability.queryset_for('view', Article)
...

DRF's GenericAPIView has a get_queryset method that you can override to perform custom filtering:
def get_queryset(self):
qs = super(YourView, self).get_queryset()
return self.filter_queryset_for_user(qs, request.user)
def filter_queryset_for_user(self, qs, user):
pass # Your logic here
This is not necessarily a bad idea; DRF docstrings recommend overriding this:
You may want to override this if you need to provide different
querysets depending on the incoming request.

I think you are misunderstanding the concept of permission in Django. django-guardian, django-authority and django-permissions these all packages are for handling permission inside your Django application. What permission does is it checks certain model or instance of model, if the user has permission to view that particular model or object, otherwise it will return 403 Unauthorized response. Permission does not change or filter your queryset to return only the valid results.
Rather if you want to apply filter your queryset, you can do so by the above answer, or you can move that code to a Mixin to follow DRY Style. For Mixin Reference you can see this link:
https://reinout.vanrees.org/weblog/2015/06/03/10-mixins.html
https://www.youtube.com/watch?v=0513I6_f2Tc

My answer to this question also provides an alternative to filter your queryset by subclassing rest_framework.filters.BaseFilterBackend and implement filter_queryset() based on your permission pattern , which would be suitable for more complicated use cases.

Related

Using python builtins to pass django request from middleware to model in order to create default model filters for current user?

I have a fairly complex django application that has been in production for over a year.
The application holds data from different customers. The data is obviously in the same table, separated by customer_id.
Recently the client has started to ask questions about data segregation. Since the app is sold on a per user basis and holds sensitive information, customers have been asking if and how we maintain data segregation per customer, and are there any security measures that we take to prevent data leakages (ie. data from one customer being accessed by another customer).
We do our filters in the view endpoints, but eventually a developer in the team might forget to include a filter in his ORM query, and cause a data leakage
So we came up with the idea to implement default filters on our models. Basically whenever a developer writes:
Some_Model.objects.all()
essentially they will execute:
Some_Model.objects.filter(customer_id = request.user.customer_id)
We plan to achieve this by overriding the objects property on each model to point to a manager with a filtered queryset. Something like this:
class Allowed_Some_Model_Manager(models.Manager):
def get_queryset(self):
return super(Allowed_Some_Model_Manager, self).get_queryset().filter(
customer_id = request.user.customer_id
# the problem is that request.user is not available in models.py
)
class Some_Model(models.Model):
name = models.CharField(max_length=50)
customer = models.ForeignKey(Customer)
objects = Allowed_Some_Model_Manager()
all_objects = models.Manager() # use this if we want all objects
However our problem is that request.user is not available in models.py.
I have found several ways to solve this.
Option 1 includes passing the request.user to the manager each time. However since I am dealing with thousands of lines of old code, I don't want to go and change all of our ORM queries.
Option 2, included using threading.local() to set the request.user in the thread local data.
Something like this: https://djangosnippets.org/snippets/2179/
There is a module that seems to be doing this: https://github.com/Alir3z4/django-crequest
However, a lot of people seem to be against this idea... Namely these two discussions:
django get_current_user() middleware - strange error message which goes away if source code is "changed" , which leads to an automatic server restart
Django custom managers - how do I return only objects created by the logged-in user?
So that brings me to Option 3 which I came up with, and I can not find anybody else using it. Use the python builtins module to pass the user from the middleware to the model.
#middleware.py
import builtins
def process_request(self, request):
if request.user.id:
builtins.django_user = request.user
#models.py
import builtins
class Allowed_Some_Model_Manager(models.Manager):
def get_queryset(self):
if 'django_user' in vars(builtins):
return super(Allowed_Some_Model_Manager, self).get_queryset().filter(
customer_id = django_user.customer_id
)
else:
return super(Allowed_Some_Model_Manager, self).get_queryset()
I have tested the code and it is working on my local django server and on Apache with mod_wsgi. But I really want to hear if there are any pitfalls of this approach. I have never used builtins module before, and I am not sure if I understand how it works, and what is the use-case for it.

custom authentication in backend - Creating user at runtime without saving

While searching online on how to accomplish custom authentication in Django I came across this and this article. Both of these articles specified the same instructions. Currently i have something like this. (Taken from first article)
class Client(models.Model):
email = models.EmailField(unique=True, max_length=100)
password = models.CharField(max_length=128)
Then in another python file I have this
from .models import Client
class ClientAuthBackend(object):
def authenticate(self, username=None, password=None):
try:
user = Client.objects.get(email=username)
return user
if password == 'master':
# Authentication success by returning the user
return user
else:
# Authentication fails if None is returned
return None
except Client.DoesNotExist:
return None
def get_user(self, user_id):
try:
return Client.objects.get(pk=user_id)
except Client.DoesNotExist:
return None
I just started using Django and have kind of skipped the model section for db interaction since in my current project I am using RAW and custom SQL due to certain reasons.
My question is where does the
user = Client.objects.get(email=username)
get its user from . Do I have to make an entry into a database ?
What I want to do is to create a user during runtime and not save to the database.I tried doing this
#The following creates and saves a user in the db
u =User.objects.create_user('john', 'lennon#thebeatles.com', 'johnpassword',cust_address="my_custom_address",cust_msg="Users custom message")
The above returns a Client.DoesNotExist exception.
My question is where does the
user = Client.objects.get(email=username)
get its user from
Apparently, Client is a models.Model. This means that represents a single record of the relevant table in your database, wherever that is depending on the relevant settings.py setting.
Therefore, the table representing that model can be created with the next Django migration and lots of other useful things Django allows for.
So essentialy the above statement instructs the Django ORM to fetch all Client records from that particular table with that specific email. If no such entries exist, none will be returned.
I tried doing this
u =User.objects.create_user('john', 'lennon#thebeatles.com', 'johnpassword',cust_address="my_custom_address",cust_msg="Users custom message")
This is where you complicate things a bit. The create_user method is not part of Django ORM, but part of the Django default auth model manager django.contrib.auth.models.User. You should either provide such a method yourself, or easier to just use the standard create method provided with the default Django model manager.
Not saving the user model even on some cache does not make sense at all, as it implies that the user registers each time he or she wishes to login.
Having said all those, I would strongly recommend you to read the official Django documentation. All the above are covered, the documentation is very comprehensive and not that long. You can then read and understand tutorials on the wild which may or may not be correct or up to date.
Take a good read specifically on the Customizing authentication topic, as it provides additional methods far easier for the beginner.

Django Admin Non Staff Access Data Filtering

I'm writing an application in Django (which I'm very new to) where the admin area will be exposed to 'customers' of the application, not just staff/superusers, because of the nature of the application and the way Django automatically generates forms in the admin area with such little code..
As such I need to robust and manageable way to maintain authentication and separating data, so only data created by a user is seen by that user.
At the moment I'm just using the default admin package and changing permissions for 'client users' to filter what data they can see (I only want them to see data they've created) using code like the below:
class MyModelAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super(MyModelAdmin, self).get_queryset(request)
return qs.filter(user=request.user)
def save_model(self, request, obj, form, change):
# field not editable in admin area so handle it here...
obj.user = request.user
obj.save()
However as the application scales, I can see ensuring this type of data filtering becoming difficult to manage, for example if there are chains of foreign keys on certain tables(A->B B->C C->D), and to filter the table at the end of the chain I need to do various JOINs to get the rows which relate to the current user.
A couple of solutions I'm pondering are creating a separate admin app per user, but this feels like overkill and even more unmanageable.
Or just adding the user column to every Model where data filtering by user is required to make it easier to filter.
Any thoughts on the best approach to take?
First off, from experience, you're better off offering editing and creating functionality to your users in an actual django app, using Views. Generic views make this very easy. Once you let your users into the admin, they will get used to it and it's hard to get them to leave.
Additionally you should use contrib.auth.Group together with django-guardian to keep track of object-level permissions instead of implementing it yourself. It pays off in the long run.
If you want to make this experience on your own however, you have more than one sensible choice:
owner on root objects in the ForeignKey pyramid
owner on every model
To realize the first option, you should implement two methods on every model down the ForeignKey chain:
def get_parent(self):
"""Should return the object that should be queried for ownership information"""
pass
def owned_by(self, user):
"""Should return a boolean indicating whether `user` owns the object, by querying `self.get_parent().owned_by(user)`"""
pass
However, as you stated, this incurrs many JOINS if your schema is sufficiently complex.
I would advise you to store the information about the owner in every model, everything else is a maintanence nightmare in my experience.
Instead of adding the field manually to every model manually, you should use inheritance. However django provides bad built-in support for inheritance with relations: An abstract base model cannot define a models.ForeignKey, so you're stuck with table based inheritance.
Table based inheritance brings another problem with itself: Consider these models:
from django.db import models
from app.settings import AUTH_USER_MODEL
class Base(models.Model):
owner = models.ForeignKey(AUTH_USER_MODEL)
class ChildA(Base):
name = models.CharField(max_length=5)
class ChildB(Base):
location = models.CharField(max_length=5)
It is easy to find the owner of a given instance of ChildA or ChildB:
>>> obj = ChildA.objects.create(owner=Peter, name="alex")
>>> obj.owner
Peter
However it is non trivial to find all objects owned by a particular user:
>>> Base.objects.filter(owner=Peter)
<Base-object at 0xffffff>
The default manager returns a Base object, and doesn't contain information about whether it is a ChildA or ChildB instance, which can be troublesome.
To circumvent this, I recommend a polymorphic approach with django-polymorphic or django-model-utils, which is more lightweight. They both provide means to retrieve the child classes for a given Base model in the queries.
See my answer here for more information on polymorphism in django.
These also incur JOINs, but at least the complexity is manageable.

Django: Filter request results to only contain data related to the requesting user

I'm a Django beginner (though I do have experience in web development using Sails.js + Angular) so bear with me.
I have an existing application that uses REST API in communicating between Sails.js backend and AngularJS frontend. Now, we've found the backend to be unsuited for our purposes, and we're going to swap to using Django in near-future. Sails.js automatically creates the REST methods for the controllers while Django doesn't, so I suppose I'm going to use something like Django Rest Framework to create the API.
So yeah, I've found corresponding features for most things. The on thing I haven't found yet is a replacement for a Sails.js feature called "policies". They are functions that can be executed on queries to certain controller actions, and can be defined as model-specific, model-controller action-specific, and request type specific. For example, you can have an "authAccess" policy that checks that the user of a request is authenticated, and the policy gets executed before the actual requested controller method gets executed. You can also use these to modify request objects before passing them to the controller. Now to my actual problem:
Let's say I have a User model that has a many-to-one relation with another model, let's call it Book, meaning a user can own many books, but a book can only have one owner. Goody good. Now, we have a logged-in user that is making a query to find all of his books. He makes a GET request to /book. I want to ensure that the returned Book objects are filtered so that ONLY HIS BOOKS are returned.
So basically in Sails I was able to write a policy that altered the request parameters to be like {user: loggedInUser} so the resulting Book results were automatically filtered. This was handy, since I was able to use the same policy to filter other models, too, like DVD or Friend or whatnot. My question is - what would be the best way to implement the same functionality in Django?
Have a look at the documentation:
http://www.django-rest-framework.org/api-guide/filtering/#filtering-against-the-current-user
Most likely you are better off overwriting the get_queryset method in a model viewset. And you can make this a generic approach by creating a base class for your views, something like:
from rest_framework import generics, viewsets, mixins, generics
class OwnerModelViewSet(viewsets.ModelViewSet):
def get_queryset(self):
"""
This view should return a list of all the records
for the currently authenticated user.
"""
return self.model.objects.filter(user=self.request.user)
All your model viewset classes can inherit from that class. It would require the foreign key field to be always named "user" though. If that is not the case here is a slightly hacky way how you could find a foreign key field to the User table. Use with care.
from django.db.models.fields.related import ForeignKey
from accounts.models import User
def _get_related_user(self, obj):
'''
Search for FK to user model and return field name or False if no FK.
This can lead to wrong results when the model has more than one user FK.
'''
for f in self.model._meta.fields:
if isinstance(f, ForeignKey) and f.rel.to == User:
return f.name
return False

django admin - group permissions to edit or view models

I'm searching for a way to customize the Django Administration to support permissions based on the user group.
For example, I've just created the Developers group, now I've also created the Tickets model, with AdminModel to specify how to list data.
I'd like to have this model visible only by Developers, and hidden to each other not in this group (eg filter the view based on groups).
I've read a lot of documentations, but couldn't really find and understand what to do to have it working.
For security purposes I'd also need to check user groups at runtime when adding-deleting objects for a specific model (the one I've hidden to people outside the Developers group), otherwise it would only need to know the URL to use the model :s
It looks like a simple task, but maybe I'm missing something... any 3rd party middleware, or just a way to do it? I'm also ready to edit the administration views if needed, but I need to know what do to.
Thank you :-)
ModelAdmin has three methods dealing with user permission: has_add_permission, has_change_permission and has_delete_permission. All three should return boolean (True/False).
So you could do something like:
class TicketAdmin(admin.ModelAdmin):
...
def has_add_permission(self, request):
return request.user.groups.filter(name='Developers').exists()
def has_change_permission(self, request, obj=None):
return request.user.groups.filter(name='Developers').exists()
def has_delete_permission(self, request, obj=None):
return request.user.groups.filter(name='Developers').exists()
When False is returned from one of these, it's results in a 403 Forbidden.
I had tried hours to find a way to edit custom admin's(based on my custom model) permission by some click on screen,without many coding.
Use Django's /admin/auth/user/ "User permissions:part"
Finally I find this:
Just to install django-admin-view-permission
and I can change the staff's custom models' permission here
Also in the group part /admin/auth/group/add/ I can create a group has certain permission, and assign specific staff to their permission group.

Categories

Resources