Django user hierarchy - django

I'm to make a web app that will implement something like user hierarchy. I would like to do it like this:
1. Superuser makes users (with some permissions), who can also add users (only with permissions which they own). This users can also add users, and so on. No one should be able to edit users who have more permissions.
The thing is that I want to make Django Admin Panel avaliable for all of these users. Is it even possible to make such thing? I've searched the web and didn't find solution. Thanks for advice.

You'll need to create your own views for adding users if you want to control the created users' permissions. On the Django admin site, any user that can create users can create superusers.
From the Django docs on creating users:
If you want your own user account to be able to create users using the Django admin site, you'll need to give yourself permission to add users and change users (i.e., the "Add user" and "Change user" permissions). If your account has permission to add users but not to change them, you won't be able to add users. Why? Because if you have permission to add users, you have the power to create superusers, which can then, in turn, change other users. So Django requires add and change permissions as a slight security measure.

Every user who'll need access to the admin must have the is_staff=True flag. It's never a good idea to allow users not associated with your organization access to the admin. Seriously, just don't do it. If that's your plan, find another.
That said, it can be done, but it's not for the faint of heart. There's a lot involved. First subclass the default UserCreationForm and UserChangeForm (Auth uses two separate forms for it's admin). Override the __init__ method of each to pull out the request from kwargs (Forms don't get the request by default, but it's necessary here, so you have to do a bit of a workaround.) Then, subclass the default UserAdmin, set form and add_form to the new forms and override get_form (to pass in request) and each of the has_foo_permission methods to limit access. The queryset method also needs to be overrode so users only see users they can modify in the admin.
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from django.contrib.auth.models import Group, Permission
class CustomUserCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
pass
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(CustomUserCreationForm, self).__init__(*args, **kwargs)
# Limit groups and permissions to those that belong to current user
if self.request and not self.request.user.is_superuser:
self.fields['groups'].queryset = self.request.user.groups.all()
self.fields['user_permissions'].queryset = self.request.user.user_permissions.all()
class CustomUserChangeForm(UserChangeForm):
class Meta(UserChangeForm.Meta):
pass
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(CustomUserChangeForm, self).__init__(*args, **kwargs)
# Limit groups and permissions to those that belong to current user
if self.request and not self.request.user.is_superuser:
self.fields['groups'].queryset = self.request.user.groups.all()
self.fields['user_permissions'].queryset = self.request.user.user_permissions.all()
class CustomUserAdmin(UserAdmin):
form = UserChangeForm
add_form = UserCreationForm
limited_fieldsets = ( # Copied from default `UserAdmin`, but removed `is_superuser`
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'user_permissions')}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
(_('Groups'), {'fields': ('groups',)}),
)
def get_fieldsets(self, request, obj=None):
if not obj:
return self.add_fieldsets
elif not request.user.is_superuser:
return self.limited_fieldsets
else:
return super(CustomUserAdmin, self).get_fieldsets(request, obj)
def get_form(self, request, obj=None, **kwargs):
"""Return a metaclass that will automatically pass `request` kwarg into the form"""
ModelForm = super(LinkAdmin, self).get_form(request, obj, **kwargs)
class ModelFormMetaClass(ModelForm):
def __new__(cls, *args, **kwargs):
kwargs['request'] = request
return ModelForm(*args, **kwargs)
return ModelFormMetaClass
def has_add_permission(self, request):
"""
If not superuser only allow add if the current user has at least some
groups or permissions. (they'll have to be able to at least have something
to assign the user they are creating).
"""
if not request.user.is_superuser:
if not request.user.groups.exists() or not request.user.user_permissions.exist():
return False
return True
def has_change_permission(self, request, obj=None):
"""
If not superuser, current user can only modify users who have a subset of the
groups and permissions they have.
"""
if obj and not request.user.is_superuser:
# Check that all of the object's groups are in the current user's groups
user_groups = list(request.user.groups.all())
for group in obj.groups.all():
try:
user_groups.index(group)
except ValueError:
return False
# Check that all of the object's permissions are in the current user's permissions
user_permissions = list(request.user.user_permissions.all())
for permission in obj.user_permissions.all():
try:
user_permissions.index(permission)
except ValueError:
return False
return True
def has_delete_permission(self, request, obj=None):
"""Same logic as `has_change_permission`"""
return self.has_change_permission(request, obj)
def queryset(self, request):
qs = super(CustomUserAdmin, self).queryset(self, request)
if request.user.is_superuser:
return qs
else:
"""
This part is a little counter-intuitive. We're going to first get a
list of all groups/permissions that don't belong to the user, and
then use that to exclude users that do have those from the queryset.
"""
user_group_pks = [g.pk for g request.user.groups.values('pk')]
exclude_groups = Group.objects.exclude(pk__in=user_group_pks)
user_permission_pks = [p.pk for p in request.user.user_permissions.values('pk')]
exclude_permissions = Permission.objects.exclude(pk__in=user_permission_pks)
return qs.exclude(groups__in=exclude_groups, user_permissions__in=exclude_permissions)
admin.site.unregister(User)
admin.site.register(User, CustomUserAdmin)

Django contains a inbuilt system for maintaining User Hierarchy - Django-RBAC.
RBAC stands for Role Based Access Control. Its a mechanism for creating and managing permission based on hierarchy. You just need to study this a bit.

Related

Django Admin: Show only some objects in ManyToMany field?

I have relatively simple data model with User, Group and Task. Each group has its own tasks and users. Users can be only assigned to one group.
Tasks belong to groups and each task has manyToMany field for users, so multiple users can have the same task assigned.
In my admin when assigning users to task it shows all created users, I want it to only show users from the same group as the task.
What would be the best approach?
I have checked available customization options for admin.ModelAdmin but haven't found anything related to my problem.
you can use formfield_for_manytomany
the formfield_for_manytomany method can be overridden to change the default formfield for a many to many field
in your case change your admin.py to :
class TaskAdmin(admin.ModelAdmin):
def get_object(self, request, object_id, s):
# Hook obj for use in formfield_for_manytomany
self.obj = super(TaskAdmin, self).get_object(request, object_id)
# print ("Got object:", self.obj)
return self.obj
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == "user":
kwargs["queryset"] = User.objects.filter(task=self.obj)
return super().formfield_for_manytomany(db_field, request, **kwargs)
admin.site.register(Task, TaskAdmin)
You can customize the model admin using a method: formfield_for_manytomany
class TaskAdmin(admin.ModelAdmin):
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == "users":
# Filter your users below as per your condition
kwargs["queryset"] = Users.objects.filter()
return super(TaskAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)

How do I correctly create a related object overriding the save() method?

I'm overriding the save() method on a subclass of a UserCreationForm. I'm doing so because I'd like to create another related object as the User object is created.
Here is the form along with the save method:
class MyUserCreationForm(UserCreationForm):
error_message = UserCreationForm.error_messages.update({
'duplicate_username': 'This username has already been taken.'
})
class Meta(UserCreationForm.Meta):
model = User
def clean_username(self):
username = self.cleaned_data["username"]
try:
User.objects.get(username=username)
except User.DoesNotExist:
return username
raise forms.ValidationError(self.error_messages['duplicate_username'])
def save(self, commit=True):
user = super(MyUserCreationForm, self).save(commit=False)
if commit:
user.save()
Profile.objects.create(user=user)
return user
So the Profile object is never created. I can get it to work, technically, if I remove the if commit: like so:
def save(self, commit=True):
user = super(MyUserCreationForm, self).save(commit=False)
user.save()
Profile.objects.create(user=user)
return user
However I'd like to know why False is being passed to the save() method each time I create a User. Based on what I've read, the conditional should be there in order to preserve the same behavior as the overridden save() method.
commit=False is meant to be used in inline forms. It's used to assign the parent key to the form instance and save the instance later on.
Source: https://github.com/django/django/blob/master/django/forms/models.py#L942
You should be able to adjust your solution a little bit.
def save(self, *args, **kwargs):
user = super(MyUserCreationForm, self).save(*args, **kwargs)
if user.pk: # If user object has been saved to the db
Profile.objects.get_or_create(user=user)
return user
But I recommend to use Django's model signals.
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save, sender=User)
def user_created_signal_handler(request, user, *args, **kwargs):
Profile.objects.get_or_create(user=user)
https://docs.djangoproject.com/en/2.0/ref/signals/#django.db.models.signals.post_save
Where is this form being used? If it's in a ModelAdmin for example, it might be because ModelAdmin has its own save_model method.
def save_model(self, request, obj, form, change):
"""
Given a model instance save it to the database.
"""
obj.save()
Since this is where the model is intended to be saved, I'm going to assume that commit is False so that it can be done here.
You could try overriding this in your ModelAdmin, for example, but it depends where this is being used. Dan Loewenherz is correct, you shouldn't really use the commit check for stuff like this. Generally, it's really more for use with the form, whereas this is a problem focused around your model.
The problem here I think is the pattern you're using. I think it's better to keep the purpose of the Form more closely tied to the model itself.
What happens if later on, a user is created using a different method or form? They will lack a Profile. I'm assuming this is the common User/User Profile problem that comes from the limitations of the built in auth user model in which case a User without a Profile could be disastrous.
My recommendation would be to use a post_save signal in models.py:
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from models import UserProfile
from django.db import models
def create_profile(sender, instance, created **kwargs):
if created:
Profile.objects.create(user = instance)
post_save.connect(create_profile, sender = User, dispatch_uid = "users-
profilecreate-signal")
Some people are hesitant to use signals, but they are the most reliable way to preserve the integrity of your model in a case like this.
The other option would be to do it at the model level on the User. Something like this:
def save(self, *args, *kwargs):
user = super(User, self).save(*args, **kwargs)
Profile.objects.get_or_create(user = user)
return user

Django: Filter users by user role

I am using the django admin site for my web app, but i am having a problem. I need that the staff users can change, create and delete another staff users, but i don't want that they change the informations of the superusers. I want to know if is possible filter the user list by role (the staff user don't see the superusers in the list).
Finally I found how to do this, I leave the code here just in case someone hav the same problem that I had
def get_queryset(self, request):
queryset = super(UserAdmin, self).get_queryset(request)
if request.user.is_superuser:
return queryset
return queryset.filter(is_superuser=False)
You will need to create a custom ModelAdmin for the User model. I recommend you to inherit from the original one and then you can override the get_queryset method.
You should end with:
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
class MyUserAdmin(UserAdmin):
def get_queryset(self, request):
qs = super(MyUserAdmin, self).get_queryset(request)
if request.user.is_superuser:
return qs
else:
return qs.filter(is_superuser=False)
admin.site.unregister(User)
admin.site.register(User, MyUserAdmin)

How can I make a Django REST framework /me/ call?

Suppose I have a ViewSet:
class ProfileViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows a user's profile to be viewed or edited.
"""
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly)
queryset = Profile.objects.all()
serializer_class = ProfileSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
...and a HyperlinkedModelSerializer:
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Profile
read_only_fields = ('user',)
I have my urls.py set up as:
router.register(r'profiles', api.ProfileViewSet, base_name='profile')
This lets me access e.g. /api/profile/1/ fine.
I want to set up a new endpoint on my API (similar to the Facebook API's /me/ call) at /api/profile/me/ to access the current user's profile - how can I do this with Django REST Framework?
Using the solution by #Gerard was giving me trouble:
Expected view UserViewSet to be called with a URL keyword argument named "pk". Fix your URL conf, or set the .lookup_field attribute on the view correctly..
Taking a look at the source code for retrieve() it seems the user_id is not used (unused *args)
This solution is working:
from django.contrib.auth import get_user_model
from django.shortcuts import get_object_or_404
from rest_framework import filters
from rest_framework import viewsets
from rest_framework import mixins
from rest_framework.decorators import list_route
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from ..serializers import UserSerializer
class UserViewSet(viewsets.ModelViewSet):
"""
A viewset for viewing and editing user instances.
"""
serializer_class = UserSerializer
User = get_user_model()
queryset = User.objects.all()
filter_backends = (filters.DjangoFilterBackend, filters.SearchFilter)
filter_fields = ('username', 'email', 'usertype')
search_fields = ('username', 'email', 'usertype')
#list_route(permission_classes=[IsAuthenticated])
def me(self, request, *args, **kwargs):
User = get_user_model()
self.object = get_object_or_404(User, pk=request.user.id)
serializer = self.get_serializer(self.object)
return Response(serializer.data)
Accessing /api/users/me replies with the same data as /api/users/1 (when the logged-in user is user with pk=1)
You could create a new method in your view class using the list_route decorator, like:
class ProfileViewSet(viewsets.ModelViewSet):
#list_route()
def me(self, request, *args, **kwargs):
# assumes the user is authenticated, handle this according your needs
user_id = request.user.id
return self.retrieve(request, user_id)
See the docs on this for more info on #list_route
I hope this helps!
You can override the get_queryset method by filtering the queryset by the logged in user, this will return the logged in user's profile in the list view (/api/profile/).
def get_queryset(self):
return Profile.objects.filter(user=self.request.user)
or
def get_queryset(self):
qs = super(ProfileViewSet, self).get_queryset()
return qs.filter(user=self.request.user)
or override the retrieve method like so, this will return the profile of the current user.
def retrieve(self, request, *args, **kwargs):
self.object = get_object_or_404(Profile, user=self.request.user)
serializer = self.get_serializer(self.object)
return Response(serializer.data)
From Gerard's answer and looking at the error pointed out by delavnog, I developed the following solution:
class ProfileViewSet(viewsets.ModelViewSet):
#list_route(methods=['GET'], permission_classes=[IsAuthenticated])
def me(self, request, *args, **kwargs):
self.kwargs.update(pk=request.user.id)
return self.retrieve(request,*args, **kwargs)
Notes:
ModelViewSet inherits GenericAPIView and the logic to get an object is implemented in there.
You need to check if the user is authenticated, otherwise request.user will not be available. Use at least permission_classes=[IsAuthenticated].
This solution is for GET but you may apply the same logic for other methods.
DRY assured!
Just override the get_object()
eg.
def get_object(self):
return self.request.user
Just providing a different way. I did it like this:
def get_object(self):
pk = self.kwargs['pk']
if pk == 'me':
return self.request.user
else:
return super().get_object()
This allows other detail_routes in the ViewSet to work like /api/users/me/activate
I've seen quite a few fragile solutions so I thought I'll respond with something more up-to-date and safer. More importantly you don't need a separate view, since me simply acts as a redirection.
#action(detail=False, methods=['get', 'patch'])
def me(self, request):
self.kwargs['pk'] = request.user.pk
if request.method == 'GET':
return self.retrieve(request)
elif request.method == 'PATCH':
return self.partial_update(request)
else:
raise Exception('Not implemented')
It's important to not duplicate the behaviour of retrieve like I've seen in some answers. What if the function retrieve ever changes? Then you end up with a different behaviour for /me and /<user pk>
If you only need to handle GET requests, you could also use Django's redirect. But that will not work with POST or PATCH.
Considering a OneToOneField relationship between the Profile and the User models with related_name='profile', I suggest the following as the #list_route has been deprecated since DRF 3.9
class ProfileViewSet(viewsets.GenericViewSet):
serializer_class = ProfileSerializer
#action(methods=('GET',), detail=False, url_path='me', url_name='me')
def me(self, request, *args, **kwargs):
serializer = self.get_serializer(self.request.user.profile)
return response.Response(serializer.data)

Django 1.4 : Create UserProfile+User as an atomic action, Roles, and Admin integration

I'm building my first real django application which has different user roles and the normal "User signal that creates an UserProfile" approach is falling short. Could you guys help me out ?
To give out more context, here are the requirements from a functional perspective :
New users are added only from the Admin and will be done by non-tech savy people, so I need a user creation flow thats intuitive and easy.
Each user has a role (Manager, Employee, Salesman, etc) with different needs and fields.
The user list needs to show both User Information and role / profile information (login, email, name, and the extra information on profile)
Initial approach :
So armed with this, I went with the recommended approach of creating a UserProfile 1-to-1 object linked to user, provide the UserProfile with a choice field where the role is set (useful for knowing what i'm dealing with when calling get_profile() ) and subclassed UserProfile into ManagerUserProfile, EmployeeUserProfile, etc.
Problem :
That works for my needs in the frontend (outside of the admin), but setting the signal to create a UserProfile when a User is created is pointless since I don't know what kind of UserProfile should I create based only on user information.
What I'm aiming at is an atomic way of creating a particular User and it's corresponding EmployeeUserProfile/ManagerUserProfile at the same time, and having a neat admin representation.
Some of my ideas:
Hide the UserAdmin and User Profile admins, create AdminModels for EmployeeUserProfile/Manager/etc and inline the User model. That way the person creating the users will see only a "New Manager" link with its corresponding fields. But they may create the UserProfile without a user ? How can i make this atomic ? How do i prevent from deleting the user within or make sure they provide all required info before allowing the profile to be saved ? -> Problem with this approach : I cannot seem to inline the user because it has no PK to UserProfile (it's the other way around).
Again, hide UserAdmin, expose the subclassed profiles Admins, and reverse the signal. When a profile is created, create a corresponding user. But for this I need to be able to provide user fields (username, pass, email, etc) from profiles admin form.
Suggestions ?
Its my first app and maybe there's a neat way for this, but I haven't found it yet.
Thanks for taking the time to read this.
Cheers,
Zeta.
I would suggest that you create a custom form, custom admin view and use that for creating users in one request, with the exact logic you need. You can get an idea of how it's done by looking at django's own custom user creation process here.
I finally found a way to implement what I needed. It may not be the cleanest, and it's open to suggestions, but it may help someone with a similar problem.
Since I needed to create only from the admin, I focused on that and build the following.
forms.py
class RequiredInlineFormSet(BaseInlineFormSet):
"""
Generates an inline formset that is required
"""
def _construct_form(self, i, **kwargs):
"""
Override the method to change the form attribute empty_permitted
"""
form = super(RequiredInlineFormSet, self)._construct_form(i, **kwargs)
form.empty_permitted = False
self.can_delete = False
return form
models.py (i'm not using signals for automatically creating a profile on user creation)
class UserProfile(models.Model):
# This field is required.
user = models.OneToOneField(User)
# Other fields here
[.......]
USER_TYPES = (
('manager', 'Manager'),
('employee', 'Employee'),
)
user_type = models.CharField(blank=True, max_length=10, choices=USER_TYPES)
def __unicode__(self):
return self.user.username
class EmployeeProfile(UserProfile):
[...]
def __init__(self, *args, **kwargs):
super(EmployeeProfile, self).__init__(*args, **kwargs)
self.user_type = 'employee'
class ManagerProfile(UserProfile):
[...]
def __init__(self, *args, **kwargs):
super(ManagerProfile, self).__init__(*args, **kwargs)
self.user_type = 'manager'
class Manager(User):
class Meta:
proxy = True
#app_label = 'auth'
verbose_name = 'manager'
verbose_name_plural = 'managers'
def save(self, *args, **kwargs):
self.is_staff = True
super(Manager, self).save(*args, **kwargs) # Call the "real" save() method.
g = Group.objects.get(name='Managers')
g.user_set.add(self)
class Employee(User):
class Meta:
proxy = True
#app_label = 'auth'
verbose_name = 'employee'
verbose_name_plural = 'employees'
def save(self, *args, **kwargs):
self.is_staff = False
super(Employee, self).save(*args, **kwargs) # Call the "real" save() method.
g = Group.objects.get(name='Employees')
g.user_set.add(self)
admin.py
class ManagerProfileAdmin(admin.StackedInline):
model = ManagerProfile
max_num = 1
extra = 1
formset = RequiredInlineFormSet
class EmployeeProfileAdmin(admin.StackedInline):
model = EmployeeProfile
max_num = 1
extra = 1
formset = RequiredInlineFormSet
class ManagerAdmin(UserAdmin):
"""
Options for the admin interface
"""
inlines = [ManagerProfileAdmin]
def queryset(self, request):
qs = super(UserAdmin, self).queryset(request)
qs = qs.filter(Q(userprofile__user_type='manager'))
return qs
class EmployeeAdmin(UserAdmin):
"""
Options for the admin interface
"""
inlines = [EmployeeProfileAdmin]
def queryset(self, request):
qs = super(UserAdmin, self).queryset(request)
qs = qs.filter(Q(userprofile__user_type='employee'))
return qs
admin.site.unregister(User)
admin.site.register(Manager, ManagerAdmin)
admin.site.register(Employee, EmployeeAdmin)