Pārlūkot izejas kodu

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 gadi atpakaļ
vecāks
revīzija
12075fe50e

+ 1 - 0
api/desecapi/serializers.py

@@ -505,6 +505,7 @@ class UserSerializer(serializers.ModelSerializer):
         extra_kwargs = {
         extra_kwargs = {
             'password': {
             'password': {
                 'write_only': True,  # Do not expose password field
                 '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 unittest import mock
 from urllib.parse import urlparse
 from urllib.parse import urlparse
 
 
+from django.contrib.auth.hashers import is_password_usable
 from django.core import mail
 from django.core import mail
 from django.urls import resolve
 from django.urls import resolve
 from django.utils import timezone
 from django.utils import timezone
@@ -89,7 +90,6 @@ class UserManagementTestCase(DesecTestCase, PublicSuffixMockMixin):
 
 
     def register_user(self, email=None, password=None, **kwargs):
     def register_user(self, email=None, password=None, **kwargs):
         email = email if email is not None else self.random_username()
         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()
         captcha_id, captcha_solution = self.get_captcha()
         return email.strip(), password, self.client.register(email, password, captcha_id, captcha_solution, **kwargs)
         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)
         super().assertContains(response, text, count, status_code, msg_prefix, html)
 
 
     def assertPassword(self, email, password):
     def assertPassword(self, email, password):
+        if password is None:
+            self.assertFalse(is_password_usable(User.objects.get(email=email).password))
+            return
+
         password = password.strip()
         password = password.strip()
         self.assertTrue(User.objects.get(email=email).check_password(password),
         self.assertTrue(User.objects.get(email=email).check_password(password),
                         'Expected user password to be "%s" (potentially trimmed), but check failed.' % 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)
         self.assertResponse(self.client.get(reverse('v1:root')), status.HTTP_200_OK)
 
 
     def test_registration(self):
     def test_registration(self):
-        self._test_registration()
+        self._test_registration(password=self.random_password())
 
 
     def test_registration_trim_email(self):
     def test_registration_trim_email(self):
         user_email = ' {} '.format(self.random_username())
         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())
         self.assertEqual(email, user_email.strip())
 
 
     def test_registration_with_domain(self):
     def test_registration_with_domain(self):
@@ -497,6 +501,12 @@ class NoUserAccountTestCase(UserLifeCycleTestCase):
         self.assertNoEmailSent()
         self.assertNoEmailSent()
         self.assertUserDoesNotExist(email)
         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):
     def test_registration_spam_protection(self):
         email = self.random_username()
         email = self.random_username()
         self.assertRegistrationSuccessResponse(
         self.assertRegistrationSuccessResponse(
@@ -522,7 +532,7 @@ class OtherUserAccountTestCase(UserManagementTestCase):
 
 
     def setUp(self):
     def setUp(self):
         super().setUp()
         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):
     def test_reset_password_unknown_user(self):
         self.assertResetPasswordSuccessResponse(
         self.assertResetPasswordSuccessResponse(
@@ -540,7 +550,7 @@ class HasUserAccountTestCase(UserManagementTestCase):
 
 
     def setUp(self):
     def setUp(self):
         super().setUp()
         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()
         self.token = self._test_login()
 
 
     def _start_reset_password(self):
     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
   generate a long random string consisting of at least 16 alphanumeric
   characters, and use a password manager instead of attempting to remember it.
   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
 - Your email address is required for account recovery in case you forgot your
   password, for contacting support, etc. We also send out announcements for
   password, for contacting support, etc. We also send out announcements for
   technical changes occasionally. It is thus deSEC's policy to require users
   technical changes occasionally. It is thus deSEC's policy to require users