0001-api-add-a-hashed_password-attribute-for-user-api-354.patch
src/authentic2/api_views.py | ||
---|---|---|
20 | 20 |
import django |
21 | 21 |
from django.db import models |
22 | 22 |
from django.contrib.auth import get_user_model |
23 |
from django.contrib.auth.hashers import identify_hasher |
|
23 | 24 |
from django.core.exceptions import MultipleObjectsReturned |
24 | 25 |
from django.utils.translation import ugettext as _ |
25 | 26 |
from django.utils.encoding import force_text |
... | ... | |
340 | 341 |
send_registration_email_next_url = serializers.URLField(write_only=True, required=False) |
341 | 342 |
password = serializers.CharField(max_length=128, required=False) |
342 | 343 |
force_password_reset = serializers.BooleanField(write_only=True, required=False, default=False) |
344 |
hashed_password = serializers.CharField(max_length=128, required=False) |
|
343 | 345 | |
344 | 346 |
def __init__(self, *args, **kwargs): |
345 | 347 |
super(BaseUserSerializer, self).__init__(*args, **kwargs) |
... | ... | |
392 | 394 |
attributes = validated_data.pop('attributes', {}) |
393 | 395 |
is_verified = validated_data.pop('is_verified', {}) |
394 | 396 |
password = validated_data.pop('password', None) |
397 |
hashed_password = validated_data.pop('hashed_password', None) |
|
395 | 398 |
self.check_perm('custom_user.add_user', validated_data.get('ou')) |
396 | 399 |
instance = super(BaseUserSerializer, self).create(validated_data) |
397 | 400 |
# prevent update on a get_or_create |
... | ... | |
413 | 416 |
instance.save() |
414 | 417 |
if force_password_reset: |
415 | 418 |
PasswordReset.objects.get_or_create(user=instance) |
419 |
if hashed_password is not None: |
|
420 |
instance.password = hashed_password |
|
421 |
instance.save() |
|
416 | 422 |
if send_registration_email and validated_data.get('email'): |
417 | 423 |
try: |
418 | 424 |
utils.send_password_reset_mail( |
... | ... | |
437 | 443 |
attributes = validated_data.pop('attributes', {}) |
438 | 444 |
is_verified = validated_data.pop('is_verified', {}) |
439 | 445 |
password = validated_data.pop('password', None) |
446 |
hashed_password = validated_data.pop('hashed_password', None) |
|
440 | 447 |
# Double check: to move an user from one ou into another you must be administrator of both |
441 | 448 |
self.check_perm('custom_user.change_user', instance.ou) |
442 | 449 |
if 'ou' in validated_data: |
... | ... | |
465 | 472 |
instance.save() |
466 | 473 |
if force_password_reset: |
467 | 474 |
PasswordReset.objects.get_or_create(user=instance) |
475 |
if hashed_password is not None: |
|
476 |
instance.password = hashed_password |
|
477 |
instance.save() |
|
468 | 478 |
return instance |
469 | 479 | |
470 | 480 |
def validate(self, data): |
... | ... | |
491 | 501 |
if ou and ou.email_is_unique and qs.filter(ou=ou, email=data['email']).exists(): |
492 | 502 |
already_used = True |
493 | 503 | |
504 |
errors = {} |
|
494 | 505 |
if already_used: |
495 |
raise serializers.ValidationError({ |
|
496 |
'email': 'email already used', |
|
497 |
}) |
|
506 |
errors['email'] = 'email already used' |
|
507 |
if data.get('password') and data.get('hashed_password'): |
|
508 |
errors['password'] = 'conflict with provided hashed_password' |
|
509 |
if data.get('hashed_password'): |
|
510 |
try: |
|
511 |
hasher = identify_hasher(data.get('hashed_password')) |
|
512 |
except ValueError: |
|
513 |
errors['hashed_password'] = "unknown hash format" |
|
514 |
else: |
|
515 |
try: |
|
516 |
hasher.safe_summary(data.get('hashed_password')) |
|
517 |
except Exception: |
|
518 |
errors['hashed_password'] = "hash format error" |
|
519 |
if errors: |
|
520 |
raise serializers.ValidationError(errors) |
|
498 | 521 |
return data |
499 | 522 | |
500 | 523 |
class Meta: |
tests/test_api.py | ||
---|---|---|
22 | 22 |
import uuid |
23 | 23 | |
24 | 24 | |
25 |
import django |
|
25 | 26 |
from django.contrib.auth.hashers import check_password |
26 | 27 |
from django.contrib.auth import get_user_model |
27 | 28 |
from django.contrib.contenttypes.models import ContentType |
... | ... | |
1268 | 1269 |
assert uuid == resp.json['uuid'] |
1269 | 1270 |
assert Role.objects.get(uuid=uuid).name == 'Role 2' |
1270 | 1271 |
assert Role.objects.get(uuid=uuid).slug == 'role-1' |
1272 | ||
1273 | ||
1274 |
def test_api_users_hashed_password(settings, app, admin): |
|
1275 |
app.authorization = ('Basic', (admin.username, admin.username)) |
|
1276 |
payload = { |
|
1277 |
'email': 'john.doe@example.net', |
|
1278 |
'first_name': 'John', |
|
1279 |
'last_name': 'Doe', |
|
1280 |
'hashed_password': 'pbkdf2_sha256$36000$re9zaUj1ize0$bX1cqB91ni4aMOtRh8//TLaJkX+xnD2w84MCQx9AJcE=', |
|
1281 |
} |
|
1282 |
resp = app.post_json('/api/users/?get_or_create=email', params=payload, status=201) |
|
1283 |
id = resp.json['id'] |
|
1284 |
assert User.objects.get(id=id).first_name == 'John' |
|
1285 |
assert User.objects.get(id=id).last_name == 'Doe' |
|
1286 |
assert User.objects.get(id=id).check_password('admin') |
|
1287 |
password = User.objects.get(id=id).password |
|
1288 | ||
1289 |
payload['hashed_password'] = 'sha-oldap$$e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4' |
|
1290 |
resp = app.post_json('/api/users/?update_or_create=email', params=payload, status=200) |
|
1291 |
assert User.objects.get(id=id).password != password |
|
1292 |
assert User.objects.get(id=id).check_password('secret') |
|
1293 | ||
1294 |
payload['password'] = 'secret' |
|
1295 |
resp = app.post_json('/api/users/?update_or_create=email', params=payload, status=400) |
|
1296 |
assert resp.json['result'] == 0 |
|
1297 |
assert resp.json['errors']['password'] == ['conflict with provided hashed_password'] |
|
1298 | ||
1299 |
del payload['password'] |
|
1300 |
payload['hashed_password'] = 'unknown_format' |
|
1301 |
resp = app.post_json('/api/users/?update_or_create=email', params=payload, status=400) |
|
1302 |
assert resp.json['result'] == 0 |
|
1303 |
assert resp.json['errors']['hashed_password'] == ['unknown hash format'] |
|
1304 | ||
1305 |
payload['hashed_password'] = 'argon2$wrong_format' |
|
1306 |
resp = app.post_json('/api/users/?update_or_create=email', params=payload, status=400) |
|
1307 |
assert resp.json['result'] == 0 |
|
1308 |
if django.VERSION >= (1, 10, 0): |
|
1309 |
assert resp.json['errors']['hashed_password'] == ['hash format error'] |
|
1310 |
else: # before Django 1.10 Argon2 is not recognized |
|
1311 |
assert resp.json['errors']['hashed_password'] == ['unknown hash format'] |
|
1271 |
- |