Skip to content

Commit f352046

Browse files
Merge pull request #55 from stan-sack/dont_require_usable_pword
Add setting to not require users to have usable password
2 parents 85d30b7 + bebefab commit f352046

File tree

4 files changed

+78
-4
lines changed

4 files changed

+78
-4
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ The following settings can be set in Djangos ``settings.py`` file:
6464
* `DJANGO_REST_MULTITOKENAUTH_RESET_TOKEN_EXPIRY_TIME` - time in hours about how long the token is active (Default: 24)
6565

6666
**Please note**: expired tokens are automatically cleared based on this setting in every call of ``ResetPasswordRequestToken.post``.
67+
68+
* `DJANGO_REST_MULTITOKENAUTH_REQUIRE_USABLE_PASSWORD` - allows password reset for a user that does not
69+
[have a usable password](https://docs.djangoproject.com/en/2.2/ref/contrib/auth/#django.contrib.auth.models.User.has_usable_password) (Default: True)
6770

6871
### Signals
6972

django_rest_passwordreset/models.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from django.db import models
33
from django.utils.encoding import python_2_unicode_compatible
44
from django.utils.translation import ugettext_lazy as _
5+
from django.contrib.auth import get_user_model
56

67
from django_rest_passwordreset.tokens import get_token_generator
78

@@ -102,3 +103,19 @@ def clear_expired(expiry_time):
102103
:param expiry_time: Token expiration time
103104
"""
104105
ResetPasswordToken.objects.filter(created_at__lte=expiry_time).delete()
106+
107+
def eligible_for_reset(self):
108+
if not self.is_active:
109+
# if the user is active we dont bother checking
110+
return False
111+
112+
if getattr(settings, 'DJANGO_REST_MULTITOKENAUTH_REQUIRE_USABLE_PASSWORD', True):
113+
# if we require a usable password then return the result of has_usable_password()
114+
return self.has_usable_password()
115+
else:
116+
# otherwise return True because we dont care about the result of has_usable_password()
117+
return True
118+
119+
# add eligible_for_reset to the user class
120+
UserModel = get_user_model()
121+
UserModel.add_to_class("eligible_for_reset", eligible_for_reset)

django_rest_passwordreset/views.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ def post(self, request, *args, **kwargs):
5555
reset_password_token.delete()
5656
return Response({'status': 'expired'}, status=status.HTTP_404_NOT_FOUND)
5757

58-
# change users password
59-
if reset_password_token.user.has_usable_password():
58+
# change users password (if we got to this code it means that the user is_active)
59+
if reset_password_token.user.eligible_for_reset():
6060
pre_password_reset.send(sender=self.__class__, user=reset_password_token.user)
6161
try:
6262
# validate the password against existing validators
@@ -114,7 +114,7 @@ def post(self, request, *args, **kwargs):
114114
# also check whether the password can be changed (is useable), as there could be users that are not allowed
115115
# to change their password (e.g., LDAP user)
116116
for user in users:
117-
if user.is_active and user.has_usable_password():
117+
if user.eligible_for_reset():
118118
active_user_found = True
119119

120120
# No active user found, raise a validation error
@@ -127,7 +127,7 @@ def post(self, request, *args, **kwargs):
127127
# last but not least: iterate over all users that are active and can change their password
128128
# and create a Reset Password Token and send a signal with the created token
129129
for user in users:
130-
if user.is_active and user.has_usable_password():
130+
if user.eligible_for_reset():
131131
# define the token as none for now
132132
token = None
133133

tests/test/test_auth_test_case.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def setUp(self):
1717
self.user1 = User.objects.create_user("user1", "user1@mail.com", "secret1")
1818
self.user2 = User.objects.create_user("user2", "user2@mail.com", "secret2")
1919
self.user3 = User.objects.create_user("user3@mail.com", "not-that-mail@mail.com", "secret3")
20+
self.user4 = User.objects.create_user("user4", "user4@mail.com")
2021

2122
def test_try_reset_password_email_does_not_exist(self):
2223
""" Tests requesting a token for an email that does not exist """
@@ -225,3 +226,56 @@ def test_signals(self,
225226
# now the other two signals should have been called
226227
self.assertTrue(mock_post_password_reset.called)
227228
self.assertTrue(mock_pre_password_reset.called)
229+
230+
def test_user_without_password(self):
231+
""" Tests requesting a token for an email without a password doesn't work"""
232+
response = self.rest_do_request_reset_token(email="user4@mail.com")
233+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
234+
decoded_response = json.loads(response.content.decode())
235+
# response should have "email" in it
236+
self.assertTrue("email" in decoded_response)
237+
238+
@override_settings(DJANGO_REST_MULTITOKENAUTH_REQUIRE_USABLE_PASSWORD=False)
239+
@patch('django_rest_passwordreset.signals.reset_password_token_created.send')
240+
def test_user_without_password_where_not_required(self, mock_reset_password_token_created):
241+
""" Tests requesting a token for an email without a password works when not required"""
242+
response = self.rest_do_request_reset_token(email="user4@mail.com")
243+
decoded_response = json.loads(response.content.decode())
244+
self.assertEqual(response.status_code, status.HTTP_200_OK)
245+
# check that the signal was sent once
246+
self.assertTrue(mock_reset_password_token_created.called)
247+
self.assertEqual(mock_reset_password_token_created.call_count, 1)
248+
last_reset_password_token = mock_reset_password_token_created.call_args[1]['reset_password_token']
249+
self.assertNotEqual(last_reset_password_token.key, "")
250+
251+
# there should be one token
252+
self.assertEqual(ResetPasswordToken.objects.all().count(), 1)
253+
254+
# if the same user tries to reset again, the user will get the same token again
255+
response = self.rest_do_request_reset_token(email="user4@mail.com")
256+
self.assertEqual(response.status_code, status.HTTP_200_OK)
257+
self.assertEqual(mock_reset_password_token_created.call_count, 2)
258+
last_reset_password_token = mock_reset_password_token_created.call_args[1]['reset_password_token']
259+
self.assertNotEqual(last_reset_password_token.key, "")
260+
261+
# there should be one token
262+
self.assertEqual(ResetPasswordToken.objects.all().count(), 1)
263+
# and it should be assigned to user1
264+
self.assertEqual(
265+
ResetPasswordToken.objects.filter(key=last_reset_password_token.key).first().user.username,
266+
"user4"
267+
)
268+
269+
# try to reset the password
270+
response = self.rest_do_reset_password_with_token(last_reset_password_token.key, "new_secret")
271+
self.assertEqual(response.status_code, status.HTTP_200_OK)
272+
273+
# there should be zero tokens
274+
self.assertEqual(ResetPasswordToken.objects.all().count(), 0)
275+
276+
# try to login with the new username/Password (should work)
277+
self.assertTrue(
278+
self.django_check_login("user4", "new_secret"),
279+
msg="User 4 should be able to login with the modified credentials"
280+
)
281+

0 commit comments

Comments
 (0)