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