瀏覽代碼

feat(api): allow account registration without a password

Simply allows passing JSON null as the user password. Django makes
sure that such users end up with an unusable password that cannot
be used for login, see
https://docs.djangoproject.com/en/2.2/topics/auth/passwords/#django.contrib.auth.hashers.make_password
Peter Thomassen 5 年之前
父節點
當前提交
12075fe50e
共有 3 個文件被更改,包括 21 次插入5 次删除
  1. 1 0
      api/desecapi/serializers.py
  2. 15 5
      api/desecapi/tests/test_user_management.py
  3. 5 0
      docs/authentication.rst

+ 1 - 0
api/desecapi/serializers.py

@@ -505,6 +505,7 @@ class UserSerializer(serializers.ModelSerializer):
         extra_kwargs = {
             'password': {
                 'write_only': True,  # Do not expose password field
+                'allow_null': True,
             }
         }
 

+ 15 - 5
api/desecapi/tests/test_user_management.py

@@ -17,6 +17,7 @@ import re
 from unittest import mock
 from urllib.parse import urlparse
 
+from django.contrib.auth.hashers import is_password_usable
 from django.core import mail
 from django.urls import resolve
 from django.utils import timezone
@@ -89,7 +90,6 @@ class UserManagementTestCase(DesecTestCase, PublicSuffixMockMixin):
 
     def register_user(self, email=None, password=None, **kwargs):
         email = email if email is not None else self.random_username()
-        password = password if password is not None else self.random_password()
         captcha_id, captcha_solution = self.get_captcha()
         return email.strip(), password, self.client.register(email, password, captcha_id, captcha_solution, **kwargs)
 
@@ -112,6 +112,10 @@ class UserManagementTestCase(DesecTestCase, PublicSuffixMockMixin):
         super().assertContains(response, text, count, status_code, msg_prefix, html)
 
     def assertPassword(self, email, password):
+        if password is None:
+            self.assertFalse(is_password_usable(User.objects.get(email=email).password))
+            return
+
         password = password.strip()
         self.assertTrue(User.objects.get(email=email).check_password(password),
                         'Expected user password to be "%s" (potentially trimmed), but check failed.' % password)
@@ -459,11 +463,11 @@ class NoUserAccountTestCase(UserLifeCycleTestCase):
         self.assertResponse(self.client.get(reverse('v1:root')), status.HTTP_200_OK)
 
     def test_registration(self):
-        self._test_registration()
+        self._test_registration(password=self.random_password())
 
     def test_registration_trim_email(self):
         user_email = ' {} '.format(self.random_username())
-        email, new_password = self._test_registration(user_email)
+        email, _ = self._test_registration(user_email)
         self.assertEqual(email, user_email.strip())
 
     def test_registration_with_domain(self):
@@ -497,6 +501,12 @@ class NoUserAccountTestCase(UserLifeCycleTestCase):
         self.assertNoEmailSent()
         self.assertUserDoesNotExist(email)
 
+    def test_no_login_with_unusable_password(self):
+        email, password = self._test_registration(password=None)
+        response = self.client.login_user(email, password)
+        self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
+        self.assertEqual(response.data['password'][0], 'This field may not be null.')
+
     def test_registration_spam_protection(self):
         email = self.random_username()
         self.assertRegistrationSuccessResponse(
@@ -522,7 +532,7 @@ class OtherUserAccountTestCase(UserManagementTestCase):
 
     def setUp(self):
         super().setUp()
-        self.other_email, self.other_password = self._test_registration()
+        self.other_email, self.other_password = self._test_registration(password=self.random_password())
 
     def test_reset_password_unknown_user(self):
         self.assertResetPasswordSuccessResponse(
@@ -540,7 +550,7 @@ class HasUserAccountTestCase(UserManagementTestCase):
 
     def setUp(self):
         super().setUp()
-        self.email, self.password = self._test_registration()
+        self.email, self.password = self._test_registration(password=self.random_password())
         self.token = self._test_login()
 
     def _start_reset_password(self):

+ 5 - 0
docs/authentication.rst

@@ -58,6 +58,11 @@ Please consider the following when registering an account:
   generate a long random string consisting of at least 16 alphanumeric
   characters, and use a password manager instead of attempting to remember it.
 
+- If you do not require a password at the moment, you can pass ``null`` (the
+  JSON value, not the string!). If you create an account this way, it will not
+  be possible to `Log In`_. You can set a password later using the `Password
+  Reset`_ procedure.
+
 - Your email address is required for account recovery in case you forgot your
   password, for contacting support, etc. We also send out announcements for
   technical changes occasionally. It is thus deSEC's policy to require users