From 0e1e85470023941dbfbc8e17512d5a6ddc39d903 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Tue, 23 Feb 2021 15:28:56 +0100 Subject: [PATCH] csv_import: allow settings password hash (#50156) --- src/authentic2/csv_import.py | 21 ++++++++++++-- .../authentic2/manager/user_imports.html | 7 +++++ src/authentic2/manager/user_views.py | 5 ++++ tests/test_csv_import.py | 28 +++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/authentic2/csv_import.py b/src/authentic2/csv_import.py index 2c056ea6..d92eb1ef 100644 --- a/src/authentic2/csv_import.py +++ b/src/authentic2/csv_import.py @@ -23,7 +23,8 @@ from chardet.universaldetector import UniversalDetector import attr from django import forms -from django.core.exceptions import FieldDoesNotExist +from django.contrib.auth.hashers import identify_hasher +from django.core.exceptions import FieldDoesNotExist, ValidationError from django.core.validators import RegexValidator from django.db import IntegrityError, models from django.db.transaction import atomic @@ -249,9 +250,10 @@ SOURCE_ID = '_source_id' SOURCE_COLUMNS = set([SOURCE_NAME, SOURCE_ID]) ROLE_NAME = '_role_name' ROLE_SLUG = '_role_slug' +PASSWORD_HASH = 'password_hash' REGISTRATION = '@registration' REGISTRATION_RESET_EMAIL = 'send-email' -SPECIAL_COLUMNS = SOURCE_COLUMNS | {ROLE_NAME, ROLE_SLUG, REGISTRATION} +SPECIAL_COLUMNS = SOURCE_COLUMNS | {ROLE_NAME, ROLE_SLUG, REGISTRATION, PASSWORD_HASH} class ImportUserForm(BaseUserForm): @@ -268,11 +270,23 @@ class ImportUserForm(BaseUserForm): choices=choices, label=_('Registration option'), required=False) + locals()[PASSWORD_HASH] = forms.CharField( + label=_('Password hash'), + required=False) + def clean(self): super(BaseUserForm, self).clean() self._validate_unique = False + def clean_password_hash(self): + password_hash = self.cleaned_data['password_hash'] + try: + hasher = identify_hasher(password_hash) + except ValueError: + raise ValidationError(_('Invalid password format or unknown hashing algorithm.')) + return password_hash + class ImportUserFormWithExternalId(ImportUserForm): locals()[SOURCE_NAME] = forms.CharField( @@ -722,6 +736,9 @@ class UserCsvImporter(object): success &= self.add_role(cell, user, do_clear=True) elif cell.header.name == REGISTRATION and row.action == 'create': success &= self.registration_option(cell, user) + elif cell.header.name == PASSWORD_HASH: + user.password = cell.value + user.save() setattr(self, row.action + 'd', getattr(self, row.action + 'd') + 1) return success diff --git a/src/authentic2/manager/templates/authentic2/manager/user_imports.html b/src/authentic2/manager/templates/authentic2/manager/user_imports.html index ddf43d0b..c4825a5f 100644 --- a/src/authentic2/manager/templates/authentic2/manager/user_imports.html +++ b/src/authentic2/manager/templates/authentic2/manager/user_imports.html @@ -250,6 +250,13 @@ john.doe@example.com,John,Doe,Role2
email key,first_name,last_name,@registration
 john.doe@example.com,John,Doe,send-email
 jane.doe@example.com,Jane,Doe,
+
+ +

{% blocktrans trimmed %}Importing email, first and last name of users + and setting a password using hash in standard Django format.{% endblocktrans %}

+
+
email key,first_name,last_name,password_hash
+john.doe@example.com,John,Doe,pbkdf2_sha256$36000$oTHdVaoMjnCp$uTkpF7Ne6KV/L5gAerS7mngXM96DOEaLsLMZ251HJ/M=
 
diff --git a/src/authentic2/manager/user_views.py b/src/authentic2/manager/user_views.py index 37ede189..e140a3a1 100644 --- a/src/authentic2/manager/user_views.py +++ b/src/authentic2/manager/user_views.py @@ -777,6 +777,11 @@ class UserImportsView(MediaMixin, PermissionMixin, FormView): 'name': attribute.name, 'key': attribute.name == key, }) + help_columns.append({ + 'label': _('Password hash'), + 'name': 'password_hash', + 'key': False, + }) ctx['help_columns'] = help_columns example_data = u','.join(column['name'] + (' key' if column['key'] else '') for column in help_columns) + '\n' example_url = 'data:text/csv;base64,%s' % base64.b64encode(example_data.encode('utf-8')).decode('ascii') diff --git a/tests/test_csv_import.py b/tests/test_csv_import.py index 921e9747..a5663555 100644 --- a/tests/test_csv_import.py +++ b/tests/test_csv_import.py @@ -22,6 +22,7 @@ import pytest import io import codecs +from django.contrib.auth.hashers import make_password, check_password from django.core import mail from django_rbac.utils import get_role_model @@ -564,3 +565,30 @@ remote,8,john.doe,,, for row in importer.rows if any(cell.errors for cell in row.cells) } assert cell_errors == {} + + +def test_csv_password_hash(profile, user_csv_importer_factory): + content = '''email key,first_name,last_name,password_hash +tnoel@entrouvert.com,Thomas,Noël,%s''' + password_hash = make_password('hop') + + importer = user_csv_importer_factory(content % password_hash) + assert importer.run() + thomas = User.objects.get(email='tnoel@entrouvert.com') + assert check_password('hop', thomas.password) + + password_hash = make_password('test', hasher='sha256') + importer = user_csv_importer_factory(content % password_hash) + assert importer.run() + thomas.refresh_from_db() + assert check_password('test', thomas.password) + + importer = user_csv_importer_factory(content % 'wrong-format') + assert importer.run() + assert importer.has_errors + assert 'unknown hashing algorithm' in importer.rows[0].cells[-1].errors[0].description + + importer = user_csv_importer_factory(content) + assert importer.run() + assert importer.has_errors + assert 'unknown hashing algorithm' in importer.rows[0].cells[-1].errors[0].description -- 2.20.1