v0.166.0
 1from typing import Any
 2
 3from plain import postgres
 4
 5from . import validators
 6from .hashers import (
 7    hash_password,
 8    identify_hasher,
 9)
10
11
12class PasswordField(postgres.TextField):
13    # A hash is what an attacker who got the table would start from. A
14    # failed test's report prints a user without it.
15    value_is_secret = True
16
17    def __init__(self, *args: Any, **kwargs: Any) -> None:
18        kwargs["max_length"] = 128
19        kwargs.setdefault(
20            "validators",
21            [
22                validators.MinimumLengthValidator(),
23                validators.CommonPasswordValidator(),
24                validators.NumericPasswordValidator(),
25            ],
26        )
27        super().__init__(*args, **kwargs)
28
29    def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
30        name, path, args, kwargs = super().deconstruct()
31        if kwargs.get("max_length") == 128:
32            del kwargs["max_length"]
33        return name, path, args, kwargs
34
35    def pre_save(self, model_instance: postgres.Model, add: bool) -> str | None:
36        value = super().pre_save(model_instance, add)
37
38        if value and not self._is_hashed(value):
39            value = hash_password(value)
40            # Set the hashed value back on the instance immediately too
41            setattr(model_instance, self.name, value)
42
43        return value
44
45    @staticmethod
46    def _is_hashed(value: str) -> bool:
47        try:
48            identify_hasher(value)
49            return True
50        except ValueError:
51            return False