Refs #17209 -- Removed login/logout and password reset/change function-based views.
Per deprecation timeline.
This commit is contained in:
parent
deb592b3e3
commit
4f313e284e
|
@ -15,8 +15,7 @@ from django.contrib.auth.tokens import default_token_generator
|
|||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.http import HttpResponseRedirect, QueryDict
|
||||
from django.shortcuts import resolve_url
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.deprecation import RemovedInDjango21Warning
|
||||
from django.utils.http import is_safe_url, urlsafe_base64_decode
|
||||
|
@ -107,23 +106,6 @@ class LoginView(SuccessURLAllowedHostsMixin, FormView):
|
|||
return context
|
||||
|
||||
|
||||
def login(request, template_name='registration/login.html',
|
||||
redirect_field_name=REDIRECT_FIELD_NAME,
|
||||
authentication_form=AuthenticationForm,
|
||||
extra_context=None, redirect_authenticated_user=False):
|
||||
warnings.warn(
|
||||
'The login() view is superseded by the class-based LoginView().',
|
||||
RemovedInDjango21Warning, stacklevel=2
|
||||
)
|
||||
return LoginView.as_view(
|
||||
template_name=template_name,
|
||||
redirect_field_name=redirect_field_name,
|
||||
form_class=authentication_form,
|
||||
extra_context=extra_context,
|
||||
redirect_authenticated_user=redirect_authenticated_user,
|
||||
)(request)
|
||||
|
||||
|
||||
class LogoutView(SuccessURLAllowedHostsMixin, TemplateView):
|
||||
"""
|
||||
Log out the user and display the 'You are logged out' message.
|
||||
|
@ -184,22 +166,6 @@ class LogoutView(SuccessURLAllowedHostsMixin, TemplateView):
|
|||
return context
|
||||
|
||||
|
||||
def logout(request, next_page=None,
|
||||
template_name='registration/logged_out.html',
|
||||
redirect_field_name=REDIRECT_FIELD_NAME,
|
||||
extra_context=None):
|
||||
warnings.warn(
|
||||
'The logout() view is superseded by the class-based LogoutView().',
|
||||
RemovedInDjango21Warning, stacklevel=2
|
||||
)
|
||||
return LogoutView.as_view(
|
||||
next_page=next_page,
|
||||
template_name=template_name,
|
||||
redirect_field_name=redirect_field_name,
|
||||
extra_context=extra_context,
|
||||
)(request)
|
||||
|
||||
|
||||
_sentinel = object()
|
||||
|
||||
|
||||
|
@ -234,143 +200,6 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N
|
|||
return HttpResponseRedirect(urlunparse(login_url_parts))
|
||||
|
||||
|
||||
# 4 views for password reset:
|
||||
# - password_reset sends the mail
|
||||
# - password_reset_done shows a success message for the above
|
||||
# - password_reset_confirm checks the link the user clicked and
|
||||
# prompts for a new password
|
||||
# - password_reset_complete shows a success message for the above
|
||||
|
||||
@csrf_protect
|
||||
def password_reset(request,
|
||||
template_name='registration/password_reset_form.html',
|
||||
email_template_name='registration/password_reset_email.html',
|
||||
subject_template_name='registration/password_reset_subject.txt',
|
||||
password_reset_form=PasswordResetForm,
|
||||
token_generator=default_token_generator,
|
||||
post_reset_redirect=None,
|
||||
from_email=None,
|
||||
extra_context=None,
|
||||
html_email_template_name=None,
|
||||
extra_email_context=None):
|
||||
warnings.warn("The password_reset() view is superseded by the "
|
||||
"class-based PasswordResetView().",
|
||||
RemovedInDjango21Warning, stacklevel=2)
|
||||
if post_reset_redirect is None:
|
||||
post_reset_redirect = reverse('password_reset_done')
|
||||
else:
|
||||
post_reset_redirect = resolve_url(post_reset_redirect)
|
||||
if request.method == "POST":
|
||||
form = password_reset_form(request.POST)
|
||||
if form.is_valid():
|
||||
opts = {
|
||||
'use_https': request.is_secure(),
|
||||
'token_generator': token_generator,
|
||||
'from_email': from_email,
|
||||
'email_template_name': email_template_name,
|
||||
'subject_template_name': subject_template_name,
|
||||
'request': request,
|
||||
'html_email_template_name': html_email_template_name,
|
||||
'extra_email_context': extra_email_context,
|
||||
}
|
||||
form.save(**opts)
|
||||
return HttpResponseRedirect(post_reset_redirect)
|
||||
else:
|
||||
form = password_reset_form()
|
||||
context = {
|
||||
'form': form,
|
||||
'title': _('Password reset'),
|
||||
}
|
||||
if extra_context is not None:
|
||||
context.update(extra_context)
|
||||
|
||||
return TemplateResponse(request, template_name, context)
|
||||
|
||||
|
||||
def password_reset_done(request,
|
||||
template_name='registration/password_reset_done.html',
|
||||
extra_context=None):
|
||||
warnings.warn("The password_reset_done() view is superseded by the "
|
||||
"class-based PasswordResetDoneView().",
|
||||
RemovedInDjango21Warning, stacklevel=2)
|
||||
context = {
|
||||
'title': _('Password reset sent'),
|
||||
}
|
||||
if extra_context is not None:
|
||||
context.update(extra_context)
|
||||
|
||||
return TemplateResponse(request, template_name, context)
|
||||
|
||||
|
||||
# Doesn't need csrf_protect since no-one can guess the URL
|
||||
@sensitive_post_parameters()
|
||||
@never_cache
|
||||
def password_reset_confirm(request, uidb64=None, token=None,
|
||||
template_name='registration/password_reset_confirm.html',
|
||||
token_generator=default_token_generator,
|
||||
set_password_form=SetPasswordForm,
|
||||
post_reset_redirect=None,
|
||||
extra_context=None):
|
||||
"""
|
||||
Check the hash in a password reset link and present a form for entering a
|
||||
new password.
|
||||
"""
|
||||
warnings.warn("The password_reset_confirm() view is superseded by the "
|
||||
"class-based PasswordResetConfirmView().",
|
||||
RemovedInDjango21Warning, stacklevel=2)
|
||||
assert uidb64 is not None and token is not None # checked by URLconf
|
||||
if post_reset_redirect is None:
|
||||
post_reset_redirect = reverse('password_reset_complete')
|
||||
else:
|
||||
post_reset_redirect = resolve_url(post_reset_redirect)
|
||||
try:
|
||||
# urlsafe_base64_decode() decodes to bytestring
|
||||
uid = urlsafe_base64_decode(uidb64).decode()
|
||||
user = UserModel._default_manager.get(pk=uid)
|
||||
except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist):
|
||||
user = None
|
||||
|
||||
if user is not None and token_generator.check_token(user, token):
|
||||
validlink = True
|
||||
title = _('Enter new password')
|
||||
if request.method == 'POST':
|
||||
form = set_password_form(user, request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return HttpResponseRedirect(post_reset_redirect)
|
||||
else:
|
||||
form = set_password_form(user)
|
||||
else:
|
||||
validlink = False
|
||||
form = None
|
||||
title = _('Password reset unsuccessful')
|
||||
context = {
|
||||
'form': form,
|
||||
'title': title,
|
||||
'validlink': validlink,
|
||||
}
|
||||
if extra_context is not None:
|
||||
context.update(extra_context)
|
||||
|
||||
return TemplateResponse(request, template_name, context)
|
||||
|
||||
|
||||
def password_reset_complete(request,
|
||||
template_name='registration/password_reset_complete.html',
|
||||
extra_context=None):
|
||||
warnings.warn("The password_reset_complete() view is superseded by the "
|
||||
"class-based PasswordResetCompleteView().",
|
||||
RemovedInDjango21Warning, stacklevel=2)
|
||||
context = {
|
||||
'login_url': resolve_url(settings.LOGIN_URL),
|
||||
'title': _('Password reset complete'),
|
||||
}
|
||||
if extra_context is not None:
|
||||
context.update(extra_context)
|
||||
|
||||
return TemplateResponse(request, template_name, context)
|
||||
|
||||
|
||||
# Class-based password reset views
|
||||
# - PasswordResetView sends the mail
|
||||
# - PasswordResetDoneView shows a success message for the above
|
||||
|
@ -511,57 +340,6 @@ class PasswordResetCompleteView(PasswordContextMixin, TemplateView):
|
|||
return context
|
||||
|
||||
|
||||
@sensitive_post_parameters()
|
||||
@csrf_protect
|
||||
@login_required
|
||||
def password_change(request,
|
||||
template_name='registration/password_change_form.html',
|
||||
post_change_redirect=None,
|
||||
password_change_form=PasswordChangeForm,
|
||||
extra_context=None):
|
||||
warnings.warn("The password_change() view is superseded by the "
|
||||
"class-based PasswordChangeView().",
|
||||
RemovedInDjango21Warning, stacklevel=2)
|
||||
if post_change_redirect is None:
|
||||
post_change_redirect = reverse('password_change_done')
|
||||
else:
|
||||
post_change_redirect = resolve_url(post_change_redirect)
|
||||
if request.method == "POST":
|
||||
form = password_change_form(user=request.user, data=request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
# Updating the password logs out all other sessions for the user
|
||||
# except the current one.
|
||||
update_session_auth_hash(request, form.user)
|
||||
return HttpResponseRedirect(post_change_redirect)
|
||||
else:
|
||||
form = password_change_form(user=request.user)
|
||||
context = {
|
||||
'form': form,
|
||||
'title': _('Password change'),
|
||||
}
|
||||
if extra_context is not None:
|
||||
context.update(extra_context)
|
||||
|
||||
return TemplateResponse(request, template_name, context)
|
||||
|
||||
|
||||
@login_required
|
||||
def password_change_done(request,
|
||||
template_name='registration/password_change_done.html',
|
||||
extra_context=None):
|
||||
warnings.warn("The password_change_done() view is superseded by the "
|
||||
"class-based PasswordChangeDoneView().",
|
||||
RemovedInDjango21Warning, stacklevel=2)
|
||||
context = {
|
||||
'title': _('Password change successful'),
|
||||
}
|
||||
if extra_context is not None:
|
||||
context.update(extra_context)
|
||||
|
||||
return TemplateResponse(request, template_name, context)
|
||||
|
||||
|
||||
class PasswordChangeView(PasswordContextMixin, FormView):
|
||||
form_class = PasswordChangeForm
|
||||
success_url = reverse_lazy('password_change_done')
|
||||
|
|
|
@ -714,7 +714,7 @@ details on these changes.
|
|||
remove calls to this method, and instead ensure that their auth related views
|
||||
are CSRF protected, which ensures that cookies are enabled.
|
||||
|
||||
* The version of :func:`django.contrib.auth.views.password_reset_confirm` that
|
||||
* The version of ``django.contrib.auth.views.password_reset_confirm()`` that
|
||||
supports base36 encoded user IDs
|
||||
(``django.contrib.auth.views.password_reset_confirm_uidb36``) will be
|
||||
removed. If your site has been running Django 1.6 for more than
|
||||
|
|
|
@ -10,7 +10,7 @@ CVE-2017-7233: Open redirect and possible XSS attack via user-supplied numeric r
|
|||
============================================================================================
|
||||
|
||||
Django relies on user input in some cases (e.g.
|
||||
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
|
||||
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
|
||||
to redirect the user to an "on success" URL. The security check for these
|
||||
redirects (namely ``django.utils.http.is_safe_url()``) considered some numeric
|
||||
URLs (e.g. ``http:999999999``) "safe" when they shouldn't be.
|
||||
|
|
|
@ -118,19 +118,19 @@ Minor features
|
|||
subclassed ``django.contrib.auth.hashers.PBKDF2PasswordHasher`` to change the
|
||||
default value.
|
||||
|
||||
* The :func:`~django.contrib.auth.views.logout` view sends "no-cache" headers
|
||||
* The ``django.contrib.auth.views.logout()`` view sends "no-cache" headers
|
||||
to prevent an issue where Safari caches redirects and prevents a user from
|
||||
being able to log out.
|
||||
|
||||
* Added the optional ``backend`` argument to :func:`~django.contrib.auth.login`
|
||||
* Added the optional ``backend`` argument to :func:`django.contrib.auth.login`
|
||||
to allow using it without credentials.
|
||||
|
||||
* The new :setting:`LOGOUT_REDIRECT_URL` setting controls the redirect of the
|
||||
:func:`~django.contrib.auth.views.logout` view, if the view doesn't get a
|
||||
``django.contrib.auth.views.logout()`` view, if the view doesn't get a
|
||||
``next_page`` argument.
|
||||
|
||||
* The new ``redirect_authenticated_user`` parameter for the
|
||||
:func:`~django.contrib.auth.views.login` view allows redirecting
|
||||
``django.contrib.auth.views.login()`` view allows redirecting
|
||||
authenticated users visiting the login page.
|
||||
|
||||
* The new :class:`~django.contrib.auth.backends.AllowAllUsersModelBackend` and
|
||||
|
|
|
@ -636,7 +636,7 @@ message as a nonexistent account.
|
|||
Password reset view now accepts ``from_email``
|
||||
----------------------------------------------
|
||||
|
||||
The :func:`django.contrib.auth.views.password_reset` view now accepts a
|
||||
The ``django.contrib.auth.views.password_reset()`` view now accepts a
|
||||
``from_email`` parameter, which is passed to the ``password_reset_form``’s
|
||||
``save()`` method as a keyword argument. If you are using this view with a
|
||||
custom password reset form, then you will need to ensure your form's ``save()``
|
||||
|
|
|
@ -36,7 +36,7 @@ which are accepted by some browsers. This allows a user to be redirected to
|
|||
an unsafe URL unexpectedly.
|
||||
|
||||
Django relies on user input in some cases (e.g.
|
||||
:func:`django.contrib.auth.views.login`, ``django.contrib.comments``, and
|
||||
``django.contrib.auth.views.login()``, ``django.contrib.comments``, and
|
||||
:doc:`i18n </topics/i18n/index>`) to redirect the user to an "on success" URL.
|
||||
The security checks for these redirects (namely
|
||||
``django.utils.http.is_safe_url()``) did not correctly validate some malformed
|
||||
|
|
|
@ -35,7 +35,7 @@ Mitigated possible XSS attack via user-supplied redirect URLs
|
|||
=============================================================
|
||||
|
||||
Django relies on user input in some cases (e.g.
|
||||
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
|
||||
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
|
||||
to redirect the user to an "on success" URL. The security checks for these
|
||||
redirects (namely ``django.utils.http.is_safe_url()``) didn't strip leading
|
||||
whitespace on the tested URL and as such considered URLs like
|
||||
|
|
|
@ -10,7 +10,7 @@ Mitigated possible XSS attack via user-supplied redirect URLs
|
|||
=============================================================
|
||||
|
||||
Django relies on user input in some cases (e.g.
|
||||
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
|
||||
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
|
||||
to redirect the user to an "on success" URL. The security checks for these
|
||||
redirects (namely ``django.utils.http.is_safe_url()``) accepted URLs with
|
||||
leading control characters and so considered URLs like ``\x08javascript:...``
|
||||
|
|
|
@ -14,7 +14,7 @@ Denial-of-service possibility in ``logout()`` view by filling session store
|
|||
===========================================================================
|
||||
|
||||
Previously, a session could be created when anonymously accessing the
|
||||
:func:`django.contrib.auth.views.logout` view (provided it wasn't decorated
|
||||
``django.contrib.auth.views.logout()`` view (provided it wasn't decorated
|
||||
with :func:`~django.contrib.auth.decorators.login_required` as done in the
|
||||
admin). This could allow an attacker to easily create many new session records
|
||||
by sending repeated requests, potentially filling up the session store or
|
||||
|
|
|
@ -13,7 +13,7 @@ Mitigated possible XSS attack via user-supplied redirect URLs
|
|||
=============================================================
|
||||
|
||||
Django relies on user input in some cases (e.g.
|
||||
:func:`django.contrib.auth.views.login`, ``django.contrib.comments``, and
|
||||
``django.contrib.auth.views.login()``, ``django.contrib.comments``, and
|
||||
:doc:`i18n </topics/i18n/index>`) to redirect the user to an "on success" URL.
|
||||
The security checks for these redirects (namely
|
||||
``django.utils.http.is_safe_url()``) didn't check if the scheme is ``http(s)``
|
||||
|
|
|
@ -10,7 +10,7 @@ Mitigated possible XSS attack via user-supplied redirect URLs
|
|||
=============================================================
|
||||
|
||||
Django relies on user input in some cases (e.g.
|
||||
:func:`django.contrib.auth.views.login`, ``django.contrib.comments``, and
|
||||
``django.contrib.auth.views.login()``, ``django.contrib.comments``, and
|
||||
:doc:`i18n </topics/i18n/index>`) to redirect the user to an "on success" URL.
|
||||
The security checks for these redirects (namely
|
||||
``django.utils.http.is_safe_url()``) didn't check if the scheme is ``http(s)``
|
||||
|
|
|
@ -36,7 +36,7 @@ which are accepted by some browsers. This allows a user to be redirected to
|
|||
an unsafe URL unexpectedly.
|
||||
|
||||
Django relies on user input in some cases (e.g.
|
||||
:func:`django.contrib.auth.views.login`, ``django.contrib.comments``, and
|
||||
``django.contrib.auth.views.login()``, ``django.contrib.comments``, and
|
||||
:doc:`i18n </topics/i18n/index>`) to redirect the user to an "on success" URL.
|
||||
The security checks for these redirects (namely
|
||||
``django.utils.http.is_safe_url()``) did not correctly validate some malformed
|
||||
|
|
|
@ -34,7 +34,7 @@ Mitigated possible XSS attack via user-supplied redirect URLs
|
|||
=============================================================
|
||||
|
||||
Django relies on user input in some cases (e.g.
|
||||
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
|
||||
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
|
||||
to redirect the user to an "on success" URL. The security checks for these
|
||||
redirects (namely ``django.utils.http.is_safe_url()``) didn't strip leading
|
||||
whitespace on the tested URL and as such considered URLs like
|
||||
|
|
|
@ -27,7 +27,7 @@ Mitigated possible XSS attack via user-supplied redirect URLs
|
|||
=============================================================
|
||||
|
||||
Django relies on user input in some cases (e.g.
|
||||
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
|
||||
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
|
||||
to redirect the user to an "on success" URL. The security checks for these
|
||||
redirects (namely ``django.utils.http.is_safe_url()``) accepted URLs with
|
||||
leading control characters and so considered URLs like ``\x08javascript:...``
|
||||
|
|
|
@ -36,7 +36,7 @@ which are accepted by some browsers. This allows a user to be redirected to
|
|||
an unsafe URL unexpectedly.
|
||||
|
||||
Django relies on user input in some cases (e.g.
|
||||
:func:`django.contrib.auth.views.login`, ``django.contrib.comments``, and
|
||||
``django.contrib.auth.views.login()``, ``django.contrib.comments``, and
|
||||
:doc:`i18n </topics/i18n/index>`) to redirect the user to an "on success" URL.
|
||||
The security checks for these redirects (namely
|
||||
``django.utils.http.is_safe_url()``) did not correctly validate some malformed
|
||||
|
|
|
@ -280,10 +280,10 @@ Minor features
|
|||
:attr:`~django.http.HttpResponse.reason_phrase`.
|
||||
|
||||
* When giving the URL of the next page for
|
||||
:func:`~django.contrib.auth.views.logout`,
|
||||
:func:`~django.contrib.auth.views.password_reset`,
|
||||
:func:`~django.contrib.auth.views.password_reset_confirm`,
|
||||
and :func:`~django.contrib.auth.views.password_change`, you can now pass
|
||||
``django.contrib.auth.views.logout()``,
|
||||
``django.contrib.auth.views.password_reset()``,
|
||||
``django.contrib.auth.views.password_reset_confirm()``,
|
||||
and ``django.contrib.auth.views.password_change()``, you can now pass
|
||||
URL names and they will be resolved.
|
||||
|
||||
* The new :option:`dumpdata --pks` option specifies the primary keys of objects
|
||||
|
@ -742,17 +742,17 @@ can set the
|
|||
|
||||
Past versions of Django used base 36 encoding of the ``User`` primary key in
|
||||
the password reset views and URLs
|
||||
(:func:`django.contrib.auth.views.password_reset_confirm`). Base 36 encoding is
|
||||
(``django.contrib.auth.views.password_reset_confirm()``). Base 36 encoding is
|
||||
sufficient if the user primary key is an integer, however, with the
|
||||
introduction of custom user models in Django 1.5, that assumption may no longer
|
||||
be true.
|
||||
|
||||
:func:`django.contrib.auth.views.password_reset_confirm` has been modified to
|
||||
``django.contrib.auth.views.password_reset_confirm()`` has been modified to
|
||||
take a ``uidb64`` parameter instead of ``uidb36``. If you are reversing this
|
||||
view, for example in a custom ``password_reset_email.html`` template, be sure
|
||||
to update your code.
|
||||
|
||||
A temporary shim for :func:`django.contrib.auth.views.password_reset_confirm`
|
||||
A temporary shim for ``django.contrib.auth.views.password_reset_confirm()``
|
||||
that will allow password reset links generated prior to Django 1.6 to continue
|
||||
to work has been added to provide backwards compatibility; this will be removed
|
||||
in Django 1.7. Thus, as long as your site has been running Django 1.6 for more
|
||||
|
|
|
@ -10,7 +10,7 @@ Denial-of-service possibility in ``logout()`` view by filling session store
|
|||
===========================================================================
|
||||
|
||||
Previously, a session could be created when anonymously accessing the
|
||||
:func:`django.contrib.auth.views.logout` view (provided it wasn't decorated
|
||||
``django.contrib.auth.views.logout()`` view (provided it wasn't decorated
|
||||
with :func:`~django.contrib.auth.decorators.login_required` as done in the
|
||||
admin). This could allow an attacker to easily create many new session records
|
||||
by sending repeated requests, potentially filling up the session store or
|
||||
|
|
|
@ -34,7 +34,7 @@ Mitigated possible XSS attack via user-supplied redirect URLs
|
|||
=============================================================
|
||||
|
||||
Django relies on user input in some cases (e.g.
|
||||
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
|
||||
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
|
||||
to redirect the user to an "on success" URL. The security checks for these
|
||||
redirects (namely ``django.utils.http.is_safe_url()``) didn't strip leading
|
||||
whitespace on the tested URL and as such considered URLs like
|
||||
|
|
|
@ -27,7 +27,7 @@ Mitigated possible XSS attack via user-supplied redirect URLs
|
|||
=============================================================
|
||||
|
||||
Django relies on user input in some cases (e.g.
|
||||
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
|
||||
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
|
||||
to redirect the user to an "on success" URL. The security checks for these
|
||||
redirects (namely ``django.utils.http.is_safe_url()``) accepted URLs with
|
||||
leading control characters and so considered URLs like ``\x08javascript:...``
|
||||
|
|
|
@ -421,7 +421,7 @@ Minor features
|
|||
<django.contrib.auth.forms.AuthenticationForm.confirm_login_allowed>` method
|
||||
to more easily customize the login policy.
|
||||
|
||||
* :func:`django.contrib.auth.views.password_reset` takes an optional
|
||||
* ``django.contrib.auth.views.password_reset()`` takes an optional
|
||||
``html_email_template_name`` parameter used to send a multipart HTML email
|
||||
for password resets.
|
||||
|
||||
|
@ -1846,7 +1846,7 @@ remove usage of these features.
|
|||
* The ``check_for_test_cookie`` method in
|
||||
:class:`~django.contrib.auth.forms.AuthenticationForm` is removed.
|
||||
|
||||
* The version of :func:`django.contrib.auth.views.password_reset_confirm` that
|
||||
* The version of ``django.contrib.auth.views.password_reset_confirm()`` that
|
||||
supports base36 encoded user IDs
|
||||
(``django.contrib.auth.views.password_reset_confirm_uidb36``) is removed.
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ CVE-2016-2512: Malicious redirect and possible XSS attack via user-supplied redi
|
|||
===============================================================================================================
|
||||
|
||||
Django relies on user input in some cases (e.g.
|
||||
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
|
||||
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
|
||||
to redirect the user to an "on success" URL. The security check for these
|
||||
redirects (namely ``django.utils.http.is_safe_url()``) considered some URLs
|
||||
with basic authentication credentials "safe" when they shouldn't be.
|
||||
|
|
|
@ -10,7 +10,7 @@ CVE-2017-7233: Open redirect and possible XSS attack via user-supplied numeric r
|
|||
============================================================================================
|
||||
|
||||
Django relies on user input in some cases (e.g.
|
||||
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
|
||||
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
|
||||
to redirect the user to an "on success" URL. The security check for these
|
||||
redirects (namely ``django.utils.http.is_safe_url()``) considered some numeric
|
||||
URLs (e.g. ``http:999999999``) "safe" when they shouldn't be.
|
||||
|
|
|
@ -10,7 +10,7 @@ Denial-of-service possibility in ``logout()`` view by filling session store
|
|||
===========================================================================
|
||||
|
||||
Previously, a session could be created when anonymously accessing the
|
||||
:func:`django.contrib.auth.views.logout` view (provided it wasn't decorated
|
||||
``django.contrib.auth.views.logout()`` view (provided it wasn't decorated
|
||||
with :func:`~django.contrib.auth.decorators.login_required` as done in the
|
||||
admin). This could allow an attacker to easily create many new session records
|
||||
by sending repeated requests, potentially filling up the session store or
|
||||
|
|
|
@ -11,7 +11,7 @@ CVE-2017-7233: Open redirect and possible XSS attack via user-supplied numeric r
|
|||
============================================================================================
|
||||
|
||||
Django relies on user input in some cases (e.g.
|
||||
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
|
||||
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
|
||||
to redirect the user to an "on success" URL. The security check for these
|
||||
redirects (namely ``django.utils.http.is_safe_url()``) considered some numeric
|
||||
URLs (e.g. ``http:999999999``) "safe" when they shouldn't be.
|
||||
|
|
|
@ -10,7 +10,7 @@ CVE-2016-2512: Malicious redirect and possible XSS attack via user-supplied redi
|
|||
===============================================================================================================
|
||||
|
||||
Django relies on user input in some cases (e.g.
|
||||
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
|
||||
``django.contrib.auth.views.login()`` and :doc:`i18n </topics/i18n/index>`)
|
||||
to redirect the user to an "on success" URL. The security check for these
|
||||
redirects (namely ``django.utils.http.is_safe_url()``) considered some URLs
|
||||
with basic authentication credentials "safe" when they shouldn't be.
|
||||
|
|
|
@ -216,7 +216,7 @@ Minor features
|
|||
makes it possible to use ``REMOTE_USER`` for setups where the header is only
|
||||
populated on login pages instead of every request in the session.
|
||||
|
||||
* The :func:`~django.contrib.auth.views.password_reset` view accepts an
|
||||
* The ``django.contrib.auth.views.password_reset()`` view accepts an
|
||||
``extra_email_context`` parameter.
|
||||
|
||||
:mod:`django.contrib.contenttypes`
|
||||
|
|
|
@ -223,3 +223,9 @@ Features removed in 2.1
|
|||
These features have reached the end of their deprecation cycle and are removed
|
||||
in Django 2.1. See :ref:`deprecated-features-1.11` for details, including how
|
||||
to remove usage of these features.
|
||||
in Django 2.1. See :ref:`deprecated-features-1.11` and for details, including
|
||||
how to remove usage of these features.
|
||||
|
||||
* ``contrib.auth.views.login()``, ``logout()``, ``password_change()``,
|
||||
``password_change_done()``, ``password_reset()``, ``password_reset_done()``,
|
||||
``password_reset_confirm()``, and ``password_reset_complete()`` are removed.
|
||||
|
|
|
@ -945,16 +945,6 @@ All authentication views
|
|||
This is a list with all the views ``django.contrib.auth`` provides. For
|
||||
implementation details see :ref:`using-the-views`.
|
||||
|
||||
.. function:: login(request, template_name=`registration/login.html`, redirect_field_name='next', authentication_form=AuthenticationForm, extra_context=None, redirect_authenticated_user=False)
|
||||
|
||||
.. deprecated:: 1.11
|
||||
|
||||
The ``login`` function-based view should be replaced by the class-based
|
||||
:class:`LoginView`.
|
||||
|
||||
The optional arguments of this view are similar to the class-based
|
||||
``LoginView`` attributes.
|
||||
|
||||
.. class:: LoginView
|
||||
|
||||
.. versionadded:: 1.11
|
||||
|
@ -1093,16 +1083,6 @@ implementation details see :ref:`using-the-views`.
|
|||
``get_user()`` method which returns the authenticated user object (this
|
||||
method is only ever called after successful form validation).
|
||||
|
||||
.. function:: logout(request, next_page=None, template_name='registration/logged_out.html', redirect_field_name='next', extra_context=None)
|
||||
|
||||
.. deprecated:: 1.11
|
||||
|
||||
The ``logout`` function-based view should be replaced by the
|
||||
class-based :class:`LogoutView`.
|
||||
|
||||
The optional arguments of this view are similar to the class-based
|
||||
``LogoutView`` attributes.
|
||||
|
||||
.. class:: LogoutView
|
||||
|
||||
.. versionadded:: 1.11
|
||||
|
@ -1165,18 +1145,6 @@ implementation details see :ref:`using-the-views`.
|
|||
The unused ``extra_context`` parameter is deprecated and will be
|
||||
removed in Django 2.1.
|
||||
|
||||
.. function:: password_change(request, template_name='registration/password_change_form.html', post_change_redirect=None, password_change_form=PasswordChangeForm, extra_context=None)
|
||||
|
||||
.. deprecated:: 1.11
|
||||
|
||||
The ``password_change`` function-based view should be replaced by the
|
||||
class-based :class:`PasswordChangeView`.
|
||||
|
||||
The optional arguments of this view are similar to the class-based
|
||||
``PasswordChangeView`` attributes, except the ``post_change_redirect`` and
|
||||
``password_change_form`` arguments which map to the ``success_url`` and
|
||||
``form_class`` attributes of the class-based view.
|
||||
|
||||
.. class:: PasswordChangeView
|
||||
|
||||
.. versionadded:: 1.11
|
||||
|
@ -1206,16 +1174,6 @@ implementation details see :ref:`using-the-views`.
|
|||
|
||||
* ``form``: The password change form (see ``form_class`` above).
|
||||
|
||||
.. function:: password_change_done(request, template_name='registration/password_change_done.html', extra_context=None)
|
||||
|
||||
.. deprecated:: 1.11
|
||||
|
||||
The ``password_change_done`` function-based view should be replaced by
|
||||
the class-based :class:`PasswordChangeDoneView`.
|
||||
|
||||
The optional arguments of this view are similar to the class-based
|
||||
``PasswordChangeDoneView`` attributes.
|
||||
|
||||
.. class:: PasswordChangeDoneView
|
||||
|
||||
.. versionadded:: 1.11
|
||||
|
@ -1233,18 +1191,6 @@ implementation details see :ref:`using-the-views`.
|
|||
* ``extra_context``: A dictionary of context data that will be added to the
|
||||
default context data passed to the template.
|
||||
|
||||
.. function:: password_reset(request, template_name='registration/password_reset_form.html', email_template_name='registration/password_reset_email.html', subject_template_name='registration/password_reset_subject.txt', password_reset_form=PasswordResetForm, token_generator=default_token_generator, post_reset_redirect=None, from_email=None, extra_context=None, html_email_template_name=None, extra_email_context=None)
|
||||
|
||||
.. deprecated:: 1.11
|
||||
|
||||
The ``password_reset`` function-based view should be replaced by the
|
||||
class-based :class:`PasswordResetView`.
|
||||
|
||||
The optional arguments of this view are similar to the class-based
|
||||
``PasswordResetView`` attributes, except the ``post_reset_redirect`` and
|
||||
``password_reset_form`` arguments which map to the ``success_url`` and
|
||||
``form_class`` attributes of the class-based view.
|
||||
|
||||
.. class:: PasswordResetView
|
||||
|
||||
.. versionadded:: 1.11
|
||||
|
@ -1345,16 +1291,6 @@ implementation details see :ref:`using-the-views`.
|
|||
The same template context is used for subject template. Subject must be
|
||||
single line plain text string.
|
||||
|
||||
.. function:: password_reset_done(request, template_name='registration/password_reset_done.html', extra_context=None)
|
||||
|
||||
.. deprecated:: 1.11
|
||||
|
||||
The ``password_reset_done`` function-based view should be replaced by
|
||||
the class-based :class:`PasswordResetDoneView`.
|
||||
|
||||
The optional arguments of this view are similar to the class-based
|
||||
``PasswordResetDoneView`` attributes.
|
||||
|
||||
.. class:: PasswordResetDoneView
|
||||
|
||||
.. versionadded:: 1.11
|
||||
|
@ -1380,18 +1316,6 @@ implementation details see :ref:`using-the-views`.
|
|||
* ``extra_context``: A dictionary of context data that will be added to the
|
||||
default context data passed to the template.
|
||||
|
||||
.. function:: password_reset_confirm(request, uidb64=None, token=None, template_name='registration/password_reset_confirm.html', token_generator=default_token_generator, set_password_form=SetPasswordForm, post_reset_redirect=None, extra_context=None)
|
||||
|
||||
.. deprecated:: 1.11
|
||||
|
||||
The ``password_reset_confirm`` function-based view should be replaced by
|
||||
the class-based :class:`PasswordResetConfirmView`.
|
||||
|
||||
The optional arguments of this view are similar to the class-based
|
||||
``PasswordResetConfirmView`` attributes, except the ``post_reset_redirect``
|
||||
and ``set_password_form`` arguments which map to the ``success_url`` and
|
||||
``form_class`` attributes of the class-based view.
|
||||
|
||||
.. class:: PasswordResetConfirmView
|
||||
|
||||
.. versionadded:: 1.11
|
||||
|
@ -1442,16 +1366,6 @@ implementation details see :ref:`using-the-views`.
|
|||
* ``validlink``: Boolean, True if the link (combination of ``uidb64`` and
|
||||
``token``) is valid or unused yet.
|
||||
|
||||
.. function:: password_reset_complete(request, template_name='registration/password_reset_complete.html', extra_context=None)
|
||||
|
||||
.. deprecated:: 1.11
|
||||
|
||||
The ``password_reset_complete`` function-based view should be replaced
|
||||
by the class-based :class:`PasswordResetCompleteView`.
|
||||
|
||||
The optional arguments of this view are similar to the class-based
|
||||
``PasswordResetCompleteView`` attributes.
|
||||
|
||||
.. class:: PasswordResetCompleteView
|
||||
|
||||
.. versionadded:: 1.11
|
||||
|
|
|
@ -1,488 +0,0 @@
|
|||
import datetime
|
||||
import itertools
|
||||
import re
|
||||
from urllib.parse import ParseResult, urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import SESSION_KEY
|
||||
from django.contrib.auth.forms import (
|
||||
AuthenticationForm, PasswordChangeForm, SetPasswordForm,
|
||||
)
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.views import login, logout
|
||||
from django.core import mail
|
||||
from django.http import QueryDict
|
||||
from django.test import RequestFactory, TestCase, override_settings
|
||||
from django.test.utils import ignore_warnings, patch_logger
|
||||
from django.utils.deprecation import RemovedInDjango21Warning
|
||||
|
||||
from .models import CustomUser, UUIDUser
|
||||
from .settings import AUTH_TEMPLATES
|
||||
|
||||
|
||||
@override_settings(
|
||||
LANGUAGES=[('en', 'English')],
|
||||
LANGUAGE_CODE='en',
|
||||
TEMPLATES=AUTH_TEMPLATES,
|
||||
ROOT_URLCONF='auth_tests.urls_deprecated',
|
||||
)
|
||||
class AuthViewsTestCase(TestCase):
|
||||
"""
|
||||
Helper base class for all the follow test cases.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.u1 = User.objects.create_user(username='testclient', password='password', email='testclient@example.com')
|
||||
cls.u3 = User.objects.create_user(username='staff', password='password', email='staffmember@example.com')
|
||||
|
||||
def login(self, username='testclient', password='password'):
|
||||
response = self.client.post('/login/', {
|
||||
'username': username,
|
||||
'password': password,
|
||||
})
|
||||
self.assertIn(SESSION_KEY, self.client.session)
|
||||
return response
|
||||
|
||||
def logout(self):
|
||||
response = self.client.get('/admin/logout/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn(SESSION_KEY, self.client.session)
|
||||
|
||||
def assertFormError(self, response, error):
|
||||
"""Assert that error is found in response.context['form'] errors"""
|
||||
form_errors = list(itertools.chain(*response.context['form'].errors.values()))
|
||||
self.assertIn(str(error), form_errors)
|
||||
|
||||
def assertURLEqual(self, url, expected, parse_qs=False):
|
||||
"""
|
||||
Given two URLs, make sure all their components (the ones given by
|
||||
urlparse) are equal, only comparing components that are present in both
|
||||
URLs.
|
||||
If `parse_qs` is True, then the querystrings are parsed with QueryDict.
|
||||
This is useful if you don't want the order of parameters to matter.
|
||||
Otherwise, the query strings are compared as-is.
|
||||
"""
|
||||
fields = ParseResult._fields
|
||||
|
||||
for attr, x, y in zip(fields, urlparse(url), urlparse(expected)):
|
||||
if parse_qs and attr == 'query':
|
||||
x, y = QueryDict(x), QueryDict(y)
|
||||
if x and y and x != y:
|
||||
self.fail("%r != %r (%s doesn't match)" % (url, expected, attr))
|
||||
|
||||
|
||||
@ignore_warnings(category=RemovedInDjango21Warning)
|
||||
class PasswordResetTest(AuthViewsTestCase):
|
||||
|
||||
def test_email_not_found(self):
|
||||
"""If the provided email is not registered, don't raise any error but
|
||||
also don't send any email."""
|
||||
response = self.client.get('/password_reset/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
def test_email_found(self):
|
||||
"Email is sent if a valid email address is provided for password reset"
|
||||
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertIn("http://", mail.outbox[0].body)
|
||||
self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email)
|
||||
# optional multipart text/html email has been added. Make sure original,
|
||||
# default functionality is 100% the same
|
||||
self.assertFalse(mail.outbox[0].message().is_multipart())
|
||||
|
||||
def test_extra_email_context(self):
|
||||
"""
|
||||
extra_email_context should be available in the email template context.
|
||||
"""
|
||||
response = self.client.post(
|
||||
'/password_reset_extra_email_context/',
|
||||
{'email': 'staffmember@example.com'},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertIn('Email email context: "Hello!"', mail.outbox[0].body)
|
||||
|
||||
def test_html_mail_template(self):
|
||||
"""
|
||||
A multipart email with text/plain and text/html is sent
|
||||
if the html_email_template parameter is passed to the view
|
||||
"""
|
||||
response = self.client.post('/password_reset/html_email_template/', {'email': 'staffmember@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
message = mail.outbox[0].message()
|
||||
self.assertEqual(len(message.get_payload()), 2)
|
||||
self.assertTrue(message.is_multipart())
|
||||
self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain')
|
||||
self.assertEqual(message.get_payload(1).get_content_type(), 'text/html')
|
||||
self.assertNotIn('<html>', message.get_payload(0).get_payload())
|
||||
self.assertIn('<html>', message.get_payload(1).get_payload())
|
||||
|
||||
def test_email_found_custom_from(self):
|
||||
"Email is sent if a valid email address is provided for password reset when a custom from_email is provided."
|
||||
response = self.client.post('/password_reset_from_email/', {'email': 'staffmember@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual("staffmember@example.com", mail.outbox[0].from_email)
|
||||
|
||||
# Skip any 500 handler action (like sending more mail...)
|
||||
@override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
|
||||
def test_poisoned_http_host(self):
|
||||
"Poisoned HTTP_HOST headers can't be used for reset emails"
|
||||
# This attack is based on the way browsers handle URLs. The colon
|
||||
# should be used to separate the port, but if the URL contains an @,
|
||||
# the colon is interpreted as part of a username for login purposes,
|
||||
# making 'evil.com' the request domain. Since HTTP_HOST is used to
|
||||
# produce a meaningful reset URL, we need to be certain that the
|
||||
# HTTP_HOST header isn't poisoned. This is done as a check when get_host()
|
||||
# is invoked, but we check here as a practical consequence.
|
||||
with patch_logger('django.security.DisallowedHost', 'error') as logger_calls:
|
||||
response = self.client.post(
|
||||
'/password_reset/',
|
||||
{'email': 'staffmember@example.com'},
|
||||
HTTP_HOST='www.example:dr.frankenstein@evil.tld'
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
self.assertEqual(len(logger_calls), 1)
|
||||
|
||||
# Skip any 500 handler action (like sending more mail...)
|
||||
@override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
|
||||
def test_poisoned_http_host_admin_site(self):
|
||||
"Poisoned HTTP_HOST headers can't be used for reset emails on admin views"
|
||||
with patch_logger('django.security.DisallowedHost', 'error') as logger_calls:
|
||||
response = self.client.post(
|
||||
'/admin_password_reset/',
|
||||
{'email': 'staffmember@example.com'},
|
||||
HTTP_HOST='www.example:dr.frankenstein@evil.tld'
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
self.assertEqual(len(logger_calls), 1)
|
||||
|
||||
def _test_confirm_start(self):
|
||||
# Start by creating the email
|
||||
self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
return self._read_signup_email(mail.outbox[0])
|
||||
|
||||
def _read_signup_email(self, email):
|
||||
urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body)
|
||||
self.assertIsNotNone(urlmatch, "No URL found in sent email")
|
||||
return urlmatch.group(), urlmatch.groups()[0]
|
||||
|
||||
def test_confirm_valid(self):
|
||||
url, path = self._test_confirm_start()
|
||||
response = self.client.get(path)
|
||||
# redirect to a 'complete' page:
|
||||
self.assertContains(response, "Please enter your new password")
|
||||
|
||||
def test_confirm_invalid(self):
|
||||
url, path = self._test_confirm_start()
|
||||
# Let's munge the token in the path, but keep the same length,
|
||||
# in case the URLconf will reject a different length.
|
||||
path = path[:-5] + ("0" * 4) + path[-1]
|
||||
|
||||
response = self.client.get(path)
|
||||
self.assertContains(response, "The password reset link was invalid")
|
||||
|
||||
def test_confirm_invalid_user(self):
|
||||
# We get a 200 response for a nonexistent user, not a 404
|
||||
response = self.client.get('/reset/123456/1-1/')
|
||||
self.assertContains(response, "The password reset link was invalid")
|
||||
|
||||
def test_confirm_overflow_user(self):
|
||||
# We get a 200 response for a base36 user id that overflows int
|
||||
response = self.client.get('/reset/zzzzzzzzzzzzz/1-1/')
|
||||
self.assertContains(response, "The password reset link was invalid")
|
||||
|
||||
def test_confirm_invalid_post(self):
|
||||
# Same as test_confirm_invalid, but trying
|
||||
# to do a POST instead.
|
||||
url, path = self._test_confirm_start()
|
||||
path = path[:-5] + ("0" * 4) + path[-1]
|
||||
|
||||
self.client.post(path, {
|
||||
'new_password1': 'anewpassword',
|
||||
'new_password2': ' anewpassword',
|
||||
})
|
||||
# Check the password has not been changed
|
||||
u = User.objects.get(email='staffmember@example.com')
|
||||
self.assertTrue(not u.check_password("anewpassword"))
|
||||
|
||||
def test_confirm_complete(self):
|
||||
url, path = self._test_confirm_start()
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'})
|
||||
# Check the password has been changed
|
||||
u = User.objects.get(email='staffmember@example.com')
|
||||
self.assertTrue(u.check_password("anewpassword"))
|
||||
|
||||
# Check we can't use the link again
|
||||
response = self.client.get(path)
|
||||
self.assertContains(response, "The password reset link was invalid")
|
||||
|
||||
def test_confirm_different_passwords(self):
|
||||
url, path = self._test_confirm_start()
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'x'})
|
||||
self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch'])
|
||||
|
||||
def test_reset_redirect_default(self):
|
||||
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_reset/done/')
|
||||
|
||||
def test_reset_custom_redirect(self):
|
||||
response = self.client.post('/password_reset/custom_redirect/', {'email': 'staffmember@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/custom/')
|
||||
|
||||
def test_reset_custom_redirect_named(self):
|
||||
response = self.client.post('/password_reset/custom_redirect/named/', {'email': 'staffmember@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_reset/')
|
||||
|
||||
def test_confirm_redirect_default(self):
|
||||
url, path = self._test_confirm_start()
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/reset/done/')
|
||||
|
||||
def test_confirm_redirect_custom(self):
|
||||
url, path = self._test_confirm_start()
|
||||
path = path.replace('/reset/', '/reset/custom/')
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/custom/')
|
||||
|
||||
def test_confirm_redirect_custom_named(self):
|
||||
url, path = self._test_confirm_start()
|
||||
path = path.replace('/reset/', '/reset/custom/named/')
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_reset/')
|
||||
|
||||
def test_confirm_display_user_from_form(self):
|
||||
url, path = self._test_confirm_start()
|
||||
response = self.client.get(path)
|
||||
|
||||
# #16919 -- The ``password_reset_confirm`` view should pass the user
|
||||
# object to the ``SetPasswordForm``, even on GET requests.
|
||||
# For this test, we render ``{{ form.user }}`` in the template
|
||||
# ``registration/password_reset_confirm.html`` so that we can test this.
|
||||
username = User.objects.get(email='staffmember@example.com').username
|
||||
self.assertContains(response, "Hello, %s." % username)
|
||||
|
||||
# However, the view should NOT pass any user object on a form if the
|
||||
# password reset link was invalid.
|
||||
response = self.client.get('/reset/zzzzzzzzzzzzz/1-1/')
|
||||
self.assertContains(response, "Hello, .")
|
||||
|
||||
|
||||
@ignore_warnings(category=RemovedInDjango21Warning)
|
||||
@override_settings(AUTH_USER_MODEL='auth_tests.CustomUser')
|
||||
class CustomUserPasswordResetTest(AuthViewsTestCase):
|
||||
user_email = 'staffmember@example.com'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.u1 = CustomUser.custom_objects.create(
|
||||
email='staffmember@example.com',
|
||||
date_of_birth=datetime.date(1976, 11, 8),
|
||||
)
|
||||
cls.u1.set_password('password')
|
||||
cls.u1.save()
|
||||
|
||||
def _test_confirm_start(self):
|
||||
# Start by creating the email
|
||||
response = self.client.post('/password_reset/', {'email': self.user_email})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
return self._read_signup_email(mail.outbox[0])
|
||||
|
||||
def _read_signup_email(self, email):
|
||||
urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body)
|
||||
self.assertIsNotNone(urlmatch, "No URL found in sent email")
|
||||
return urlmatch.group(), urlmatch.groups()[0]
|
||||
|
||||
def test_confirm_valid_custom_user(self):
|
||||
url, path = self._test_confirm_start()
|
||||
response = self.client.get(path)
|
||||
# redirect to a 'complete' page:
|
||||
self.assertContains(response, "Please enter your new password")
|
||||
# then submit a new password
|
||||
response = self.client.post(path, {
|
||||
'new_password1': 'anewpassword',
|
||||
'new_password2': 'anewpassword',
|
||||
})
|
||||
self.assertRedirects(response, '/reset/done/')
|
||||
|
||||
|
||||
@ignore_warnings(category=RemovedInDjango21Warning)
|
||||
@override_settings(AUTH_USER_MODEL='auth_tests.UUIDUser')
|
||||
class UUIDUserPasswordResetTest(CustomUserPasswordResetTest):
|
||||
|
||||
def _test_confirm_start(self):
|
||||
# instead of fixture
|
||||
UUIDUser.objects.create_user(
|
||||
email=self.user_email,
|
||||
username='foo',
|
||||
password='foo',
|
||||
)
|
||||
return super()._test_confirm_start()
|
||||
|
||||
|
||||
@ignore_warnings(category=RemovedInDjango21Warning)
|
||||
class ChangePasswordTest(AuthViewsTestCase):
|
||||
|
||||
def fail_login(self, password='password'):
|
||||
response = self.client.post('/login/', {
|
||||
'username': 'testclient',
|
||||
'password': password,
|
||||
})
|
||||
self.assertFormError(response, AuthenticationForm.error_messages['invalid_login'] % {
|
||||
'username': User._meta.get_field('username').verbose_name
|
||||
})
|
||||
|
||||
def logout(self):
|
||||
self.client.get('/logout/')
|
||||
|
||||
def test_password_change_fails_with_invalid_old_password(self):
|
||||
self.login()
|
||||
response = self.client.post('/password_change/', {
|
||||
'old_password': 'donuts',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
self.assertFormError(response, PasswordChangeForm.error_messages['password_incorrect'])
|
||||
|
||||
def test_password_change_fails_with_mismatched_passwords(self):
|
||||
self.login()
|
||||
response = self.client.post('/password_change/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'donuts',
|
||||
})
|
||||
self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch'])
|
||||
|
||||
def test_password_change_succeeds(self):
|
||||
self.login()
|
||||
self.client.post('/password_change/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
self.fail_login()
|
||||
self.login(password='password1')
|
||||
|
||||
def test_password_change_done_succeeds(self):
|
||||
self.login()
|
||||
response = self.client.post('/password_change/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_change/done/')
|
||||
|
||||
@override_settings(LOGIN_URL='/login/')
|
||||
def test_password_change_done_fails(self):
|
||||
response = self.client.get('/password_change/done/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/login/?next=/password_change/done/')
|
||||
|
||||
def test_password_change_redirect_default(self):
|
||||
self.login()
|
||||
response = self.client.post('/password_change/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_change/done/')
|
||||
|
||||
def test_password_change_redirect_custom(self):
|
||||
self.login()
|
||||
response = self.client.post('/password_change/custom/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/custom/')
|
||||
|
||||
def test_password_change_redirect_custom_named(self):
|
||||
self.login()
|
||||
response = self.client.post('/password_change/custom/named/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_reset/')
|
||||
|
||||
|
||||
@ignore_warnings(category=RemovedInDjango21Warning)
|
||||
class SessionAuthenticationTests(AuthViewsTestCase):
|
||||
def test_user_password_change_updates_session(self):
|
||||
"""
|
||||
#21649 - Ensure contrib.auth.views.password_change updates the user's
|
||||
session auth hash after a password change so the session isn't logged out.
|
||||
"""
|
||||
self.login()
|
||||
response = self.client.post('/password_change/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
# if the hash isn't updated, retrieving the redirection page will fail.
|
||||
self.assertRedirects(response, '/password_change/done/')
|
||||
|
||||
|
||||
@ignore_warnings(category=RemovedInDjango21Warning)
|
||||
class TestLogin(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.request = self.factory.get('/')
|
||||
|
||||
def test_template_name(self):
|
||||
response = login(self.request, 'template.html')
|
||||
self.assertEqual(response.template_name, ['template.html'])
|
||||
|
||||
def test_form_class(self):
|
||||
class NewForm(AuthenticationForm):
|
||||
def confirm_login_allowed(self, user):
|
||||
pass
|
||||
response = login(self.request, 'template.html', None, NewForm)
|
||||
self.assertEqual(response.context_data['form'].__class__.__name__, 'NewForm')
|
||||
|
||||
def test_extra_context(self):
|
||||
extra_context = {'fake_context': 'fake_context'}
|
||||
response = login(self.request, 'template.html', None, AuthenticationForm, extra_context)
|
||||
self.assertEqual(response.resolve_context('fake_context'), 'fake_context')
|
||||
|
||||
|
||||
@ignore_warnings(category=RemovedInDjango21Warning)
|
||||
class TestLogout(AuthViewsTestCase):
|
||||
def setUp(self):
|
||||
self.login()
|
||||
self.factory = RequestFactory()
|
||||
self.request = self.factory.post('/')
|
||||
self.request.session = self.client.session
|
||||
|
||||
def test_template_name(self):
|
||||
response = logout(self.request, None, 'template.html')
|
||||
self.assertEqual(response.template_name, ['template.html'])
|
||||
|
||||
def test_next_page(self):
|
||||
response = logout(self.request, 'www.next_page.com')
|
||||
self.assertEqual(response.url, 'www.next_page.com')
|
||||
|
||||
def test_extra_context(self):
|
||||
extra_context = {'fake_context': 'fake_context'}
|
||||
response = logout(self.request, None, 'template.html', None, extra_context)
|
||||
self.assertEqual(response.resolve_context('fake_context'), 'fake_context')
|
|
@ -1,38 +0,0 @@
|
|||
from django.conf.urls import url
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import views
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
# special urls for deprecated function-based views
|
||||
urlpatterns = [
|
||||
url(r'^login/$', views.login, name='login'),
|
||||
url(r'^logout/$', views.logout, name='logout'),
|
||||
url(r'^password_change/$', views.password_change, name='password_change'),
|
||||
url(r'^password_change/done/$', views.password_change_done, name='password_change_done'),
|
||||
url(r'^password_reset/$', views.password_reset, name='password_reset'),
|
||||
url(r'^password_reset/done/$', views.password_reset_done, name='password_reset_done'),
|
||||
url(r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
||||
views.password_reset_confirm, name='password_reset_confirm'),
|
||||
url(r'^reset/done/$', views.password_reset_complete, name='password_reset_complete'),
|
||||
|
||||
url(r'^password_reset_from_email/$', views.password_reset, {'from_email': 'staffmember@example.com'}),
|
||||
url(r'^password_reset_extra_email_context/$', views.password_reset,
|
||||
{'extra_email_context': {'greeting': 'Hello!'}}),
|
||||
url(r'^password_reset/custom_redirect/$', views.password_reset, {'post_reset_redirect': '/custom/'}),
|
||||
url(r'^password_reset/custom_redirect/named/$', views.password_reset, {'post_reset_redirect': 'password_reset'}),
|
||||
url(r'^password_reset/html_email_template/$', views.password_reset,
|
||||
{'html_email_template_name': 'registration/html_password_reset_email.html'}),
|
||||
url(r'^reset/custom/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
||||
views.password_reset_confirm,
|
||||
{'post_reset_redirect': '/custom/'}),
|
||||
url(r'^reset/custom/named/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
||||
views.password_reset_confirm,
|
||||
{'post_reset_redirect': 'password_reset'}),
|
||||
url(r'^password_change/custom/$', views.password_change, {'post_change_redirect': '/custom/'}),
|
||||
url(r'^password_change/custom/named/$', views.password_change, {'post_change_redirect': 'password_reset'}),
|
||||
url(r'^login_required/$', login_required(views.password_reset)),
|
||||
url(r'^login_required_login_url/$', login_required(views.password_reset, login_url='/somewhere/')),
|
||||
|
||||
# This line is only required to render the password reset with is_admin=True
|
||||
url(r'^admin/', admin.site.urls),
|
||||
]
|
Loading…
Reference in New Issue