From 7d877eec5f82d565dba27806dd3a77edb0a26a9a Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Wed, 4 Sep 2019 18:02:32 +0200
Subject: [PATCH] api: add a hashed_password attribute for user api (#35482)

---
 src/authentic2/api_views.py | 18 +++++++++++++++---
 tests/test_api.py           | 30 ++++++++++++++++++++++++++++++
 2 files changed, 45 insertions(+), 3 deletions(-)

diff --git a/src/authentic2/api_views.py b/src/authentic2/api_views.py
index 38ce142f..ba0386bb 100644
--- a/src/authentic2/api_views.py
+++ b/src/authentic2/api_views.py
@@ -340,6 +340,7 @@ class BaseUserSerializer(serializers.ModelSerializer):
     send_registration_email_next_url = serializers.URLField(write_only=True, required=False)
     password = serializers.CharField(max_length=128, required=False)
     force_password_reset = serializers.BooleanField(write_only=True, required=False, default=False)
+    hashed_password = serializers.CharField(max_length=128, required=False)
 
     def __init__(self, *args, **kwargs):
         super(BaseUserSerializer, self).__init__(*args, **kwargs)
@@ -392,6 +393,7 @@ class BaseUserSerializer(serializers.ModelSerializer):
         attributes = validated_data.pop('attributes', {})
         is_verified = validated_data.pop('is_verified', {})
         password = validated_data.pop('password', None)
+        hashed_password = validated_data.pop('hashed_password', None)
         self.check_perm('custom_user.add_user', validated_data.get('ou'))
         instance = super(BaseUserSerializer, self).create(validated_data)
         # prevent update on a get_or_create
@@ -413,6 +415,9 @@ class BaseUserSerializer(serializers.ModelSerializer):
         instance.save()
         if force_password_reset:
             PasswordReset.objects.get_or_create(user=instance)
+        if hashed_password is not None:
+            instance.password = hashed_password
+            instance.save()
         if send_registration_email and validated_data.get('email'):
             try:
                 utils.send_password_reset_mail(
@@ -437,6 +442,7 @@ class BaseUserSerializer(serializers.ModelSerializer):
         attributes = validated_data.pop('attributes', {})
         is_verified = validated_data.pop('is_verified', {})
         password = validated_data.pop('password', None)
+        hashed_password = validated_data.pop('hashed_password', None)
         # Double check: to move an user from one ou into another you must be administrator of both
         self.check_perm('custom_user.change_user', instance.ou)
         if 'ou' in validated_data:
@@ -465,6 +471,9 @@ class BaseUserSerializer(serializers.ModelSerializer):
             instance.save()
         if force_password_reset:
             PasswordReset.objects.get_or_create(user=instance)
+        if hashed_password is not None:
+            instance.password = hashed_password
+            instance.save()
         return instance
 
     def validate(self, data):
@@ -491,10 +500,13 @@ class BaseUserSerializer(serializers.ModelSerializer):
             if ou and ou.email_is_unique and qs.filter(ou=ou, email=data['email']).exists():
                 already_used = True
 
+        errors = {}
         if already_used:
-            raise serializers.ValidationError({
-                'email': 'email already used',
-            })
+            errors['email'] = 'email already used'
+        if data.get('password') and data.get('hashed_password'):
+            errors['hashed_password'] = 'conflict with provided password'
+        if errors:
+            raise serializers.ValidationError(errors)
         return data
 
     class Meta:
diff --git a/tests/test_api.py b/tests/test_api.py
index 7145c44c..b3ce450d 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1187,6 +1187,19 @@ def test_api_users_get_or_create(settings, app, admin):
     assert User.objects.get(id=id).last_name == 'Doe'
     assert User.objects.get(id=id).password != password
     assert User.objects.get(id=id).check_password('secret')
+    password = User.objects.get(id=id).password
+
+    payload['hashed_password'] = 'pbkdf2_sha256$36000$re9zaUj1ize0$bX1cqB91ni4aMOtRh8//TLaJkX+xnD2w84MCQx9AJcE='
+    resp = app.post_json('/api/users/?update_or_create=email', params=payload, status=400)
+    assert resp.json['result'] == 0
+    assert resp.json['errors']['hashed_password'] == ['conflict with provided password']
+
+    del payload['password']
+    resp = app.post_json('/api/users/?update_or_create=email', params=payload, status=200)
+    assert User.objects.get(id=id).first_name == 'Jane'
+    assert User.objects.get(id=id).last_name == 'Doe'
+    assert User.objects.get(id=id).password != password
+    assert User.objects.get(id=id).check_password('admin')
 
 
 def test_api_users_get_or_create_email_is_unique(settings, app, admin):
@@ -1244,6 +1257,23 @@ def test_api_users_get_or_create_multi_key(settings, app, admin):
     assert User.objects.get(id=id).email == 'john.doe@example2.net'
     assert User.objects.get(id=id).password != password
     assert User.objects.get(id=id).check_password('secret')
+    password = User.objects.get(id=id).password
+
+    payload['hashed_password'] = 'pbkdf2_sha256$36000$re9zaUj1ize0$bX1cqB91ni4aMOtRh8//TLaJkX+xnD2w84MCQx9AJcE='
+    resp = app.post_json(
+        '/api/users/?update_or_create=first_name&update_or_create=last_name',
+        params=payload, status=400)
+    assert resp.json['result'] == 0
+    assert resp.json['errors']['hashed_password'] == ['conflict with provided password']
+
+    del payload['password']
+    resp = app.post_json(
+        '/api/users/?update_or_create=first_name&update_or_create=last_name',
+        params=payload, status=200)
+    assert User.objects.get(id=id).first_name == 'John'
+    assert User.objects.get(id=id).last_name == 'Doe'
+    assert User.objects.get(id=id).password != password
+    assert User.objects.get(id=id).check_password('admin')
 
 
 def test_api_roles_get_or_create(settings, ou1, app, admin):
-- 
2.23.0.rc1

