Plain is headed towards 1.0! Subscribe for development updates →

 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            )