Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The default reset password view crashes for Django3.1+ #543

Closed
SebastianRemander opened this issue Apr 29, 2021 · 4 comments
Closed

The default reset password view crashes for Django3.1+ #543

SebastianRemander opened this issue Apr 29, 2021 · 4 comments

Comments

@SebastianRemander
Copy link

SebastianRemander commented Apr 29, 2021

One line description of the issue

The default reset password view provided by Django auth modules (i.e., django.contrib.auth.views) does not work for Django3.1 and higher.

To reproduce the issue

latest djongo installed from source (djongo @ git+git://github.com/nesdis/djongo.git@a33aea3bdf5ac949620be042e168494aa2ade054)

  • django==3.0.5 -> the test passes
  • django==3.2 (or django==3.1.*) -> the test crashes

test_views.py

from django.contrib.auth import get_user_model
from django.core import mail
from django.test import Client, TestCase
from django.urls import reverse


User = get_user_model()


class TestViews(TestCase):

    def test_password_reset_view(self):
        email = "test@example.com"
        user = User(email=email)
        user.set_password("12345")
        user.save()
        self.assertEqual(User.objects.count(), 1)
        self.assertEqual(len(mail.outbox), 0)
        client = Client()

        response = client.get(reverse("password_reset"))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.template_name, ['registration/password_reset_form.html'])
        self.assertEqual(len(mail.outbox), 0)

        response = client.post(
            reverse("password_reset"),
            {"email": "not@found.com"},
        )
        # should return 302 for not existing users to not allow fishing accounts
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, "/reset-password/done/")
        self.assertEqual(len(mail.outbox), 0)

        response = client.post(
            reverse("password_reset"),
            {"email": email},
        )
        # should return 302 for existing user and send the email
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, "/reset-password/done/")
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(mail.outbox[0].to, [email])
        # unique url for actual reset available in context
        token = response.context[0]["token"]
        uid = response.context[0]["uid"]
        response = client.get(
            reverse(
                "password_reset_confirm",
                kwargs={"token": token, "uidb64": uid}
            ),
            follow=True,
        )
        self.assertEqual(response.status_code, 200)

        reset_redirect = response.request["PATH_INFO"]
        self.assertEqual(reset_redirect, f"/reset/{uid}/set-password/")

        response = client.post(
            reset_redirect,
            {"new_password1": "newpass", "new_password2": "newpass"},
        )
        self.assertEqual(response.status_code, 200)
        self.assertTrue(b"This password is too short." in response.content)

        response = client.post(
            reset_redirect,
            {"new_password1": "newpass12345", "new_password2": "newpass12345"},
        )
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, "/reset/done/")
        # cannot login with old pw
        response = client.post(
            '/login/', {'email': email, 'password': "12345"},
        )
        self.assertEqual(response.status_code, 200)
        self.assertTrue(b"Login failed." in response.content)

        # can login with new pw
        response = client.post(
            '/login/', {'email': email, 'password': "newpass12345"},
        )
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, "/")

Traceback of the crashing test

djongo.exceptions.SQLDecodeError is the relevant error...

ERROR: test_password_reset_view (DjangoApp.test.test_views.TestViews)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/djongo/cursor.py", line 51, in execute
    self.result = Query(
  File "/usr/local/lib/python3.9/site-packages/djongo/sql2mongo/query.py", line 784, in __init__
    self._query = self.parse()
  File "/usr/local/lib/python3.9/site-packages/djongo/sql2mongo/query.py", line 876, in parse
    raise e
  File "/usr/local/lib/python3.9/site-packages/djongo/sql2mongo/query.py", line 857, in parse
    return handler(self, statement)
  File "/usr/local/lib/python3.9/site-packages/djongo/sql2mongo/query.py", line 933, in _select
    return SelectQuery(self.db, self.connection_properties, sm, self._params)
  File "/usr/local/lib/python3.9/site-packages/djongo/sql2mongo/query.py", line 116, in __init__
    super().__init__(*args)
  File "/usr/local/lib/python3.9/site-packages/djongo/sql2mongo/query.py", line 62, in __init__
    self.parse()
  File "/usr/local/lib/python3.9/site-packages/djongo/sql2mongo/query.py", line 152, in parse
    self.where = WhereConverter(self, statement)
  File "/usr/local/lib/python3.9/site-packages/djongo/sql2mongo/converters.py", line 27, in __init__
    self.parse()
  File "/usr/local/lib/python3.9/site-packages/djongo/sql2mongo/converters.py", line 119, in parse
    self.op = WhereOp(
  File "/usr/local/lib/python3.9/site-packages/djongo/sql2mongo/operators.py", line 476, in __init__
    self.evaluate()
  File "/usr/local/lib/python3.9/site-packages/djongo/sql2mongo/operators.py", line 465, in evaluate
    op.evaluate()
  File "/usr/local/lib/python3.9/site-packages/djongo/sql2mongo/operators.py", line 465, in evaluate
    op.evaluate()
  File "/usr/local/lib/python3.9/site-packages/djongo/sql2mongo/operators.py", line 279, in evaluate
    raise SQLDecodeError
djongo.exceptions.SQLDecodeError:

	Keyword: None
	Sub SQL: None
	FAILED SQL: ('SELECT "DjangoApp_cuser"."id", "DjangoApp_cuser"."password", "DjangoApp_cuser"."last_login", "DjangoApp_cuser"."is_superuser", "DjangoApp_cuser"."email", "DjangoApp_cuser"."is_staff", "DjangoApp_cuser"."is_active" FROM "DjangoApp_cuser" WHERE ("DjangoApp_cuser"."email" iLIKE %(0)s AND "DjangoApp_cuser"."is_active")',)
	Params: (('not@found.com',),)
	Version: 1.3.4

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/usr/local/lib/python3.9/site-packages/djongo/cursor.py", line 59, in execute
    raise db_exe from e
djongo.database.DatabaseError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/opt/app/src/source/DjangoApp/test/test_views.py", line 28, in test_password_reset_view
    response = client.post(
  File "/usr/local/lib/python3.9/site-packages/django/test/client.py", line 751, in post
    response = super().post(path, data=data, content_type=content_type, secure=secure, **extra)
  File "/usr/local/lib/python3.9/site-packages/django/test/client.py", line 407, in post
    return self.generic('POST', path, post_data, content_type,
  File "/usr/local/lib/python3.9/site-packages/django/test/client.py", line 473, in generic
    return self.request(**r)
  File "/usr/local/lib/python3.9/site-packages/django/test/client.py", line 719, in request
    self.check_exception(response)
  File "/usr/local/lib/python3.9/site-packages/django/test/client.py", line 580, in check_exception
    raise exc_value
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/utils/decorators.py", line 130, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/contrib/auth/views.py", line 222, in dispatch
    return super().dispatch(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/generic/edit.py", line 142, in post
    return self.form_valid(form)
  File "/usr/local/lib/python3.9/site-packages/django/contrib/auth/views.py", line 235, in form_valid
    form.save(**opts)
  File "/usr/local/lib/python3.9/site-packages/django/contrib/auth/forms.py", line 298, in save
    for user in self.get_users(email):
  File "/usr/local/lib/python3.9/site-packages/django/contrib/auth/forms.py", line 274, in get_users
    return (
  File "/usr/local/lib/python3.9/site-packages/django/db/models/query.py", line 280, in __iter__
    self._fetch_all()
  File "/usr/local/lib/python3.9/site-packages/django/db/models/query.py", line 1324, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "/usr/local/lib/python3.9/site-packages/django/db/models/query.py", line 51, in __iter__
    results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)
  File "/usr/local/lib/python3.9/site-packages/django/db/models/sql/compiler.py", line 1169, in execute_sql
    cursor.execute(sql, params)
  File "/usr/local/lib/python3.9/site-packages/django/db/backends/utils.py", line 66, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "/usr/local/lib/python3.9/site-packages/django/db/backends/utils.py", line 75, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/usr/local/lib/python3.9/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/usr/local/lib/python3.9/site-packages/django/db/utils.py", line 90, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/usr/local/lib/python3.9/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/usr/local/lib/python3.9/site-packages/djongo/cursor.py", line 59, in execute
    raise db_exe from e
django.db.utils.DatabaseError

custom_user.py

Cuser is imported into models/init.py to have settings AUTH_USER_MODEL = 'DjangoApp.CUser' recognize it.

from djongo import models
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.utils.translation import gettext_lazy as _

from ..managers import CustomUserManager

class CUser(AbstractBaseUser, PermissionsMixin, models.Model):
    email = models.EmailField(unique=True, null=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.'
        ),
    )

    USERNAME_FIELD = 'email'
    objects = CustomUserManager()

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')

    def get_full_name(self):
        return self.email

    def get_short_name(self):
        return self.get_full_name()

    def __str__(self):
        return self.email

settings.py (relevant parts)

INSTALLED_APPS = [
    'DjangoApp',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
]


AUTH_USER_MODEL = 'DjangoApp.CUser'

urls.py (relevant parts)

from django.contrib.auth import views as auth_views

urlpatterns = [
    ## AUTH views (Django built-ins classes) ##
    path('change-password/', auth_views.PasswordChangeView.as_view(), name='password_change'),
    path('change-password/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),
    path('reset-password/', auth_views.PasswordResetView.as_view(), name='password_reset'),
    path('reset-password/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
    path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
]
@SebastianRemander
Copy link
Author

@nesdis @tony13tv this is at least some real issue that is preventing djongo from working with the modern django. I'm no sql2mongo expert so I'm not the best person to contribute and open a PR for this issue. However, I can guide and help you to setup this testcase for this repo - I think it would be good for you to have these kind of tests for the future, to not get version-locked 🙂

@Tisagh
Copy link

Tisagh commented May 10, 2021

Did you update Django after installing Djongo?
When I installed Djongo on my Django 3.2 build it automatically uninstalls Django and installs Django 3.0.5

@SebastianRemander
Copy link
Author

Did you update Django after installing Djongo?
When I installed Djongo on my Django 3.2 build it automatically uninstalls Django and installs Django 3.0.5

I did not update Django after installing Djongo. Please see "To reproduce the issue" section - installing latest djongo from the source does not uninstall Django, at least for me (pip 21.1.1 and python3.9).

@SebastianRemander
Copy link
Author

Monkey patch approach introduced in #562 (comment) solves this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants