diff options
-rw-r--r-- | CHANGES.md | 1 | ||||
-rw-r--r-- | example_project/settings.py | 22 | ||||
-rw-r--r-- | ishtar_common/tests.py | 15 | ||||
-rw-r--r-- | ishtar_common/utils.py | 44 |
4 files changed, 80 insertions, 2 deletions
diff --git a/CHANGES.md b/CHANGES.md index 8a21b2e98..4301e95c4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ Ishtar changelog - Load task refactoring - manage external_id regen with tasks - Containers: manage history - Manage expiration of passwords +- Manage strong password policy (ISHTAR_STRONG_PASSWORD_POLICY) with "Each character type" validator ### Bug fixes ### - Json fields: fix bad save of multi values diff --git a/example_project/settings.py b/example_project/settings.py index 934582b9b..705e92154 100644 --- a/example_project/settings.py +++ b/example_project/settings.py @@ -295,6 +295,7 @@ ISHTAR_PASSWORD_EXPIRATION_DAYS = None ISHTAR_SEARCH_LANGUAGE = "french" ISHTAR_SECURE = True ISHTAR_SECURE_OPTIONS = False +ISHTAR_STRONG_PASSWORD_POLICY = False ISHTAR_DPTS = [] MAX_ATTEMPTS = 1 # django background tasks @@ -314,6 +315,8 @@ DISTRIBUTION = "source" LIB_BASE_PATH = ROOT_PATH + "../" FIXTURE_AUTH_PATH = ROOT_PATH + "../" +AUTH_PASSWORD_VALIDATORS = [] + try: from custom_settings import * except ImportError: @@ -463,3 +466,22 @@ if ISHTAR_SECURE_OPTIONS: SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True X_FRAME_OPTIONS = "DENY" + +if ISHTAR_STRONG_PASSWORD_POLICY and not AUTH_PASSWORD_VALIDATORS: + AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + 'OPTIONS': { + 'min_length': 12, + } + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'ishtar_common.utils.EachCharacterTypeValidator', + }, + ] diff --git a/ishtar_common/tests.py b/ishtar_common/tests.py index e7e665a6f..774fab2a2 100644 --- a/ishtar_common/tests.py +++ b/ishtar_common/tests.py @@ -93,6 +93,7 @@ from ishtar_common.utils import ( rename_and_simplify_media_name, try_fix_file, reverse_coordinates, + EachCharacterTypeValidator, ) from ishtar_common.tasks import launch_export from ishtar_common import utils_secretary @@ -4040,3 +4041,17 @@ class TemplateGenerationTest(TestCase): filtr = doc.get_filter(template, filter_re) for key in expected_keys: self.assertIn(key, filtr) + + +class PasswordValidatorTest(TestCase): + def test_eachcharactertypevalidator(self): + validator = EachCharacterTypeValidator() + self.assertRaises(ValidationError, validator.validate, "") + self.assertRaises(ValidationError, validator.validate, "1") + self.assertRaises(ValidationError, validator.validate, "1a") + self.assertRaises(ValidationError, validator.validate, "1aA") + try: + validator.validate("1aA.") + except ValidationError: + self.fail("Each character class is put, ValidationError should " + "not raise.") diff --git a/ishtar_common/utils.py b/ishtar_common/utils.py index 65b937339..12ab2e646 100644 --- a/ishtar_common/utils.py +++ b/ishtar_common/utils.py @@ -34,6 +34,7 @@ import re import requests from secretary import Renderer as MainSecretaryRenderer, UndefinedSilently import shutil +import string import subprocess import sys import tempfile @@ -51,7 +52,8 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.gis.geos import GEOSGeometry from django.contrib.sessions.backends.db import SessionStore from django.core.cache import cache -from django.core.exceptions import SuspiciousOperation, ObjectDoesNotExist +from django.core.exceptions import SuspiciousOperation, ObjectDoesNotExist, \ + ValidationError from django.core.files import File from django.core.files.storage import FileSystemStorage from django.core.validators import EMPTY_VALUES @@ -2249,4 +2251,42 @@ def reverse_coordinates(wkt): def reverse_list_coordinates(lst): - return list(reversed(lst))
\ No newline at end of file + return list(reversed(lst)) + + +class EachCharacterTypeValidator: + def __init__(self, character_types=None): + if not character_types: + character_types = ( + (_("uppercase letter"), string.ascii_uppercase), + (_("lowercase letter"), string.ascii_lowercase), + (_("number"), [str(i) for i in range(10)]), + (_("punctuation sign"), string.punctuation) + ) + self.character_types = character_types + + def validate(self, password, user=None): + ok = set() + for letter in password: + for idx, character_type in enumerate(self.character_types): + __, character_type = character_type + if idx in ok: + continue + if letter in character_type: + ok.add(idx) + missing = [ + str(character_type[0]) + for idx, character_type in enumerate(self.character_types) + if idx not in ok] + if not missing: + return + msg = str(_("This password must contain one of this character: ")) + ", ".join( + missing) + str(_(".")) + raise ValidationError(msg, code='missing_character_types') + + def get_help_text(self): + return str( + _("Your password must contain each of these characters: ") + ) + ", ".join( + [str(character_type[0]) for character_type in self.character_types] + ) + str(_(".")) |