Opened 5 weeks ago

Closed 4 weeks ago

#36087 closed Bug (fixed)

Password reset does not support a custom user model with a composite primary key

Reported by: Jacob Walls Owned by: Sarah Boyce
Component: contrib.auth Version: dev
Severity: Release blocker Keywords:
Cc: Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Jacob Walls)

It may not be likely someone would try this, but even so, we might add a system check or otherwise document that contrib.auth is not composite primary key ready. Then we can decide on whether to add support.

Adjusting a custom user model like this:

  • tests/auth_tests/models/custom_user.py

    diff --git a/tests/auth_tests/models/custom_user.py b/tests/auth_tests/models/custom_user.py
    index 4586e452cd..0647e47ede 100644
    a b class CustomUserManager(BaseUserManager):  
    5252
    5353
    5454class CustomUser(AbstractBaseUser):
     55    pk = models.CompositePrimaryKey("email", "date_of_birth")
    5556    email = models.EmailField(verbose_name="email address", max_length=255, unique=True)
    5657    is_active = models.BooleanField(default=True)
    5758    is_admin = models.BooleanField(default=False)

Leads to various failures in contrib.auth that expect to deserialize a pk by decoding to a bytestring.

The failure I was playing with was a little easier to see by doing this:

  • django/contrib/auth/views.py

    diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py
    index a18cfdb347..4c82a4103c 100644
    a b class PasswordResetConfirmView(PasswordContextMixin, FormView):  
    304304            user = UserModel._default_manager.get(pk=uid)
    305305        except (
    306306            TypeError,
    307             ValueError,
     307            # ValueError,
    308308            OverflowError,
    309309            UserModel.DoesNotExist,
    310310            ValidationError,

Then...

======================================================================
ERROR: test_confirm_valid_custom_user (auth_tests.test_views.CustomUserPasswordResetTest.test_confirm_valid_custom_user)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/.../django/tests/auth_tests/test_views.py", line 529, in test_confirm_valid_custom_user
    response = self.client.get(path)
               ^^^^^^^^^^^^^^^^^^^^^

...

File "/Users/.../django/django/contrib/auth/views.py", line 275, in dispatch
    self.user = self.get_user(kwargs["uidb64"])
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/.../django/django/contrib/auth/views.py", line 304, in get_user
    user = UserModel._default_manager.get(pk=uid)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
...
  File "/Users/.../django/django/db/models/lookups.py", line 30, in __init__
    self.rhs = self.get_prep_lookup()
               ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/.../django/django/db/models/fields/tuple_lookups.py", line 36, in get_prep_lookup
    self.check_rhs_is_tuple_or_list()
  File "/Users/.../django/django/db/models/fields/tuple_lookups.py", line 45, in check_rhs_is_tuple_or_list
    raise ValueError(
ValueError: 'exact' lookup of 'pk' must be a tuple or a list

----------------------------------------------------------------------
Ran 2 tests in 0.056s

FAILED (errors=1)

Change History (6)

comment:1 by Jacob Walls, 5 weeks ago

Description: modified (diff)

comment:2 by Jacob Walls, 5 weeks ago

Description: modified (diff)

comment:3 by Sarah Boyce, 4 weeks ago

Owner: set to Sarah Boyce
Severity: NormalRelease blocker
Status: newassigned
Summary: Add system check mentioning contrib.auth is not composite primary key readyPassword reset does not support a custom user model with a composite primary key
Triage Stage: UnreviewedAccepted
Type: Cleanup/optimizationBug

Good spot!
There's a chance that adding support for custom user models with CompositePrimaryKey's might be easier than adding a system check
Something roughly like:

  • django/contrib/auth/forms.py

    a b class PasswordResetForm(forms.Form):  
    478478        email_field_name = UserModel.get_email_field_name()
    479479        for user in self.get_users(email):
    480480            user_email = getattr(user, email_field_name)
     481            user_pk_bytes = force_bytes(UserModel._meta.pk.value_to_string(user))
    481482            context = {
    482483                "email": user_email,
    483484                "domain": domain,
    484485                "site_name": site_name,
    485                 "uid": urlsafe_base64_encode(force_bytes(user.pk)),
     486                "uid": urlsafe_base64_encode(user_pk_bytes),
    486487                "user": user,
    487488                "token": token_generator.make_token(user),
    488489                "protocol": "https" if use_https else "http",
  • django/contrib/auth/views.py

    diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py
    index a18cfdb347..cd810a1edc 100644
    a b class PasswordResetConfirmView(PasswordContextMixin, FormView):  
    301301        try:
    302302            # urlsafe_base64_decode() decodes to bytestring
    303303            uid = urlsafe_base64_decode(uidb64).decode()
    304             user = UserModel._default_manager.get(pk=uid)
     304            pk = UserModel._meta.pk.to_python(uid)
     305            user = UserModel._default_manager.get(pk=pk)
    305306        except (
    306307            TypeError,

If it's more complex, then perhaps we add some docs or a system check as suggested

comment:4 by Sarah Boyce, 4 weeks ago

Has patch: set

comment:5 by Jacob Walls, 4 weeks ago

Triage Stage: AcceptedReady for checkin

comment:6 by Sarah Boyce <42296566+sarahboyce@…>, 4 weeks ago

Resolution: fixed
Status: assignedclosed

In 23c6effa:

Fixed #36087 -- Supported password reset on a custom user model with a composite primary key.

Note: See TracTickets for help on using tickets.
Back to Top