Handling JWT Authentication and Soft Deleted Users in Django Rest Framework - django

I'm trying to make a username available once a user deletes their account. By default the username is unique meaning the username won't be available even after the account is soft deleted.
This is, by default, the setup that comes with django out of the box.
class CustomUser(AbstractUser):
username = models.CharField(
_('username'),
max_length=150,
unique=True,
help_text=_('Required. 150 characters or fewer. Letters, digits and #/./+/-/_ only.'),
validators=[UnicodeUsernameValidator()],
error_messages={
'unique': _("A user with that username already exists."),
},
)
is_active = models.BooleanField(
_('active'),
default=True,
help_text=_(
'Designates whether this user should be treated as active. '
'Unselect this instead of deleting accounts.'
),
)
is_active is used here to mark a model as deleted.
So that I can be able to take advantage of UniqueConstraint and add a condition, I have to drop the uniqueness of the username.
class CustomUser(AbstractUser):
username = models.CharField(
_('username'),
max_length=150,
unique=False,
help_text=_('Required. 150 characters or fewer. Letters, digits and #/./+/-/_ only.'),
validators=[UnicodeUsernameValidator()],
error_messages={
'unique': _("A user with that username already exists."),
},
)
is_active = models.BooleanField(
_('active'),
default=True,
help_text=_(
'Designates whether this user should be treated as active. '
'Unselect this instead of deleting accounts.'
),
)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['username'],
name='active_username_constraint',
condition=models.Q(is_active=True)
)
]
This works for registrations. After a user has deleted their account, the username can be re-used during registration. However, when a user logs in, the following error is raised.
MultipleObjectsReturned at /api/vdev/login/
get() returned more than one CustomUser -- it returned 2!
I'm trying to find out a way to check if a user is_active as part of the authentication process. Is there a way this can be done?

I'm guessing the error is raised when the authenticate is being called from the UserModel. Looking at the source code, the method calls get_by_natural_key from the user manager. Examining the source code for this method shows us where we need to make the change.
Hence, what you'll probably have to do is to create a custom user manager, inheriting from BaseUserManager and overriding get_by_natural_key.
class MyUserManager(BaseUserManager):
...
def get_by_natural_key(self, username):
return self.get(**{self.model.USERNAME_FIELD: username, "is_active": True})
Then in your custom user model, set your manager to this custom user manager:
class CustomUser(AbstractUser):
...
objects = MyUserManager()

Related

Django User Editing, AbstractUser

We are trying to build endpoints to allow users to edit their own profiles from our front end and we've encountered a problem while trying to edit the "logged in user". This issue happens in django admin as well.
All the rest of this post is specifically referring to the "user" in django admin. I have extended the user and built a custom admin.
if we have 3 users, (imagine all three are super users/is_staff for now). Logged in user 1, I can edit users 2 and 3, but when I go to edit user 1 (logged in user), the message says it was updated but the database does not change.
If I then login as user 2 and update user 1, I can update user 1 but not user 2 as the logged in user.
This same behavior happens on our endpoints with request.user. request.user can edit any user except for the logged in user.
CODE
accounts/models.py
class User(AbstractUser):
timezone = models.CharField(max_length=255, blank=True, null=True)
is_customer = models.BooleanField(default=False)
is_agent = models.BooleanField(default=False)
is_carrier = models.BooleanField(default=False)
is_shipper = models.BooleanField(default=False)
is_tracking = models.BooleanField(default=False)
class Meta:
db_table = 'auth_user'
def __str__(self):
return self.first_name
It is defined in settings:
AUTH_USER_MODEL = 'accounts.User'
accounts/admin.py
CustomUser = get_user_model()
class UserInline(admin.StackedInline):
model = User
class AgentInline(admin.StackedInline):
model = Agent
class CustomUserAdmin(UserAdmin):
model = CustomUser
agent_fields = ('timezone', 'is_agent', 'is_customer', 'is_shipper', 'is_carrier', 'is_tracking')
fieldsets = UserAdmin.fieldsets + (
('Agent Info', {'fields': agent_fields}),
)
inlines = [
AgentInline,
]
admin.site.register(CustomUser, CustomUserAdmin)
MIGRATIONS 0001_initial.py
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0009_alter_user_last_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and #/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('is_customer', models.BooleanField(default=False)),
('is_agent', models.BooleanField(default=False)),
('is_carrier', models.BooleanField(default=False)),
('is_shipper', models.BooleanField(default=False)),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]
Screenshots of my testing
Here's the output of the UPDATE, you can see that first name is being saved as blank string. So maybe this is form related?
It's likely that when we added the custom user model we did something wrong as I believe it was added after the project creation.
Because of this i'm assuming the logged in user is maybe making a change to the wrong table in the DB. I'm not exactly sure how to identify this though, normally I would shell in but the original auth_user model is disabled from being imported because of our custom model.
Let me know if I can provide anymore context.
UPDATE 1
It looks like the update is actually working, then it's being immediately overwritten by the original data. See this screenshot, this happens on a single update. You can see an UPDATE statement with the last name having a value, then a second UPDATE with the original data.
tl,dr: The update was done properly, but a middleware caused a request.user.save() (or similar), writing back the old values to the db.
Findings from the comments expanded into an answer:
It turns out the update query was executed just as expected, which matches the message "User was changed successfully". Enabling logging of all sql queries allowed to confirm this.
However, right after the correct update query, there was another query resetting the user to its state before. Here, it helps to know that when the form is saved, it does not update the python object accessible via request.user (because it doesn't know that the updated user is request.user). It would only be updated if someone called refresh_from_db on it.
Thus, I suspected something called request.user.save(), which would then store back the outdated state. This would match the queries we observed. But the django admin view should not do that. On the internet, there are some pages on how to get stack traces together with the query log, which should allow to find out where the query is issued from.
However, even without the hassle of enabling the log, the culprit in this case could be identified to be some middleware. This can be easily tested by simply commenting out any (custom) middleware that is not essential.
I think you probably did initial migration before creating CUSTOM_USER
Now you can delete all migration files and can run a fresh migration.

Where is django.contrib.auth.User defined in Django source code

In Django official guide, it reads:
Inside this django.contrib.auth model, there is a User class, who has following attributes (username, password, email, first_name, last_name).
When I check the source code in github, I did not find this definition in django.contrib.auth.
I can only see class AbstractBaseUser(models.Model): in django/contrib/auth/base_user.py on this link, and class User(AbstractUser): in django/contrib/auth/models.py in this webpage.
Q1: what does class models.User mean in above official document, it means User is a class under models.py ?
Q2: if above is right, then where User class get attributes such as username, email etc?
Q1: what does class models.User mean in above official document, it means User is a class under models.py?
In Django one refers to a model with the app_name.ModelName. So if you specify a model, this is implemented in the app_name/models.py, but since models are defined in the models.py file, it makes no sense to include that in the name of the model.
For example the default for the AUTH_USER_MODEL setting [Django-doc] is auth.User, since the name of the app is auth, and the name of the model is User.
Q2: if above is right, then where User class get attributes such as username, email etc?
Through inheritance. Indeed if we look at the source code of the models.py file [GitHub], we see:
class User(AbstractUser):
"""
Users within the Django authentication system are represented by this
model.
Username and password are required. Other fields are optional.
"""
class Meta(AbstractUser.Meta):
swappable = 'AUTH_USER_MODEL'
and the AbstractUser model [GitHub] defines the fields for username, email, etc.:
class AbstractUser(AbstractBaseUser, PermissionsMixin):
# …
username = models.CharField(
_('username'),
max_length=150,
unique=True,
help_text=_('Required. 150 characters or fewer. Letters, digits and #/./+/-/_ only.'),
validators=[username_validator],
error_messages={
'unique': _("A user with that username already exists."),
},
)
first_name = models.CharField(_('first name'), max_length=150, blank=True)
last_name = models.CharField(_('last name'), max_length=150, blank=True)
email = models.EmailField(_('email address'), blank=True)
is_staff = models.BooleanField(
_('staff status'),
default=False,
help_text=_('Designates whether the user can log into this admin site.'),
)
is_active = models.BooleanField(
_('active'),
default=True,
help_text=_(
'Designates whether this user should be treated as active. '
'Unselect this instead of deleting accounts.'
),
)
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
# …
AbstractUser is an abstract model. This means that Django does not create a table for it. Models that inherit from an abstract table will thus inherit fields, methods, etc. and these fields will then be defined on the model that inherits from AbstractUser.

Add to auth_user [duplicate]

This question already has answers here:
Extending the User model with custom fields in Django
(16 answers)
Closed 2 years ago.
I have this program that gets most of its data from the auth_user table one the django database. I don’t know how to add a new column to it and make it actually work since it doesn’t recognize it when I do. Does anybody know how you would add a column that can be used in auth_user table in the default django database.
I think this is a part that well documented on Django's documentation site. I guess you are trying to create custom user model. But I recommend you to extend user model. Extending default user model will provide you more flexibility. I hope these two links will enough. If not, please comment me about missing parts to cover.
You can override default user model or extend user model with OneToOneField relation to satisfy this requirement.
1. I'll show you how to do custom User Model.
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from core.util.validators import UnicodeUsernameValidator
from .manager import UserManager
class User(AbstractBaseUser, PermissionsMixin):
"""
Username, Email and password are required. Other fields are optional.
"""
username_validator = UnicodeUsernameValidator()
username = models.CharField(
_('username'),
max_length=150,
unique=True,
help_text=_(
'150 characters or fewer. Letters, digits and #/./+/-/_ only.'),
validators=[username_validator],
error_messages={
'unique': _("A user with that username already exists."),
},
null=True,
default=None
)
email = models.EmailField(
_('email address'),
blank=False,
unique=True,
error_messages={
'unique': _("A user with that email address already exists."),
},
)
is_staff = models.BooleanField(
_('staff status'),
default=False,
help_text=_(
'Designates whether the user can log into this admin site.'),
)
is_active = models.BooleanField(
_('active'),
default=True,
help_text=_(
'Designates whether this user should be treated as active. '
'Unselect this instead of deleting accounts.'
),
)
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
objects = UserManager()
EMAIL_FIELD = 'email'
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email']
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
permissions = (
('user_permission', "User Permission"),
('admin_permission', "Admin Permission"),
)
This is like a copy of what they have in their User model. here, You can add new fields.
Make sure to register this in settings.py
AUTH_USER_MODEL = 'userapp.User'
you can access extra_fields like
user.extra_field
2. I'll show you how to extend using OneToOneField.
from django.contrib.auth import get_user_model
class Profile(models.Model):
user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE)
...
# Extra fields Here
then you can access the extra fields as
user.profile.extra_field

Modify django's username policy?

Django default admin authentication module disallows use of the \ char in usernames. How can I make it accept it?
It looks like it's possible to edit contrib/auth/models.py's username field's validator, but that won't do because it requires a change to django's base code.
You can use custom regex validators and a custom user model in order to achieve this. Be sure to set USERNAME_FIELD and REQUIRED_FIELDS in your model definition and AUTH_USER_MODEL in your settings.
custom_userprofile_app.models.py
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
class User(AbstractBaseUser, PermissionsMixin):
username = models.CharField(_('username'), max_length=30, unique=True,
help_text=_('Required. 30 characters or fewer.'),
validators=[
validators.RegexValidator(r'^(.*)$',
_('Enter a valid username. '
'This value may contain any characters', 'invalid'),
validators.MinLengthValidator(5, 'Username must be at least 5 characters'),
],
error_messages={
'unique': _("A user with that username already exists."),
})
# The rest of your fields, etc
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email','first_name','last_name']
settings.py
AUTH_USER_MODEL = 'custom_userprofile_app.User'
You can override django's default User model and then edit the regex validator.
You are just required to create a child class of base user model and then define a setting for that.
Read how to specify custom user model # django docs

Create user inactive as default (is_active default False)

I have facebook authentication in my website which I use omab / django-social-auth
I want to redirect users to another website to make them fill their details. So I want to have users as inactive when they first authenticate with their facebook accounts, then after they complete the form I save them as active users.
I manipulated the django/contrib/auth/models.py under my enviroment as with is_active fields as default=False; but they are saved as active user
but still the same result, even I add a normal user from the admin panel. Is there something I am missing?
class User(models.Model):
"""
Users within the Django authentication system are represented by this
model.
Username and password are required. Other fields are optional.
"""
username = models.CharField(_('username'), max_length=30, unique=True,
help_text=_('Required. 30 characters or fewer. Letters, numbers and '
'#/./+/-/_ characters'))
first_name = models.CharField(_('first name'), max_length=30, blank=True)
last_name = models.CharField(_('last name'), max_length=30, blank=True)
email = models.EmailField(_('e-mail address'), blank=True)
password = models.CharField(_('password'), max_length=128)
is_staff = models.BooleanField(_('staff status'), default=False,
help_text=_('Designates whether the user can log into this admin '
'site.'))
is_active = models.BooleanField(_('active'), default=False,
help_text=_('Designates whether this user should be treated as '
'active. Unselect this instead of deleting accounts.'))
is_superuser = models.BooleanField(_('superuser status'), default=False,
help_text=_('Designates that this user has all permissions without '
'explicitly assigning them.'))
last_login = models.DateTimeField(_('last login'), default=timezone.now)
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
groups = models.ManyToManyField(Group, verbose_name=_('groups'),
blank=True, help_text=_('The groups this user belongs to. A user will '
'get all permissions granted to each of '
'his/her group.'))
user_permissions = models.ManyToManyField(Permission,
verbose_name=_('user permissions'), blank=True,
help_text='Specific permissions for this user.')
objects = UserManager()
def create_user(self, username, email=None, password=None):
"""
Creates and saves a User with the given username, email and password.
"""
now = timezone.now()
if not username:
raise ValueError('The given username must be set')
email = UserManager.normalize_email(email)
user = self.model(username=username, email=email,
is_staff=False, is_active=False, is_superuser=False,
last_login=now, date_joined=now)
user.set_password(password)
user.save(using=self._db)
return user
Avoid modifying built-ins. There are better ways to do things.
Like signals. Signals are awesome.
In this case, I'd attach to the pre_save signal of django.contrib.auth.models.User, and manually correct the is_active property of the model instance (if the object is new).
This way, you can add some logic to make sure you're properly marking a user as not active.
Because user's added within the Admin should probably be active, if the admin marks them as active.
jack_shed suggested signals, which helped me find the direction to take it. But there was still work from there to figure out how exactly to test and modify after receiving the signal.
Here's what worked for me.
from django.dispatch import receiver
from django.db.models.signals import pre_save
from django.contrib.auth.models import User
#receiver(pre_save, sender=User)
def set_new_user_inactive(sender, instance, **kwargs):
if instance._state.adding is True:
print("Creating Inactive User")
instance.is_active = False
else:
print("Updating User Record")
This will catch the action of creating a user before the save occurs, then test if this instance state is "adding" or not. That differentiates between creating and updating a model instance.
If you don't do this test, updating the user sets is_active to False also, and there ends up being no way to activate them through django.
Elegant solution when using django-allauth.
There is another very nice solution.. one that sounds very much like what you desire.
I have created a custom form (in my case a ModelForm) that I can hand over to django-allauth via the ACCOUNT_SIGNUP_FORM_CLASS setting. What this does.. is ask the new potential user to supply additional fields during the signup process.
That has some very nice advantages:
You can add some fields very elegantly in addition to the default stuff.
It works for both social and "normal" signup.
No patching of 3rd party apps required.
You are still able to modify and maintain everything in the admin.
In the custom form you get access to the new user instance before it gets saved to the
database. This means you can even process the provided information do things like create a profile object for him and set the user as inactive ..all in one go.
This works because you can check if everything is ok.. and only then commit to do all these steps or reject the form with a validation error. :)
Well.. sounds good right?
But how exactly does it work (i.e. look like)?
I am glad you asked.. ^_^
For your use case it might look something like this:
settings.py
[...]
ACCOUNT_SIGNUP_FORM_CLASS = "<your_app>.forms.SignupForm"
[...]
forms.py
class SignupForm(forms.Form):
first_name = forms.CharField(max_length=30)
last_name = forms.CharField(max_length=30)
def signup(self, request, user):
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
user.is_active = False
user.save()
I think a better approach would be to customize your own model manager by leaving the user value inactive as default.
Create in your app: project_app/managers.py:
# project_app/managers.py
from django.contrib.auth.base_user import BaseUserManager
class UserManager(BaseUserManager):
def create_user(self, username, email=None, password=None, **extra_fields):
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
extra_fields.setdefault('is_active', False)
return self._create_user(username, email, password, **extra_fields)
and in you models:
#project_app/models.py
from .managers import UserManager
from django.contrib.auth.models import AbstractUser
class UserManager(AbstractUser):
objects = UserManager()
# your custom fields go here.