users.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. from __future__ import annotations
  2. import uuid
  3. from django.conf import settings
  4. from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
  5. from django.core.mail import EmailMessage, get_connection
  6. from django.db import models
  7. from django.template.loader import get_template
  8. from django.utils import timezone
  9. from django_prometheus.models import ExportModelOperationsMixin
  10. from desecapi import logger, metrics
  11. class MyUserManager(BaseUserManager):
  12. def create_user(self, email, password, **extra_fields):
  13. """
  14. Creates and saves a User with the given email and password.
  15. """
  16. if not email:
  17. raise ValueError("Users must have an email address")
  18. email = self.normalize_email(email)
  19. user = self.model(email=email, **extra_fields)
  20. user.set_password(password)
  21. user.save(using=self._db)
  22. return user
  23. class User(ExportModelOperationsMixin("User"), AbstractBaseUser):
  24. @staticmethod
  25. def _limit_domains_default():
  26. return settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT
  27. id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
  28. email = models.EmailField(
  29. verbose_name="email address",
  30. db_collation="case_insensitive", # LIKE queries: LOWER(email COLLATE "und-x-icu") LIKE '%...%'
  31. unique=True,
  32. )
  33. email_verified = models.DateTimeField(null=True, blank=True)
  34. is_active = models.BooleanField(default=True, null=True)
  35. is_admin = models.BooleanField(default=False)
  36. created = models.DateTimeField(auto_now_add=True)
  37. credentials_changed = models.DateTimeField(auto_now_add=True)
  38. limit_domains = models.PositiveIntegerField(
  39. default=_limit_domains_default.__func__, null=True, blank=True
  40. )
  41. needs_captcha = models.BooleanField(default=True)
  42. outreach_preference = models.BooleanField(default=True)
  43. objects = MyUserManager()
  44. USERNAME_FIELD = "email"
  45. REQUIRED_FIELDS = []
  46. def get_full_name(self):
  47. return self.email
  48. def get_short_name(self):
  49. return self.email
  50. def __str__(self):
  51. return self.email
  52. # noinspection PyMethodMayBeStatic
  53. def has_perm(self, *_):
  54. """Does the user have a specific permission?"""
  55. # Simplest possible answer: Yes, always
  56. return True
  57. # noinspection PyMethodMayBeStatic
  58. def has_module_perms(self, *_):
  59. """Does the user have permissions to view the app `app_label`?"""
  60. # Simplest possible answer: Yes, always
  61. return True
  62. @property
  63. def is_staff(self):
  64. """Is the user a member of staff?"""
  65. # Simplest possible answer: All admins are staff
  66. return self.is_admin
  67. @property
  68. def mfa_enabled(self):
  69. return self.basefactor_set.exclude(last_used__isnull=True).exists()
  70. def activate(self):
  71. self.is_active = True
  72. self.needs_captcha = False
  73. self.save()
  74. def change_email(self, email):
  75. old_email = self.email
  76. self.email = email
  77. self.credentials_changed = timezone.now()
  78. self.validate_unique()
  79. self.save()
  80. self.send_email("change-email-confirmation-old-email", recipient=old_email)
  81. def change_password(self, raw_password):
  82. self.set_password(raw_password)
  83. self.credentials_changed = timezone.now()
  84. self.save()
  85. self.send_email("password-change-confirmation")
  86. def delete(self, *args, **kwargs):
  87. pk = self.pk
  88. ret = super().delete(*args, **kwargs)
  89. logger.warning(f"User {pk} deleted")
  90. return ret
  91. def save(self, *args, **kwargs):
  92. if kwargs.pop("credentials_changed", False):
  93. self.credentials_changed = timezone.now()
  94. # https://docs.djangoproject.com/en/4.2/releases/4.2/#setting-update-fields-in-model-save-may-now-be-required
  95. if kwargs.get("update_fields") is not None:
  96. kwargs["update_fields"] = {"credentials_changed"}.union(
  97. kwargs["update_fields"]
  98. )
  99. super().save(*args, **kwargs)
  100. def send_email(
  101. self, reason, context=None, recipient=None, subject=None, template=None
  102. ):
  103. fast_lane = "email_fast_lane"
  104. slow_lane = "email_slow_lane"
  105. immediate_lane = "email_immediate_lane"
  106. lanes = {
  107. "activate-account": slow_lane,
  108. "change-email": slow_lane,
  109. "change-email-confirmation-old-email": fast_lane,
  110. "change-outreach-preference": slow_lane,
  111. "confirm-account": slow_lane,
  112. "create-totp": fast_lane,
  113. "password-change-confirmation": fast_lane,
  114. "reset-password": fast_lane,
  115. "delete-account": fast_lane,
  116. "domain-dyndns": fast_lane,
  117. "renew-domain": immediate_lane,
  118. }
  119. if reason not in lanes:
  120. raise ValueError(
  121. f"Cannot send email to user {self.pk} without a good reason: {reason}"
  122. )
  123. context = context or {}
  124. template = template or get_template(f"emails/{reason}/content.txt")
  125. content = template.render(context)
  126. content += f"\nSupport Reference: user_id = {self.pk}\n"
  127. logger.warning(
  128. f"Queuing email for user account {self.pk} (reason: {reason}, lane: {lanes[reason]})"
  129. )
  130. num_queued = EmailMessage(
  131. subject=(
  132. subject or get_template(f"emails/{reason}/subject.txt").render(context)
  133. ).strip(),
  134. body=content,
  135. from_email=get_template("emails/from.txt").render(),
  136. to=[recipient or self.email],
  137. connection=get_connection(
  138. lane=lanes[reason], debug={"user": self.pk, "reason": reason}
  139. ),
  140. ).send()
  141. metrics.get("desecapi_messages_queued").labels(
  142. reason, self.pk, lanes[reason]
  143. ).observe(num_queued)
  144. return num_queued