tokens.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  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
  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. from desecapi.models import RRset
  18. # No 0OIl characters, non-alphanumeric only (select by double-click no line-break)
  19. # https://github.com/bitcoin/bitcoin/blob/master/src/base58.h
  20. ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
  21. class Token(ExportModelOperationsMixin("Token"), rest_framework.authtoken.models.Token):
  22. @staticmethod
  23. def _allowed_subnets_default():
  24. return [ipaddress.IPv4Network("0.0.0.0/0"), ipaddress.IPv6Network("::/0")]
  25. _validators = [
  26. validators.MinValueValidator(timedelta(0)),
  27. validators.MaxValueValidator(timedelta(days=365 * 1000)),
  28. ]
  29. id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
  30. key = models.CharField("Key", max_length=128, db_index=True, unique=True)
  31. user = models.ForeignKey("User", on_delete=models.CASCADE)
  32. name = models.CharField("Name", blank=True, max_length=64)
  33. last_used = models.DateTimeField(null=True, blank=True)
  34. mfa = models.BooleanField(default=None, null=True)
  35. perm_manage_tokens = models.BooleanField(default=False)
  36. allowed_subnets = ArrayField(
  37. CidrAddressField(), default=_allowed_subnets_default.__func__
  38. )
  39. max_age = models.DurationField(null=True, default=None, validators=_validators)
  40. max_unused_period = models.DurationField(
  41. null=True, default=None, validators=_validators
  42. )
  43. domain_policies = models.ManyToManyField("Domain", through="TokenDomainPolicy")
  44. plain = None
  45. objects = NetManager()
  46. class Meta:
  47. constraints = [
  48. models.UniqueConstraint(fields=["id", "user"], name="unique_id_user")
  49. ]
  50. @property
  51. def is_valid(self):
  52. now = timezone.now()
  53. # Check max age
  54. try:
  55. if self.created + self.max_age < now:
  56. return False
  57. except TypeError:
  58. pass
  59. # Check regular usage requirement
  60. try:
  61. if (self.last_used or self.created) + self.max_unused_period < now:
  62. return False
  63. except TypeError:
  64. pass
  65. return True
  66. def generate_key(self):
  67. # Entropy: len(ALPHABET) == 58, log_2(58) * 28 = 164.02
  68. self.plain = "".join(secrets.choice(ALPHABET) for _ in range(28))
  69. self.key = Token.make_hash(self.plain)
  70. return self.key
  71. @staticmethod
  72. def make_hash(plain):
  73. return make_password(plain, salt="static", hasher="pbkdf2_sha256_iter1")
  74. def get_policy(self, rrset=None):
  75. order_by = [
  76. F(field).asc(
  77. nulls_last=True # default Postgres sorting, but: explicit is better than implicit
  78. )
  79. for field in ["domain", "subname", "type"]
  80. ]
  81. return (
  82. self.tokendomainpolicy_set.filter(
  83. Q(domain=rrset.domain if rrset else None) | Q(domain__isnull=True),
  84. Q(subname=rrset.subname if rrset else None) | Q(subname__isnull=True),
  85. Q(type=rrset.type if rrset else None) | Q(type__isnull=True),
  86. )
  87. .order_by(*order_by)
  88. .first()
  89. )
  90. class TokenDomainPolicy(ExportModelOperationsMixin("TokenDomainPolicy"), models.Model):
  91. id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
  92. token = models.ForeignKey(Token, on_delete=models.CASCADE)
  93. domain = models.ForeignKey("Domain", on_delete=models.CASCADE, null=True)
  94. subname = models.CharField(
  95. max_length=178,
  96. blank=True,
  97. null=True,
  98. validators=RRset.subname.field._validators,
  99. )
  100. type = models.CharField(
  101. max_length=10, null=True, validators=RRset.type.field._validators
  102. )
  103. perm_write = models.BooleanField(default=False)
  104. # Token user, filled via trigger. Used by compound FK constraints to tie domain.owner to token.user (see migration).
  105. token_user = models.ForeignKey(
  106. "User", on_delete=models.CASCADE, db_constraint=False, related_name="+"
  107. )
  108. class Meta:
  109. constraints = [
  110. models.UniqueConstraint(
  111. name="unique_policy",
  112. fields=["token", "domain", "subname", "type"],
  113. nulls_distinct=False,
  114. ),
  115. ]
  116. triggers = [
  117. # Ensure that token_user is consistent with token (to fulfill compound FK constraint, see migration)
  118. pgtrigger.Trigger(
  119. name="token_user",
  120. operation=pgtrigger.Update | pgtrigger.Insert,
  121. when=pgtrigger.Before,
  122. func="NEW.token_user_id = (SELECT user_id FROM desecapi_token WHERE id = NEW.token_id); RETURN NEW;",
  123. ),
  124. # Ensure that if there is *any* domain policy for a given token, there is always one with domain=None.
  125. pgtrigger.Trigger(
  126. name="default_policy_primacy",
  127. operation=pgtrigger.Insert | pgtrigger.Update | pgtrigger.Delete,
  128. when=pgtrigger.After,
  129. timing=pgtrigger.Deferred,
  130. func=pgtrigger.Func(
  131. """
  132. IF
  133. EXISTS(SELECT * FROM {meta.db_table} WHERE token_id = COALESCE(NEW.token_id, OLD.token_id)) AND NOT EXISTS(
  134. SELECT * FROM {meta.db_table} WHERE token_id = COALESCE(NEW.token_id, OLD.token_id) AND domain_id IS NULL AND subname IS NULL AND type IS NULL
  135. )
  136. THEN
  137. RAISE EXCEPTION 'Token policies without a default policy are not allowed.';
  138. END IF;
  139. RETURN NULL;
  140. """
  141. ),
  142. ),
  143. ]
  144. @property
  145. def is_default_policy(self):
  146. default_policy = self.token.get_policy()
  147. return default_policy is not None and self.pk == default_policy.pk
  148. @property
  149. def represents_default_policy(self):
  150. return self.domain is None and self.subname is None and self.type is None
  151. def clean(self):
  152. if self._state.adding: # create
  153. # Can't violate policy precedence (default policy has to be first)
  154. default_policy = self.token.get_policy()
  155. if (default_policy is None) and not self.represents_default_policy:
  156. raise ValidationError(
  157. {
  158. "non_field_errors": [
  159. "Policy precedence: The first policy must be the default policy."
  160. ]
  161. }
  162. )
  163. else: # update
  164. # Can't make non-default policy default and vice versa
  165. if self.is_default_policy != self.represents_default_policy:
  166. raise ValidationError(
  167. {
  168. "non_field_errors": [
  169. "When using policies, there must be exactly one default policy."
  170. ]
  171. }
  172. )
  173. def delete(self, *args, **kwargs):
  174. # Can't delete default policy when others exist
  175. if (
  176. self.is_default_policy
  177. and self.token.tokendomainpolicy_set.exclude(pk=self.pk).exists()
  178. ):
  179. raise ValidationError(
  180. {
  181. "non_field_errors": [
  182. "Policy precedence: Can't delete default policy when there exist others."
  183. ]
  184. }
  185. )
  186. return super().delete(*args, **kwargs)
  187. def save(self, *args, **kwargs):
  188. self.clean()
  189. super().save(*args, **kwargs)