this.newPassword && this.currentPassword !== this.newPassword
const profile = Object.assign({}, this.form.value)
delete profile.totp_code
+ this.error = null
this.networkActive = true
this.profileService
.update(profile)
},
error: (error) => {
this.toastService.showError($localize`Error saving profile`, error)
+ this.error = error?.error
this.networkActive = false
},
})
user1 = {
"username": "testuser",
- "password": "test",
+ "password": "areallysupersecretpassword235",
"first_name": "Test",
"last_name": "User",
}
f"{self.ENDPOINT}{user1.pk}/",
data={
"first_name": "Updated Name 2",
- "password": "123xyz",
+ "password": "newreallystrongpassword456",
},
)
self.assertEqual(user.first_name, user_data["first_name"])
self.assertEqual(user.last_name, user_data["last_name"])
+ def test_update_profile_invalid_password_returns_field_error(self):
+ """
+ GIVEN:
+ - Configured user
+ WHEN:
+ - API call is made to update profile with weak password
+ THEN:
+ - Profile update fails with password field error
+ """
+
+ user_data = {
+ "email": "new@email.com",
+ "password": "short", # shorter than default validator threshold
+ "first_name": "new first name",
+ "last_name": "new last name",
+ }
+
+ response = self.client.patch(self.ENDPOINT, user_data)
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertIn("password", response.data)
+ self.assertIsInstance(response.data["password"], list)
+ self.assertTrue(
+ any(
+ "too short" in message.lower() for message in response.data["password"]
+ ),
+ )
+
+ def test_update_profile_placeholder_password_skips_validation(self):
+ """
+ GIVEN:
+ - Configured user with existing password
+ WHEN:
+ - API call is made with the obfuscated placeholder password value
+ THEN:
+ - Profile is updated without changing the password or running validators
+ """
+
+ original_password = "orig-pass-12345"
+ self.user.set_password(original_password)
+ self.user.save()
+
+ user_data = {
+ "email": "new@email.com",
+ "password": "*" * 12, # matches obfuscated value from serializer
+ "first_name": "new first name",
+ "last_name": "new last name",
+ }
+
+ response = self.client.patch(self.ENDPOINT, user_data)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ user = User.objects.get(username=self.user.username)
+ self.assertTrue(user.check_password(original_password))
+ self.assertEqual(user.email, user_data["email"])
+ self.assertEqual(user.first_name, user_data["first_name"])
+ self.assertEqual(user.last_name, user_data["last_name"])
+
def test_update_auth_token(self):
"""
GIVEN:
from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
+from django.contrib.auth.password_validation import validate_password
from rest_framework import serializers
from rest_framework.authtoken.serializers import AuthTokenSerializer
logger = logging.getLogger("paperless.settings")
+class PasswordValidationMixin:
+ def _has_real_password(self, value: str | None) -> bool:
+ return bool(value) and value.replace("*", "") != ""
+
+ def validate_password(self, value: str) -> str:
+ if not self._has_real_password(value):
+ return value
+
+ request = self.context.get("request") if hasattr(self, "context") else None
+ user = self.instance or (
+ request.user if request and hasattr(request, "user") else None
+ )
+ validate_password(value, user) # raise ValidationError if invalid
+
+ return value
+
+
class PaperlessAuthTokenSerializer(AuthTokenSerializer):
code = serializers.CharField(
label="MFA Code",
return attrs
-class UserSerializer(serializers.ModelSerializer):
+class UserSerializer(PasswordValidationMixin, serializers.ModelSerializer):
password = ObfuscatedPasswordField(required=False)
user_permissions = serializers.SlugRelatedField(
many=True,
return obj.get_group_permissions()
def update(self, instance, validated_data):
- if "password" in validated_data:
- if len(validated_data.get("password").replace("*", "")) > 0:
- instance.set_password(validated_data.get("password"))
- instance.save()
- validated_data.pop("password")
+ password = validated_data.pop("password", None)
+ if self._has_real_password(password):
+ instance.set_password(password)
+ instance.save()
+
super().update(instance, validated_data)
return instance
user_permissions = None
if "user_permissions" in validated_data:
user_permissions = validated_data.pop("user_permissions")
- password = None
- if (
- "password" in validated_data
- and len(validated_data.get("password").replace("*", "")) > 0
- ):
- password = validated_data.pop("password")
+ password = validated_data.pop("password", None)
user = User.objects.create(**validated_data)
# set groups
if groups:
if user_permissions:
user.user_permissions.set(user_permissions)
# set password
- if password:
+ if self._has_real_password(password):
user.set_password(password)
user.save()
return user
return "Unknown App"
-class ProfileSerializer(serializers.ModelSerializer):
+class ProfileSerializer(PasswordValidationMixin, serializers.ModelSerializer):
email = serializers.EmailField(allow_blank=True, required=False)
password = ObfuscatedPasswordField(required=False, allow_null=False)
auth_token = serializers.SlugRelatedField(read_only=True, slug_field="key")
serializer.is_valid(raise_exception=True)
user = self.request.user if hasattr(self.request, "user") else None
- if len(serializer.validated_data.get("password").replace("*", "")) > 0:
- user.set_password(serializer.validated_data.get("password"))
+ password = serializer.validated_data.pop("password", None)
+ if password and password.replace("*", ""):
+ user.set_password(password)
user.save()
- serializer.validated_data.pop("password")
for key, value in serializer.validated_data.items():
setattr(user, key, value)