1import gzip
2from pathlib import Path
3
4from plain.exceptions import (
5 ValidationError,
6)
7from plain.utils.deconstruct import deconstructible
8from plain.utils.functional import cached_property
9from plain.utils.text import pluralize
10
11
12@deconstructible
13class MinimumLengthValidator:
14 """
15 Validate that the password is of a minimum length.
16 """
17
18 def __init__(self, min_length=8):
19 self.min_length = min_length
20
21 def __call__(self, password):
22 if len(password) < self.min_length:
23 raise ValidationError(
24 pluralize(
25 "This password is too short. It must contain at least "
26 "%(min_length)d character.",
27 "This password is too short. It must contain at least "
28 "%(min_length)d characters.",
29 self.min_length,
30 ),
31 code="password_too_short",
32 params={"min_length": self.min_length},
33 )
34
35
36@deconstructible
37class CommonPasswordValidator:
38 """
39 Validate that the password is not a common password.
40
41 The password is rejected if it occurs in a provided list of passwords,
42 which may be gzipped. The list Plain ships with contains 20000 common
43 passwords (lowercased and deduplicated), created by Royce Williams:
44 https://gist.github.com/roycewilliams/226886fd01572964e1431ac8afc999ce
45 The password list must be lowercased to match the comparison in validate().
46 """
47
48 @cached_property
49 def DEFAULT_PASSWORD_LIST_PATH(self):
50 return Path(__file__).resolve().parent / "common-passwords.txt.gz"
51
52 def __init__(self, password_list_path=DEFAULT_PASSWORD_LIST_PATH):
53 if password_list_path is CommonPasswordValidator.DEFAULT_PASSWORD_LIST_PATH:
54 password_list_path = self.DEFAULT_PASSWORD_LIST_PATH
55 try:
56 with gzip.open(password_list_path, "rt", encoding="utf-8") as f:
57 self.passwords = {x.strip() for x in f}
58 except OSError:
59 with open(password_list_path) as f:
60 self.passwords = {x.strip() for x in f}
61
62 def __call__(self, password):
63 if password.lower().strip() in self.passwords:
64 raise ValidationError(
65 "This password is too common.",
66 code="password_too_common",
67 )
68
69
70@deconstructible
71class NumericPasswordValidator:
72 """
73 Validate that the password is not entirely numeric.
74 """
75
76 def __call__(self, password):
77 if password.isdigit():
78 raise ValidationError(
79 "This password is entirely numeric.",
80 code="password_entirely_numeric",
81 )