I'm trying to limit standard user possibilities in the User application Django's admin, to avoid the possibility of privileges escalation.
I've tried the approach mentioned in docs:
https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.get_form
class UserAdmin(BaseUserAdmin):
form = UserForm
inlines = (UserProfileInline,)
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
kwargs["form"] = SuperUserForm
return super().get_form(request, obj, **kwargs)
class UserForm(ModelForm):
class Meta:
model = User
exclude = ("is_superuser",) # it is just an example
class SuperUserForm(ModelForm):
class Meta:
model = User
fields = "__all__
Unfortunately, this results in such an error:
"Key 'is_superuser' not found in 'UserForm'. Choices are: date_joined, email, first_name, groups, is_active, is_staff, last_login, last_name, password, user_permissions, username."
If I would decide to exclude "groups":
"Key 'groups' not found in 'UserForm'. Choices are: date_joined, email, first_name, is_active, is_staff, is_superuser, last_login, last_name, password, user_permissions, username."
I end up with totally different approach:
class UserAdmin(BaseUserAdmin):
def has_change_permission(self, request, obj=None):
if obj != request.user and not request.user.is_superuser:
return False
return True
def changelist_view(self, request, extra_context=None):
if request.user.is_superuser:
return super().changelist_view(request, extra_context)
return HttpResponseRedirect(
reverse(
"admin:%s_%s_change" % (self.model._meta.app_label, self.model._meta.model_name),
args=(request.user.id,),
)
)
def get_fieldsets(self, request, obj=None):
fieldsets = list(super().get_fieldsets(request, obj))
if request.user.is_superuser:
return fieldsets
return [
(None, {"fields": ("username", "password")}),
("Personal info", {"fields": ("first_name", "last_name", "email")}),
]
I do not know if it is "Djangoish" but it seems to work as expected.
Related
I am working on my Django (DRF) application. A have a CustomUser model
class CustomAccountManager(BaseUserManager):
def create_superuser(self, email, user_name, password, **other_fields):
...
def create_user(self, email, user_name, password, **other_fields):
if not email:
raise ValueError(_('You must provide an email address'))
email = self.normalize_email(email)
user = self.model(email=email, user_name=user_name, **other_fields)
user.set_password(password)
user.save()
return user
class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(_('email address'), unique=True)
user_name = models.CharField(max_length=150, unique=True) # Full name
phone_number = models.CharField(max_length=20, unique=True)
...
I have created a custom way to change password.
I am sending current_password, new_password_verify and new_password_verify in body parameter.
My solution is working, but looks bulky
What is the proper way to implement password change in Django?
class CustomUserViewSet(viewsets.ModelViewSet):
def update(self, request: Request, *args, **kwargs):
instance: CustomUser = self.get_object()
serializer = self.get_serializer(
instance, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, "_prefetched_objects_cache", None):
instance._prefetched_objects_cache = {}
return Response({
"is_password_updated": self.update_password(request, instance), # <-------UPDATE
"result": serializer.data
})
def update_password(self, request, instance):
"""update password if 'new_password_verify' and 'new_password' are in request"""
if "current_password" in request.data and instance.check_password(request.data["current_password"]) and \
"new_password" in request.data and "new_password_verify" in request.data and \
request.data["new_password"] == request.data["new_password_verify"]:
instance.set_password(request.data["new_password"])
instance.save()
return True
return False
I think you can extract the validating logic from the view to make it more modularized by defining the serializer.
class ChangePasswordSerializer(serializers.Serializer):
current_password = serializers.CharField(trim_whitespace = False, validators=[validate_password]),
new_password = serializers.CharField(trim_whitespace = False, validators=[validate_password])
new_password_verify = serializers.CharField(trim_whitespace = False)
def validate(self, attrs):
if attrs.get('new_password') != attrs.get('new_password_verify'):
serializers.ValidationError('Password and confirm password do not match')
return attrs
def validate_password(value):
# you can set your own validating logic here if you want to
# for example, like validations for length or regex
pass
Of course, you don't need to upload new_password_verify data, and check that part in the frontend then the new_password_verify field and validate method is not necessary and the code will be simpler.
class CustomUserViewSet(viewsets.ModelViewSet):
def update(self, request: Request, *args, **kwargs):
...
return Response({
"is_password_updated": self.update_password(request, instance)
"result": serializer.data
})
def update_password(self, request, instance):
serializer = ChangePasswordSerializer(data = request.data)
if serializer.is_valid():
input_data = serializer.validated_data
cur_password = input_data.get('current_password')
new_password = input_data.get('new_password')
if instance.check_password(cur_password):
instance.set_password(new_password)
instance.save()
return True
return False
I added a new field name phone for phone number in my registration form, but now it throws error when i submit it throws
Cannot resolve keyword 'phone' into field. Choices are: address,
date_joined, email, first_name, groups, id, is_active, is_staff,
is_superuser, last_login, last_name, logentry, notifications, order,
password, profile, user_permissions, username
this error i am using Django default usercreation forms
here is my forms.py
class SignUpForm(UserCreationForm):
phone_regex = RegexValidator(regex=r'^\+?1?\d{10}$', message="Inform a valid phone number.")
email = forms.EmailField(max_length=254, required=True, help_text='Required. Inform a valid email address.')
phone = forms.CharField(validators=[phone_regex], max_length=10, required=True, help_text='Required. Inform a valid phone number.')
class Meta:
model = User
fields = ('username', 'email', 'phone', 'password1', 'password2',)
def clean_email(self):
email = self.cleaned_data['email']
qs = User.objects.exclude(pk=self.instance.pk).filter(email__iexact=email)
if qs.exists():
raise ValidationError('A user with this email address already exists')
return email
def clean_phone(self):
phone = self.cleaned_data['phone']
qs = User.objects.exclude(pk=self.instance.pk).filter(phone__iexact=phone)
if qs.exists():
raise ValidationError('A user with same phone number already exists')
return phone
views.py
class SignUpView(View):
form_class = SignUpForm
template_name = 'register.html'
def get(self, request, *args, **kwargs):
form = self.form_class()
return render(request, self.template_name, {'form': form})
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
user = form.save(commit=False)
user.is_active = False # Deactivate account till it is confirmed
user.save()
current_site = get_current_site(request)
subject = 'Activate Your MySite Account'
message = render_to_string('account_activation_email.html', {
'user': user,
'domain': current_site.domain,
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'token': account_activation_token.make_token(user),
})
user.email_user(subject, message)
messages.success(request, ('Please Confirm your email to complete registration.'))
return render( request, 'activation_sent_success.html')
return render(request, self.template_name, {'form': form})
any suggestion help will be appreciated, thank you
The error is caused by phone field:
class Meta:
model = User
fields = ('username', 'email', 'phone', 'password1', 'password2',)
^^^^^^
In your case, fields in class Meta is used to specify which fields of your User model will be in your form. You can omit some fields of your model with it.
Your User model does not have a field named phone. Adding this field to your model will solve your problem and do not forget executing python manage.py makemigrations.
If your model has this field, then you might forget applying migrations with python manage.py makemigrations.
I am using LDAP for authentication in my Django app so when adding a user in the admin page I don't need to set a password since the user will be authenticated against the LDAP backend. I'd like to modify the 'Add user' page in the admin views to have a boolean selector to identify when I am trying to add an LDAP user so that the password field is not required. I'd like to retain the option of supplying a password in case I need to add a local user that authenticates against Django's backend.
Here is what I've cobbled together so far:
models.py
Modified the save method so that the user gets populated in the CustomUser model if isLdap is True.
class CustomUser(AbstractUser):
pass
isLdap = models.BooleanField(default=False)
def save(self, *args, **kwargs):
if self.isLdap:
user = LDAPBackend().populate_user(self.username)
else:
super().save(*args, **kwargs)
def __str__(self):
return self.username
admin.py
I successfully added a check box to identify if the new user is an LDAP user but the value isn't being saved to the CustomUser model and I need to change save_model so it saves the actual password if it is valid otherwise set_unusable_password().
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
add_fieldsets = (
(None, {
'description': (
"Enable 'IsLdap' if the username is a LAN ID. "
),
'fields': ('username','isLdap'),
}),
('Password', {
'fields': ('password1', 'password2'),
}),
)
model = CustomUser
def save_model(self, request, obj, form, change):
obj.set_unusable_password()
super(UserAdmin, self).save_model(request, obj, form, change)
admin.site.register(CustomUser, CustomUserAdmin)
forms.py
Not sure if this is the spot or how to do it, but I think here is where I need to conditionally set ....required = False if isLdap is True.
class CustomUserCreationForm(UserCreationForm):
def __init__(self, *args, **kwargs):
super(UserCreationForm, self).__init__(*args, **kwargs)
self.fields['password1'].required = False
self.fields['password2'].required = False
def clean_password2(self):
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError("Passwords don't match")
return password2
Appreciate any help in getting this code functional!
Here's what I ended up doing:
Models.py
from django.db import models
from django.contrib.auth.models import (AbstractUser, BaseUserManager, )
from django_auth_ldap.backend import LDAPBackend
from django.dispatch import receiver
# Create your models here.
class CustomUserManager(BaseUserManager):
def create_user(self, username, is_ldap, password, **extra_fields):
if not username:
raise ValueError(_('The username must be set'))
user = self.model(username=username,
is_ldap=is_ldap,
**extra_fields)
user.set_password(password)
user.save()
return user
def create_superuser(self, username, is_ldap, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('is_active', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self.create_user(username, is_ldap, password, **extra_fields)
class CustomUser(AbstractUser):
is_ldap = models.BooleanField(default=False)
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['is_ldap']
objects = CustomUserManager()
def __str__(self):
return self.username
#receiver(models.signals.post_save, sender=CustomUser)
def user_created(sender, instance, created, **kwargs):
if created and instance.is_ldap:
user = LDAPBackend().populate_user(instance.username)
admin.py
from django import forms
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.core.exceptions import ValidationError
from .models import CustomUser
class CustomUserCreationForm(forms.ModelForm):
"""A form for creating new users. Includes all the required
fields, plus a repeated password."""
password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['password1'].required = False
self.fields['password2'].required = False
class Meta:
model = CustomUser
fields = ('username','is_ldap',)
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
is_ldap = self.cleaned_data.get("is_ldap")
if is_ldap:
if password1 or password2:
raise ValidationError("Leave password fields empty for an LDAP user")
else:
if not password1 or not password2:
raise ValidationError("Passwords don't match")
elif password1 and password2 and password1 != password2:
raise ValidationError("Passwords don't match")
return password2
def save(self, commit=True):
# Save the provided password in hashed format
user = super().save(commit=False)
if self.cleaned_data['is_ldap']:
user.set_unusable_password()
else:
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
class CustomUserChangeForm(forms.ModelForm):
"""A form for updating users. Includes all the fields on
the user, but replaces the password field with admin's
disabled password hash display field.
"""
password = ReadOnlyPasswordHashField()
class Meta:
model = CustomUser
fields = ('username','is_ldap',)
class CustomUserAdmin(UserAdmin):
# The forms to add and change user instances
form = CustomUserChangeForm
add_form = CustomUserCreationForm
model = CustomUser
# The fields to be used in displaying the User model.
# These override the definitions on the base UserAdmin
# that reference specific fields on auth.User.
list_display = ('username', 'first_name', 'last_name', 'is_staff', 'is_active', 'is_ldap',)
# list_filter = ('username', 'is_staff', 'is_active',)
fieldsets = (
(None, {'fields': ('username', 'is_ldap', 'password')}),
('Personal info', {'fields': ('first_name', 'last_name')}),
('Permissions', {'fields': ('is_superuser','groups')}),
)
# add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
# overrides get_fieldsets to use this attribute when creating a user.
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'is_ldap', 'password1', 'password2'),
}),
)
search_fields = ('username',)
ordering = ('username',)
filter_horizontal = (['groups',])
admin.site.register(CustomUser, CustomUserAdmin)
I want to register users by using template, i want also, in the moment of registration, set the group(default groups permission provided by Django) of every new user created, I set the group of user in the template but when i look to the group of user in the database i I found it empty.
class ProfileUserManager(BaseUserManager):
def create_user(self, username, password,**extra_fields):
user = self.model(username=username, **extra_fields)
user.set_password(password)
user.save()
return user
def create_superuser(self, username, password,**extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('is_active', True)
if extra_fields.get('is_staff') is not True:
raise ValueError(_('Superuser must have is_staff=True.'))
if extra_fields.get('is_superuser') is not True:
raise ValueError(_('Superuser must have is_superuser=True.'))
return self.create_user(username,password,**extra_fields)
class ProfileUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(_('email address'), unique=True)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
date_joined = models.DateTimeField(default=timezone.now)
username = models.CharField(max_length=255,unique=True)
first_name=models.CharField(max_length=255)
last_name= models.CharField(max_length=255)
departement= models.CharField(max_length=255)
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = []
objects = ProfileUserManager()
def __str__(self):
return self.username
forms.py
class FormAddAccount(UserCreationForm):
class Meta(UserCreationForm.Meta):
model = get_user_model()
fields = ('email', 'password1', 'password2', 'is_staff','username','groups','first_name','last_name','departement')
first, you create decoratorsy.py in your app then add the following code in decorators.py
from django.http import HttpResponse
from django.shortcuts import redirect
def unauthenticated_user(view_func):
def wrapper_func(request, *args, **kwargs):
if request.user.is_authenticated:
return redirect('url')
else:
return view_func(request, *args, **kwargs)
return wrapper_func
def allowed_users(allowed_roles=[]):
def decorator(view_func):
def wrapper_func(request, *args, **kwargs):
group = None
if request.user.groups.exists():
group = request.user.groups.all()[0].name
if group in allowed_roles:
return view_func(request, *args, **kwargs)
else:
return HttpResponse('You are not authorized to view this page')
return wrapper_func
return decorator
def admin_only(view_func):
def wrapper_function(request, *args, **kwargs):
group = None
if request.user.groups.exists():
group = request.user.groups.all()[0].name
if group == 'GroupName':
return redirect('url')
if group == 'admin':
return view_func(request, *args, **kwargs)
return wrapper_function
views.py
def createuser(request):
form = CreateUserForm()
if request.method == 'POST':
form = CreateUserForm(request.POST)
if form.is_valid():
form.save()
user = form.save()
username = form.cleaned_data.get('username')
group = Group.objects.get(name='GroupName')
user.groups.add(group)
Customer.objects.create(
user=user, )
messages.success(request, 'Account was created for ' + username)
return redirect('login')
context = {'form': form}
return user
on your views.py you add #allowed_users(allowed_roles=['GROUPNAME'])
for the permission, that group see the following things
I just add some lines of code to Views.py when i add a user:
def createView(request):
form=FormAddAccount()
if request.method=="POST":
form=FormAddAccount(request.POST)
if form.is_valid():
user=form.save()
for element in form.cleaned_data["groups"] :
group = Group.objects.get(name=element)
user.groups.add(group)
user.save()
redirect("accounts/list.html")
context={
'form':form
}
return render(request,"accounts/addaccount.html",context)
I am trying to register users in django auth module through an API call but the users are getting registered without the password being hashed which, I suspect, is making my authentication fail. Registering the users through the admin form is hashing the password and therefore working.
I developed my own User model by extending AbstractBaseUser and also created a UserManager extending BaseUserManager and defining the create_user and create_superuser method. I developed a simple serializer for it.
I read somewhere that the password can only be hashed if I developed the Admin form as well and so I did it. In this form, I followed django documentation and developed clean_password and save functions. I also registered these forms on the app admin.py.
Lastly, I created the APIView to the POST requests where I send the registration json and use the serializer do validate and save.
model
class UserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError('The given email must be set')
user = self.model(
email=self.normalize_email(email),
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password, **extra_fields):
user = self.create_user(email,
password=password,
**extra_fields)
user.is_admin = True
user.save(using=self._db)
return user
class User(AbstractBaseUser):
email = models.EmailField(max_length=40, unique=True)
first_name = models.CharField(max_length=30, blank=True)
last_name = models.CharField(max_length=30, blank=True)
is_active = models.BooleanField(default=True)
is_admin = models.BooleanField(default=False)
photo_path = models.CharField(max_length=30, blank=True)
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['first_name', 'last_name']
def save(self, *args, **kwargs):
super(User, self).save(*args, **kwargs)
return self
def get_full_name(self):
return self.email
def get_short_name(self):
return self.email
def __str__(self):
return self.email
def has_perm(self, perm, obj=None):
return True
def has_module_perms(self, app_label):
return True
#property
def is_staff(self):
return self.is_admin
serializer
class UserSerializer(serializers.ModelSerializer):
class Meta(object):
model = User
fields = ('id', 'email', 'first_name', 'last_name', 'password')
extra_kwargs = {'password': {'write_only': True}}
forms
class UserCreationForm(forms.ModelForm):
password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
class Meta:
model = User
fields = ('email', 'photo_path')
def clean_password2(self):
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError("Passwords don't match")
return password2
def save(self, commit=True):
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
class UserChangeForm(forms.ModelForm):
class Meta:
model = User
fields = ('email', 'photo_path', 'password')
def clean_password(self):
return self.initial["password"]
admin.py
class UserAdmin(BaseUserAdmin):
form = UserChangeForm
add_form = UserCreationForm
list_display = ('email', 'first_name', 'is_staff')
list_filter = ('is_admin',)
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Personal info', {'fields': ('first_name',)}),
('Permissions', {'fields': ('is_admin',)}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'password1', 'password2')}
),
)
search_fields = ('email',)
ordering = ('email',)
filter_horizontal = ()
admin.site.register(User, UserAdmin)
view post
class CreateUserAPIView(APIView):
permission_classes = (AllowAny,)
def post(self, request):
user = request.data
serializer = UserSerializer(data=user)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
I expected to get a user in the DB with a hashed password, like when I create a user in the admin panel. but I get a user created with a plain text password.
What I would do, is the following in your serializer. Notice the set_password. That way you make sure it is hashed
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
class Meta:
model = models.User
fields = ('username', 'password', 'email')
def create(self, validated_data):
user = super(UserSerializer, self).create(validated_data)
user.set_password(validated_data['password'])
user.save()
return user
If you are using md5 for hashing then you can use the hashlib module and hash the password before saving in create_superuser
form hashlib import md5
def create_superuser(self, email, password, **extra_fields):
user = self.create_user(email,password=md5(password),**extra_fields)
user.is_admin = True
user.save(using=self._db)
return user
Sorry for the quick auto-response but I found out that the view post code was actually not executing my model create_user code. I don't know what was connecting the .save() method of the serializer to the authentication system but it was still creating the users. I will leave this question open so someone can maybe explain what was happening. To make it work i did the following changes:
class CreateUserAPIView(APIView):
permission_classes = (AllowAny,)
def post(self, request):
user = User.objects.create_user(request.data['email'], request.data['password']);
return Response(user, status=status.HTTP_201_CREATED)