Add user profile to request.user - python

I have multiple User Types which I represent by user profiles in the form of a model:
Student
Teacher
I need access to the specific user profile on every request.
To avoid executing an extra query every time I would like to add directly a select_related to the request.user object.
I couldn't find anything about it in the docs. Does anyone know the best way to do that?

Interesting question. Looking at the source code of AuthenticationMiddleware and auth.get_user it seems that only thing you'll need to do will be to implement and use you own authentication backend. If you don't use any other custom backend features, you can subclass the ModelBackend, overriding only the get_user method to suit your needs:
class MyModelBackend(ModelBackend):
def get_user(self, user_id):
try:
user = UserModel._default_manager.select_related("profile").get(pk=user_id)
except UserModel.DoesNotExist:
return None
return user if self.user_can_authenticate(user) else None
Of course, you'll need to add it to your settings AUTHENTICATION_BACKENDS.

Related

Django Multiple Auth Models

I am working on a project where I need to have 3 types of Users.
Admin
Vendor
Customer
I want to be having seperate Models for all three of them Vendor & Customers instead of having a type field in a common User Model.
My first approach to this problem was to define all models by sub-classing the AbstractUser model
# models.py
from django.contrib.auth.models import AbstractUser
class Customer(AbstractUser):
pass
class Vendor(AbstractUser):
pass
And add a custom Authentication backend to handle the authentication of users based on the request object.
# settings.py
AUTHENTICATION_BACKENDS = ['backends.CustomAuthBackend']
And my backends.py file will contain the logic to authenticate users and use different models for each one based on the request object.
# backends.py
from __future__ import print_function
class CustomAuthBackend(object):
def authenticate(self, request, username=None, password=None):
# authenticate user based on request object
def get_user(self, user_id):
# logic to get user
However this does not work and looks like i also need to specifiy the AUTH_USER_MODEL in settings.py which is used for authentication.
Is it possible at all.Does Django allow to authenticate from 3 different tables. How can I go ahead with this ? Is there any other approach to this and what should I change ?
I have done similar staff a couple days ago, you are in the right approach. But there are a few other things needed to change to make it work. I'll explain what I did to make it success.
First, you have to custom your own user model, and you have to do it in the first place before you make the migrations. And also in the model file define different userManagers to manager different type of users. Then in your settings file you have to set AUTH_USER_MODEL and AUTHENTICATION_BACKENDS, AUTH_USER_MODEL is the default user model django will use for authentication and you can only set to one user model, but for AUTHENTICATION_BACKENDS you can have multiple backends,it's a list and django will loop every option inside to authenticate. by default it use django.contrib.auth.backends.ModelBackend, you can add your own auth backends. Check this on how to make your own authentication backend:https://docs.djangoproject.com/en/1.11/topics/auth/customizing/#writing-an-authentication-backend.
And depends on your application, you may also need to custom the serializer function and override some classes to make it work. In my own application, I was using DRF and JWT token to authenticate, so I also override some of the function which by default use the AUTH_USER_MODEL variable. In the end, I'm able to use admin model to login the admin page and use another custom user model to authenticate the application and get the JWT token. Anyway, always reference this page: https://docs.djangoproject.com/en/1.11/topics/auth/customizing/#customizing-authentication-in-django. Hope this can help you in your application.
django.contrib.auth is designed to work with one UserModel. Depending on what you want to achieve there are different apporaches.
If you just want to store different kind of profile/meta data for the different types of user you might use multi table inheritance - in this case you might stick with the default user model.
When it comes to different permissions based on the user type you should not solve this using separate classes. Instead use groups. This approach is much more flexible. It can nearly always happen that one person should belong to more than one group of users. When you model this based on the user classes you are in trouble.

Object-level Permissions

I am sure that this is fairly straightforward, but I have scoured the documentation and I can't quite figure out how to do this.
I have extended my User class to have two ManyToMany relationships to other users: trainers and teammates.
If a user owns an object (defined by a user ForeignKey on the model), then that user should be able to GET, POST, PUT, PATCH, and DELETE. I have set up these endpoints with ModelViewSet. If a user is a trainer of the owner, they should have the same privileges. If a user is a teammate of the owner, they should only be able to GET.
In a list view of these objects, a user should only see the objects they own and the objects where they are a trainer or teammate of the owner. If they try and access a detail view of an object where they are not the friend or the teammate of the owner, it should return a 403.
I extended BasePermission as follows to try and create this behavior -- I then added it to the ModelViewSet where I wanted this behavior.
class TrainerAndOwnerOrTeammate(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
user = request.user
owner = obj.user
if user == owner:
return True
if user in owner.trainers.all():
return True
if user in owner.teammates.all():
return request.method in permissions.SAFE_METHODS
return False
Since the REST Framework documentation specifies that this isn't run on a per-object basis for list views, I overrode get_queryset to filter by the request user.
The issue is now I get a 404 error, not a 403, if I try and access a detail view I shouldn't have access to. I understand why that's happening, but is there a way to fix it?
I think one solution is to drop get_queryset() and define a custom filter. Filter classes let you filter the queryset based on the request and view being accessed. Another way is to split your viewset into multiple individual views. Another way is to define get_object.
I ended up overriding the list method of the ModelViewSet and keeping the permission class. The permission class handled the detail views and actions, and the list method handled the list view.

Django raw_id_fields read_only permission

I have this Django project that I'm working on, which won't allow users to select an entry (User entries) on the raw_id_fields popup, if they don't have change permissions (which they can't have at all). That's really weird cause that doesn't happen with the select tag list if I remove the raw_id_fields attribute on my ModelForm class at admins.py.
How can this permission behavior be consistent if it changes according to a different interface setting? I mean, user only have permission to select users on the form if they are displayed as a select tag. It seems to me that it's a big consistency failure with the way Django permissions was designed, which, in my opinion, should have native can_view permission, in addition to can_|add, change, delete.
While googling around I found a few topics discussing this matter, but all of them end up with some really painful solutions that don't seem straightforward to me. I wonder if something so simple could have a straightforward solution that won't require lots of workarounds.
Here is an example that looks like my actual code:
models.py
class Project(models.Model):
manager = models.ForeignKey(User)
...
admins.py
class ProjectAdmin(admin.ModelAdmin):
raw_id_fields = ['manager',]
...
As you've said, this is really weird. Thats why I've opted to "extend" the django admin to my specific requirements sometimes.
The easiest way to meet this goal is by overriding the has_change_permission of the referenced ModelAdmin
As you have the request object as an argument of the method, you can evaluate:
if the request comes from a raw_id_field or not
if the user has permissions to see that models or not
any other constraint you have
A simple prototype for the method:
def has_change_permission(self, request, obj=None):
if obj is None and '_popup' in request.GET:
return True
return super(MyAdmin, self).has_change_permission(request, obj)

Custom authenticate method or maybe some other way to do the same? Django

this is a begginers question. I have a database in mysql with tables to store users with a username and a password, simple as that. I dont want to use the authentications backends and tables that django installs the first time you run 'SyncDb'. So my question is: how do i tell django to use the database that i created instead of looking for active Users in the pre defined databases.
I´m using this code but of course this is looking for users in the already mention 'auth_user' table.
class LoginView(FormView):
form_class = LoginForm
redirect_field_name = REDIRECT_FIELD_NAME
template_name = 'login.html'
success_url = 'index'
def form_valid(self, form):
username = form.cleaned_data['username']
password = form.cleaned_data['password']
user = authenticate(username=username,
password=password)
if user is not None:
if user.is_active:
login(self.request, user)
return HttpResponseRedirect(self.get_success_url())
else:
return self.form_invalid(form)
def form_invalid(self):
return HttpResponseRedirect(reverse('app_name:login'))
def get_success_url(self):
if self.success_url:
redirect_to = self.success_url
else:
redirect_to = self.request.REQUEST.get(self.redirect_field_name, '')
netloc = urlparse.urlparse(redirect_to)[1]
if not redirect_to:
redirect_to = settings.LOGIN_REDIRECT_URL
elif netloc and netloc != self.request.get_host():
redirect_to = settings.LOGIN_REDIRECT_URL
return redirect_to
def post(self, request, *args, **kwargs):
form_class = self.get_form_class()
form = self.get_form(form_class)
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid()
So that´s my begginers question. Thank you very much
PS: my form_invalid() is not working...(error: passing 2 parameters instead of 1) any suggestions please tell me. Again, thank you very much
We had to figure this out for our project and the initial confusion comes from the fact that you basically need to handle two issues in Django to make this happen:
1) Make sure that that app is routed to the correct database
1a) Make sure that other database can actually be written by Django
2) Create an "app" module to use to overwrite the default user model
Luckily this is pretty much straightforward once the problem is clearly defined.
https://docs.djangoproject.com/en/dev/topics/db/multi-db/
The first section "Defining your databases" will show you how to add additional databases to your project. Make sure you have credentials for Django to use to access, so depending on your access structure, that may include an additional step or not.
After that the section "Automatic database routing" will explain the general idea behind db routers. However, I recommend you check out this excellent discussion and leverage that code to make your database routing easier: http://justcramer.com/2010/12/30/database-routers-in-django/. This way you can just make your project use this router and define a lib in your settings.py (DATABASE_CONFIG) to tell each application where to route after setting DATABASE_ROUTERS to the code at the link. Make your default router the one with all the django stuff, then define your legacy db whenever necessary.
Lastly, for the user model check out the docs on "Customizing authentication in Django" (I can't post the link because this is my first answer and I do not have enough reputation). You will need to write a custom model (and potentially admin forms and custom authentication for permissions based on your implementation) and include it through your project settings.py with AUTH_USER_MODEL. The key section on that page is "Substituting a custom User model". If it is as simple as just matching a password to a user it should be mostly painless, however, be careful how your passwords are hashed. PASSWORD_HASHERS in your settings.py tells django the hashers to use on passwords when saving them (and the preferred order). If you want to keep a certain hashing scheme you need to move that to the top of the list, otherwise, Django will port it to the first listed and thus preferred scheme when it needs to do a PW access. This is actually a great feature as it will auto-migrate passwords to stronger schemes, but may be something to be mindful of if you need to integrate with other systems.
As a general pointer (and this is in no way intended as a RTM) the Django documents are very good and will typically answer your questions (and if not the source is quite readable and I've had to solve a few things that way). However, as you might notice, until you realize the issue you are trying to fix, and what Django calls it, it can be a little confusing to fit the pieces together. It will be worth your time if you plan to do anything at all with Django to get a good read of the primary documentation, possibly twice, to help make the pieces fit. Hopefully this answers your question and gives you a sense of where to go, as you can see this scenario can get a little broad so let me know if a more specific question gives you issues, otherwise these are the key points I recall implementing and during a code review as I wrote this.

Multiple versions of django admin page for the same model

In my django admin section, I'd like to show different versions of the admin page depending on what kind of user is currently logged in. I can think of a couple ways this might work, but haven't figured out how to do any of them.
Perhaps I could put logic into the admin.ModelAdmin to look at the current user and change the 'exclude' field dynamically. Does that work? Or maybe run different custom templates based on who's logged in, and have the templates include / exclude the fields as appropriate.
I could register two versions of the admin.ModelAdmin class, one for each type of user, and maybe restrict access through permissions? But the permissions system seems to believe fairly deeply in one set of permissions per model class so I'm not sure how to change that.
I could grab a couple of the widgets that are used in rendering the admin page templates, and include them in my own page that does the one specific job I need powerful users to be able to do.
I could set up multiple AdminSites and restrict access to them through the url / view system. But then I'm not sure how to register different admin.ModelAdmin classes with the different AdminSites.
Any advice on this would be appreciated.
Answer
Thanks for the hint. Here's how I did it...
def get_form(self, request, obj=None, **kwargs):
"""This dynamically inserts the "owners" field into the exclude list
if the current user is not superuser.
"""
if not request.user.is_superuser:
if self.exclude:
self.exclude.append('owners')
else:
self.exclude = ['owners']
else:
# Necessary since Admin objects outlive requests
try:
self.exclude.remove('owners')
except:
pass
return super(OwnersModelAdmin,self).get_form(request, obj=None, **kwargs)
There are quite a few hooks provided in the ModelAdmin class for this sort of thing.
One possibility would be to override the get_form method. This takes the request, as well as the object being edited, so you could get the current user from there, and return different ModelForms dependent on the user.
It's worth looking at the source for ModelAdmin - it's in django.contrib.admin.options - to see if overriding this or any other other methods might meet your needs.

Categories