Repository: mathesar-foundation/mathesar

Issue:
Error when trying to reset password of other user
## Steps to reproduce

1. Set up another Mathesar user (other than the one you're logged in as).

1. Edit the another user and try to reset their password.

1. Observe this error message:

    ![image](https://github.com/mathesar-foundation/mathesar/assets/42411/8968438c-bb80-4596-a8d7-deecc19010cc)
    
    An API request is made to `/api/ui/v0/users/2/password_reset/` which returns a Django error

    > AttributeError at /api/ui/v0/users/2/password_reset/
    >
    > 'PasswordResetSerializer' object has no attribute 'validate_password'

    <details>
    <summary>Traceback</summary>

    ```
    Environment:


    Request Method: POST
    Request URL: http://localhost:8000/api/ui/v0/users/2/password_reset/

    Django Version: 4.2.10
    Python Version: 3.9.19
    Installed Applications:
    ['django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'whitenoise.runserver_nostatic',
    'django.contrib.staticfiles',
    'rest_framework',
    'django_filters',
    'django_property_filter',
    'drf_spectacular',
    'mathesar']
    Installed Middleware:
    ['django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'mathesar.middleware.CursorClosedHandlerMiddleware',
    'mathesar.middleware.PasswordChangeNeededMiddleware',
    'django_userforeignkey.middleware.UserForeignKeyMiddleware',
    'django_request_cache.middleware.RequestCacheMiddleware']



    Traceback (most recent call last):
      File "/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py", line 55, in inner
        response = get_response(request)
      File "/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py", line 197, in _get_response
        response = wrapped_callback(request, *callback_args, **callback_kwargs)
      File "/usr/local/lib/python3.9/site-packages/django/views/decorators/csrf.py", line 56, in wrapper_view
        return view_func(*args, **kwargs)
      File "/usr/local/lib/python3.9/site-packages/rest_framework/viewsets.py", line 125, in view
        return self.dispatch(request, *args, **kwargs)
      File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 509, in dispatch
        response = self.handle_exception(exc)
      File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 466, in handle_exception
        response = exception_handler(exc, context)
      File "/code/mathesar/exception_handlers.py", line 63, in mathesar_exception_handler
        raise exc
      File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 506, in dispatch
        response = handler(request, *args, **kwargs)
      File "/code/mathesar/api/ui/viewsets/users.py", line 29, in password_reset
        serializer.is_valid(raise_exception=True)
      File "/usr/local/lib/python3.9/site-packages/rest_framework/serializers.py", line 235, in is_valid
        raise ValidationError(self.errors)
      File "/code/mathesar/api/exceptions/mixins.py", line 98, in errors
        pretty_errors = self.build_pretty_errors(ugly_errors)
      File "/code/mathesar/api/exceptions/mixins.py", line 64, in build_pretty_errors
        pretty.extend(self.get_field_error_entries(errors[error_type], field))
      File "/usr/local/lib/python3.9/site-packages/rest_framework_friendly_errors/mixins.py", line 180, in get_field_error_entries
        return [self.get_field_error_entry(error, field) for error in errors]
      File "/usr/local/lib/python3.9/site-packages/rest_framework_friendly_errors/mixins.py", line 180, in <listcomp>
        return [self.get_field_error_entry(error, field) for error in errors]
      File "/usr/local/lib/python3.9/site-packages/rest_framework_friendly_errors/mixins.py", line 168, in get_field_error_entry
        validator = getattr(self, "validate_%s" % field.field_name)

    Exception Type: AttributeError at /api/ui/v0/users/2/password_reset/
    Exception Value: 'PasswordResetSerializer' object has no attribute 'validate_password'
    ```

    </details>

I can reproduce this on the latest develop branch as well as the most recent release (Mathesar 0.1.6).




Discussion:
Hi, can I work on this issue?
Sure, @camicacace you're welcome to submit a PR. Ask here or in Matrix if you have any questions.

Resolving patch:
diff --git a/mathesar/api/ui/serializers/users.py b/mathesar/api/ui/serializers/users.py
--- a/mathesar/api/ui/serializers/users.py
+++ b/mathesar/api/ui/serializers/users.py
@@ -1,4 +1,5 @@
 from django.contrib.auth.password_validation import validate_password
+from django.core.exceptions import ValidationError as DjangoValidationError
 from rest_access_policy import FieldAccessMixin, PermittedPkRelatedField
 from rest_framework import serializers
 
@@ -71,7 +72,7 @@ def create(self, validated_data):
 
 
 class ChangePasswordSerializer(MathesarErrorMessageMixin, serializers.Serializer):
-    password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
+    password = serializers.CharField(write_only=True, required=True)
     old_password = serializers.CharField(write_only=True, required=True)
 
     def validate_old_password(self, value):
@@ -80,6 +81,13 @@ def validate_old_password(self, value):
             return value
         raise IncorrectOldPassword(field='old_password')
 
+    def validate_password(self, value):
+        try:
+            validate_password(value)
+        except DjangoValidationError as e:
+            raise e
+        return value
+
     def update(self, instance, validated_data):
         instance.set_password(validated_data['password'])
         instance.save()
@@ -87,7 +95,7 @@ def update(self, instance, validated_data):
 
 
 class PasswordResetSerializer(MathesarErrorMessageMixin, serializers.Serializer):
-    password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
+    password = serializers.CharField(write_only=True, required=True)
 
 
 class DatabaseRoleSerializer(MathesarErrorMessageMixin, serializers.ModelSerializer):


Test patch:
diff --git a/mathesar/tests/api/test_user_api.py b/mathesar/tests/api/test_user_api.py
--- a/mathesar/tests/api/test_user_api.py
+++ b/mathesar/tests/api/test_user_api.py
@@ -61,10 +61,11 @@ def test_user_password_reset_non_superuser(client_bob, user_bob):
 
 
 def test_user_password_change(client_bob, user_bob):
-    new_password = 'new_password'
+    new_password = 'NewPass0!'
+    old_password = 'password'
     data = {
         'password': new_password,
-        'old_password': 'password'
+        'old_password': old_password
     }
     response = client_bob.post('/api/ui/v0/users/password_change/', data=data)
     assert response.status_code == 200
@@ -72,6 +73,19 @@ def test_user_password_change(client_bob, user_bob):
     assert user_bob.check_password(new_password) is True
 
 
+def test_user_password_change_invalid(client_bob, user_bob):
+    new_password = 'new_pwd'
+    old_password = 'password'
+    data = {
+        'password': new_password,
+        'old_password': old_password
+    }
+    response = client_bob.post('/api/ui/v0/users/password_change/', data=data)
+    assert response.status_code == 400
+    user_bob.refresh_from_db()
+    assert user_bob.check_password(new_password) is False
+
+
 def test_diff_user_detail_as_non_superuser(client_bob, admin_user):
     response = client_bob.get(f'/api/ui/v0/users/{admin_user.id}/')
     response_data = response.json()