tokens.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. from __future__ import annotations
  2. import ipaddress
  3. import secrets
  4. import uuid
  5. from datetime import timedelta
  6. import pgtrigger
  7. import rest_framework.authtoken.models
  8. from django.contrib.auth.hashers import make_password
  9. from django.contrib.postgres.fields import ArrayField
  10. from django.core import validators
  11. from django.core.exceptions import ValidationError
  12. from django.db import models, transaction
  13. from django.db.models import F, Q
  14. from django.utils import timezone
  15. from django_prometheus.models import ExportModelOperationsMixin
  16. from netfields import CidrAddressField, NetManager
  17. # No 0OIl characters, non-alphanumeric only (select by double-click no line-break)
  18. # https://github.com/bitcoin/bitcoin/blob/master/src/base58.h
  19. ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
  20. class Token(ExportModelOperationsMixin("Token"), rest_framework.authtoken.models.Token):
  21. @staticmethod
  22. def _allowed_subnets_default():
  23. return [ipaddress.IPv4Network("0.0.0.0/0"), ipaddress.IPv6Network("::/0")]
  24. _validators = [
  25. validators.MinValueValidator(timedelta(0)),
  26. validators.MaxValueValidator(timedelta(days=365 * 1000)),
  27. ]
  28. id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
  29. key = models.CharField("Key", max_length=128, db_index=True, unique=True)
  30. user = models.ForeignKey("User", on_delete=models.CASCADE)
  31. name = models.CharField("Name", blank=True, max_length=64)
  32. last_used = models.DateTimeField(null=True, blank=True)
  33. mfa = models.BooleanField(default=None, null=True)
  34. perm_manage_tokens = models.BooleanField(default=False)
  35. allowed_subnets = ArrayField(
  36. CidrAddressField(), default=_allowed_subnets_default.__func__
  37. )
  38. max_age = models.DurationField(null=True, default=None, validators=_validators)
  39. max_unused_period = models.DurationField(
  40. null=True, default=None, validators=_validators
  41. )
  42. domain_policies = models.ManyToManyField("Domain", through="TokenDomainPolicy")
  43. plain = None
  44. objects = NetManager()
  45. class Meta:
  46. constraints = [
  47. models.UniqueConstraint(fields=["id", "user"], name="unique_id_user")
  48. ]
  49. @property
  50. def is_valid(self):
  51. now = timezone.now()
  52. # Check max age
  53. try:
  54. if self.created + self.max_age < now:
  55. return False
  56. except TypeError:
  57. pass
  58. # Check regular usage requirement
  59. try:
  60. if (self.last_used or self.created) + self.max_unused_period < now:
  61. return False
  62. except TypeError:
  63. pass
  64. return True
  65. def generate_key(self):
  66. # Entropy: len(ALPHABET) == 58, log_2(58) * 28 = 164.02
  67. self.plain = "".join(secrets.choice(ALPHABET) for _ in range(28))
  68. self.key = Token.make_hash(self.plain)
  69. return self.key
  70. @staticmethod
  71. def make_hash(plain):
  72. return make_password(plain, salt="static", hasher="pbkdf2_sha256_iter1")
  73. def get_policy(self, *, domain=None):
  74. order_by = F("domain").asc(
  75. nulls_last=True
  76. ) # default Postgres sorting, but: explicit is better than implicit
  77. return (
  78. self.tokendomainpolicy_set.filter(Q(domain=domain) | Q(domain__isnull=True))
  79. .order_by(order_by)
  80. .first()
  81. )
  82. @transaction.atomic
  83. def delete(self):
  84. # This is needed because Model.delete() emulates cascade delete via django.db.models.deletion.Collector.delete()
  85. # which deletes related objects in pk order. However, the default policy has to be deleted last.
  86. # Perhaps this will change with https://code.djangoproject.com/ticket/21961
  87. self.tokendomainpolicy_set.filter(domain__isnull=False).delete()
  88. self.tokendomainpolicy_set.filter(domain__isnull=True).delete()
  89. return super().delete()
  90. @pgtrigger.register(
  91. # Ensure that token_user is consistent with token
  92. pgtrigger.Trigger(
  93. name="token_user",
  94. operation=pgtrigger.Update | pgtrigger.Insert,
  95. when=pgtrigger.Before,
  96. func="NEW.token_user_id = (SELECT user_id FROM desecapi_token WHERE id = NEW.token_id); RETURN NEW;",
  97. ),
  98. # Ensure that if there is *any* domain policy for a given token, there is always one with domain=None.
  99. pgtrigger.Trigger(
  100. name="default_policy_on_insert",
  101. operation=pgtrigger.Insert,
  102. when=pgtrigger.Before,
  103. # Trigger `condition` arguments (corresponding to WHEN clause) don't support subqueries, so we use `func`
  104. func="IF (NEW.domain_id IS NOT NULL and NOT EXISTS(SELECT * FROM desecapi_tokendomainpolicy WHERE domain_id IS NULL AND token_id = NEW.token_id)) THEN "
  105. " RAISE EXCEPTION 'Cannot insert non-default policy into % table when default policy is not present', TG_TABLE_NAME; "
  106. "END IF; RETURN NEW;",
  107. ),
  108. pgtrigger.Protect(
  109. name="default_policy_on_update",
  110. operation=pgtrigger.Update,
  111. when=pgtrigger.Before,
  112. condition=pgtrigger.Q(old__domain__isnull=True, new__domain__isnull=False),
  113. ),
  114. # Ideally, a deferred trigger (https://github.com/Opus10/django-pgtrigger/issues/14). Available in 3.4.0.
  115. pgtrigger.Trigger(
  116. name="default_policy_on_delete",
  117. operation=pgtrigger.Delete,
  118. when=pgtrigger.Before,
  119. # Trigger `condition` arguments (corresponding to WHEN clause) don't support subqueries, so we use `func`
  120. func="IF (OLD.domain_id IS NULL and EXISTS(SELECT * FROM desecapi_tokendomainpolicy WHERE domain_id IS NOT NULL AND token_id = OLD.token_id)) THEN "
  121. " RAISE EXCEPTION 'Cannot delete default policy from % table when non-default policy is present', TG_TABLE_NAME; "
  122. "END IF; RETURN OLD;",
  123. ),
  124. )
  125. class TokenDomainPolicy(ExportModelOperationsMixin("TokenDomainPolicy"), models.Model):
  126. token = models.ForeignKey(Token, on_delete=models.CASCADE)
  127. domain = models.ForeignKey("Domain", on_delete=models.CASCADE, null=True)
  128. perm_dyndns = models.BooleanField(default=False)
  129. perm_rrsets = models.BooleanField(default=False)
  130. # Token user, filled via trigger. Used by compound FK constraints to tie domain.owner to token.user (see migration).
  131. token_user = models.ForeignKey(
  132. "User", on_delete=models.CASCADE, db_constraint=False, related_name="+"
  133. )
  134. class Meta:
  135. constraints = [
  136. models.UniqueConstraint(fields=["token", "domain"], name="unique_entry"),
  137. models.UniqueConstraint(
  138. fields=["token"],
  139. condition=Q(domain__isnull=True),
  140. name="unique_entry_null_domain",
  141. ),
  142. ]
  143. def clean(self):
  144. default_policy = self.token.get_policy(domain=None)
  145. if self.pk: # update
  146. # Can't change policy's default status ("domain NULLness") to maintain policy precedence
  147. if (self.domain is None) != (self.pk == default_policy.pk):
  148. raise ValidationError(
  149. {
  150. "domain": "Policy precedence: Cannot disable default policy when others exist."
  151. }
  152. )
  153. else: # create
  154. # Can't violate policy precedence (default policy has to be first)
  155. if (self.domain is not None) and (default_policy is None):
  156. raise ValidationError(
  157. {
  158. "domain": "Policy precedence: The first policy must be the default policy."
  159. }
  160. )
  161. def delete(self, *args, **kwargs):
  162. # Can't delete default policy when others exist
  163. if (self.domain is None) and self.token.tokendomainpolicy_set.exclude(
  164. domain__isnull=True
  165. ).exists():
  166. raise ValidationError(
  167. {
  168. "domain": "Policy precedence: Can't delete default policy when there exist others."
  169. }
  170. )
  171. return super().delete(*args, **kwargs)
  172. def save(self, *args, **kwargs):
  173. self.clean()
  174. super().save(*args, **kwargs)