models.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. import datetime
  2. import random
  3. import time
  4. import uuid
  5. from base64 import b64encode
  6. from collections import OrderedDict
  7. from os import urandom
  8. import rest_framework.authtoken.models
  9. from django.conf import settings
  10. from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
  11. from django.core.exceptions import SuspiciousOperation, ValidationError
  12. from django.core.validators import MinValueValidator, RegexValidator
  13. from django.db import models, transaction
  14. from django.utils import timezone
  15. from desecapi import pdns, mixins
  16. def validate_lower(value):
  17. if value != value.lower():
  18. raise ValidationError('Invalid value (not lowercase): %(value)s',
  19. code='invalid',
  20. params={'value': value})
  21. def validate_upper(value):
  22. if value != value.upper():
  23. raise ValidationError('Invalid value (not uppercase): %(value)s',
  24. code='invalid',
  25. params={'value': value})
  26. class MyUserManager(BaseUserManager):
  27. def create_user(self, email, password=None, registration_remote_ip=None, lock=False, dyn=False):
  28. """
  29. Creates and saves a User with the given email, date of
  30. birth and password.
  31. """
  32. if not email:
  33. raise ValueError('Users must have an email address')
  34. user = self.model(
  35. email=self.normalize_email(email),
  36. registration_remote_ip=registration_remote_ip,
  37. locked=timezone.now() if lock else None,
  38. dyn=dyn,
  39. )
  40. user.set_password(password)
  41. user.save(using=self._db)
  42. return user
  43. def create_superuser(self, email, password):
  44. """
  45. Creates and saves a superuser with the given email, date of
  46. birth and password.
  47. """
  48. user = self.create_user(email, password=password)
  49. user.is_admin = True
  50. user.save(using=self._db)
  51. return user
  52. class User(AbstractBaseUser):
  53. email = models.EmailField(
  54. verbose_name='email address',
  55. max_length=191,
  56. unique=True,
  57. )
  58. is_active = models.BooleanField(default=True)
  59. is_admin = models.BooleanField(default=False)
  60. registration_remote_ip = models.CharField(max_length=1024, blank=True)
  61. locked = models.DateTimeField(null=True, blank=True)
  62. created = models.DateTimeField(auto_now_add=True)
  63. limit_domains = models.IntegerField(default=settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT, null=True, blank=True)
  64. dyn = models.BooleanField(default=False)
  65. objects = MyUserManager()
  66. USERNAME_FIELD = 'email'
  67. REQUIRED_FIELDS = []
  68. def get_full_name(self):
  69. return self.email
  70. def get_short_name(self):
  71. return self.email
  72. def get_or_create_first_token(self):
  73. try:
  74. token = Token.objects.filter(user=self).earliest('created')
  75. except Token.DoesNotExist:
  76. token = Token.objects.create(user=self)
  77. return token.key
  78. def __str__(self):
  79. return self.email
  80. # noinspection PyMethodMayBeStatic
  81. def has_perm(self, *_):
  82. """Does the user have a specific permission?"""
  83. # Simplest possible answer: Yes, always
  84. return True
  85. # noinspection PyMethodMayBeStatic
  86. def has_module_perms(self, *_):
  87. """Does the user have permissions to view the app `app_label`?"""
  88. # Simplest possible answer: Yes, always
  89. return True
  90. @property
  91. def is_staff(self):
  92. """Is the user a member of staff?"""
  93. # Simplest possible answer: All admins are staff
  94. return self.is_admin
  95. def unlock(self):
  96. if self.locked is None:
  97. return
  98. # Create domains on pdns that were created after the account was locked.
  99. # Those are obtained using created__gt=self.locked.
  100. # Using published=None gives the same result at the time of writing this
  101. # comment, but it is not semantically the same. If there ever will be
  102. # unpublished domains that are older than the lock, they are not created.
  103. for domain in self.domains.filter(created__gt=self.locked):
  104. domain.create_on_pdns()
  105. # Unlock
  106. self.locked = None
  107. self.save()
  108. class Token(rest_framework.authtoken.models.Token):
  109. key = models.CharField("Key", max_length=40, db_index=True, unique=True)
  110. # relation to user is a ForeignKey, so each user can have more than one token
  111. user = models.ForeignKey(
  112. User, related_name='auth_tokens',
  113. on_delete=models.CASCADE, verbose_name="User"
  114. )
  115. name = models.CharField("Name", max_length=64, default="")
  116. user_specific_id = models.BigIntegerField("User-Specific ID")
  117. def save(self, *args, **kwargs):
  118. if not self.user_specific_id:
  119. self.user_specific_id = random.randrange(16 ** 8)
  120. super().save(*args, **kwargs) # Call the "real" save() method.
  121. def generate_key(self):
  122. return b64encode(urandom(21)).decode('utf-8').replace('/', '-').replace('=', '_').replace('+', '.')
  123. class Meta:
  124. abstract = False
  125. unique_together = (('user', 'user_specific_id'),)
  126. class Domain(models.Model, mixins.SetterMixin):
  127. created = models.DateTimeField(auto_now_add=True)
  128. name = models.CharField(max_length=191,
  129. unique=True,
  130. validators=[validate_lower,
  131. RegexValidator(regex=r'^[a-z0-9_.-]+$',
  132. message='Domain name malformed.',
  133. code='invalid_domain_name')
  134. ])
  135. owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='domains')
  136. published = models.DateTimeField(null=True, blank=True)
  137. _dirtyName = False
  138. def setter_name(self, val):
  139. if val != self.name:
  140. self._dirtyName = True
  141. return val
  142. def clean(self):
  143. if self._dirtyName:
  144. raise ValidationError('You must not change the domain name.')
  145. @property
  146. def keys(self):
  147. return pdns.get_keys(self) if self.published else None
  148. @property
  149. def pdns_id(self):
  150. if '/' in self.name or '?' in self.name:
  151. raise SuspiciousOperation('Invalid hostname ' + self.name)
  152. # See also pdns code, apiZoneNameToId() in ws-api.cc
  153. name = self.name.translate(str.maketrans({'/': '=2F', '_': '=5F'}))
  154. if not name.endswith('.'):
  155. name += '.'
  156. return name
  157. # This method does not use @transaction.atomic as this could lead to
  158. # orphaned zones on pdns.
  159. def create_on_pdns(self):
  160. """
  161. Create zone on pdns
  162. This method should only be called for new domains when they are created,
  163. or when the domain was created with a locked account and not yet propagated.
  164. """
  165. # Throws exception if pdns already knows this zone for some reason
  166. # which means that it is not ours and we should not mess with it.
  167. # We escalate the exception to let the next level deal with the
  168. # response.
  169. pdns.create_zone(self, settings.DEFAULT_NS)
  170. # Update published timestamp on domain
  171. self.published = timezone.now()
  172. self.save()
  173. # Make our RRsets consistent with pdns (specifically, NS may exist)
  174. self.sync_from_pdns()
  175. # For dedyn.io domains, propagate NS and DS delegation RRsets
  176. subname, parent_pdns_id = self.pdns_id.split('.', 1)
  177. if parent_pdns_id == 'dedyn.io.':
  178. try:
  179. parent = Domain.objects.get(name='dedyn.io')
  180. except Domain.DoesNotExist:
  181. pass
  182. else:
  183. rrsets = RRset.plain_to_rrsets([
  184. {'subname': subname, 'type': 'NS', 'ttl': 3600,
  185. 'contents': settings.DEFAULT_NS},
  186. {'subname': subname, 'type': 'DS', 'ttl': 60,
  187. 'contents': [ds for k in self.keys for ds in k['ds']]}
  188. ], domain=parent)
  189. parent.write_rrsets(rrsets)
  190. @transaction.atomic
  191. def sync_from_pdns(self):
  192. self.rrset_set.all().delete()
  193. rrsets = []
  194. rrs = []
  195. for rrset_data in pdns.get_rrset_datas(self):
  196. if rrset_data['type'] in RRset.RESTRICTED_TYPES:
  197. continue
  198. records = rrset_data.pop('records')
  199. rrset = RRset(**rrset_data)
  200. rrsets.append(rrset)
  201. rrs.extend([RR(rrset=rrset, content=record) for record in records])
  202. RRset.objects.bulk_create(rrsets)
  203. RR.objects.bulk_create(rrs)
  204. @transaction.atomic
  205. def write_rrsets(self, rrsets):
  206. # Base queryset for all RRsets of the current domain
  207. rrset_qs = RRset.objects.filter(domain=self)
  208. # Set to check RRset uniqueness
  209. rrsets_seen = set()
  210. # We want to return all new, changed, and unchanged RRsets (but not
  211. # deleted ones). We store them here, indexed by (subname, type).
  212. rrsets_to_return = OrderedDict()
  213. # Record contents to send to pdns, indexed by their RRset
  214. rrsets_for_pdns = {}
  215. # Always-false Q object: https://stackoverflow.com/a/35894246/6867099
  216. q_meaty = models.Q(pk__isnull=True)
  217. q_empty = models.Q(pk__isnull=True)
  218. # Determine which RRsets need to be updated or deleted
  219. for rrset, rrs in rrsets.items():
  220. if rrset.domain != self:
  221. raise ValueError('RRset has wrong domain')
  222. if (rrset.subname, rrset.type) in rrsets_seen:
  223. raise ValueError('RRset repeated with same subname and type')
  224. if rrs is not None and not all(rr.rrset is rrset for rr in rrs):
  225. raise ValueError('RR has wrong parent RRset')
  226. rrsets_seen.add((rrset.subname, rrset.type))
  227. q = models.Q(subname=rrset.subname, type=rrset.type)
  228. if rrs or rrs is None:
  229. rrsets_to_return[(rrset.subname, rrset.type)] = rrset
  230. q_meaty |= q
  231. else:
  232. # Set TTL so that pdns does not get confused if missing
  233. rrset.ttl = 1
  234. rrsets_for_pdns[rrset] = []
  235. q_empty |= q
  236. # Construct querysets representing RRsets that do (not) have RR
  237. # contents and lock them
  238. qs_meaty = rrset_qs.filter(q_meaty).select_for_update()
  239. qs_empty = rrset_qs.filter(q_empty).select_for_update()
  240. # For existing RRsets, execute TTL updates and/or mark for RR update.
  241. # First, let's create a to-do dict; we'll need it later for new RRsets.
  242. rrsets_with_new_rrs = []
  243. rrsets_meaty_todo = dict(rrsets_to_return)
  244. for rrset in qs_meaty.all():
  245. rrsets_to_return[(rrset.subname, rrset.type)] = rrset
  246. rrset_temp = rrsets_meaty_todo.pop((rrset.subname, rrset.type))
  247. rrs = {rr.content for rr in rrset.records.all()}
  248. partial = rrsets[rrset_temp] is None
  249. if partial:
  250. rrs_temp = rrs
  251. else:
  252. rrs_temp = {rr.content for rr in rrsets[rrset_temp]}
  253. # Take current TTL if none was given
  254. rrset_temp.ttl = rrset_temp.ttl or rrset.ttl
  255. changed_ttl = (rrset_temp.ttl != rrset.ttl)
  256. changed_rrs = not partial and (rrs_temp != rrs)
  257. if changed_ttl:
  258. rrset.ttl = rrset_temp.ttl
  259. rrset.save()
  260. if changed_rrs:
  261. rrsets_with_new_rrs.append(rrset)
  262. if changed_ttl or changed_rrs:
  263. rrsets_for_pdns[rrset] = [RR(rrset=rrset, content=rr_content)
  264. for rr_content in rrs_temp]
  265. # At this point, rrsets_meaty_todo contains new RRsets only, with
  266. # a list of RRs or with None associated.
  267. for key, rrset in list(rrsets_meaty_todo.items()):
  268. if rrsets[rrset] is None:
  269. # None means "don't change RRs". In the context of a new RRset,
  270. # this really is no-op, and we do not need to return the RRset.
  271. rrsets_to_return.pop((rrset.subname, rrset.type))
  272. else:
  273. # If there are associated RRs, let's save the RRset. This does
  274. # not save the RRs yet.
  275. rrsets_with_new_rrs.append(rrset)
  276. rrset.save()
  277. # In either case, send a request to pdns so that we can take
  278. # advantage of pdns' type validation check (even if no RRs given).
  279. rrsets_for_pdns[rrset] = rrsets[rrset]
  280. # Repeat lock to make sure new RRsets are also locked
  281. rrset_qs.filter(q_meaty).select_for_update()
  282. # Delete empty RRsets
  283. qs_empty.delete()
  284. # Update contents of modified RRsets
  285. RR.objects.filter(rrset__in=rrsets_with_new_rrs).delete()
  286. RR.objects.bulk_create([rr
  287. for (rrset, rrs) in rrsets_for_pdns.items()
  288. if rrs and rrset in rrsets_with_new_rrs
  289. for rr in rrs])
  290. # Update published timestamp on domain
  291. self.published = timezone.now()
  292. self.save()
  293. # Send RRsets to pdns
  294. if rrsets_for_pdns:
  295. pdns.set_rrsets(self, rrsets_for_pdns)
  296. # Return RRsets
  297. return list(rrsets_to_return.values())
  298. @transaction.atomic
  299. def delete(self, *args, **kwargs):
  300. # Delete delegation for dynDNS domains (direct child of dedyn.io)
  301. subname, parent_pdns_id = self.pdns_id.split('.', 1)
  302. if parent_pdns_id == 'dedyn.io.':
  303. try:
  304. parent = Domain.objects.get(name='dedyn.io')
  305. except Domain.DoesNotExist:
  306. pass
  307. else:
  308. rrsets = parent.rrset_set.filter(subname=subname,
  309. type__in=['NS', 'DS']).all()
  310. parent.write_rrsets({rrset: [] for rrset in rrsets})
  311. # Delete domain
  312. super().delete(*args, **kwargs)
  313. pdns.delete_zone(self)
  314. @transaction.atomic
  315. def save(self, *args, **kwargs):
  316. new = self.pk is None
  317. self.clean()
  318. self.clean_fields()
  319. super().save(*args, **kwargs)
  320. if new and not self.owner.locked:
  321. self.create_on_pdns()
  322. def __str__(self):
  323. """
  324. Return domain name.
  325. """
  326. return self.name
  327. class Meta:
  328. ordering = ('created',)
  329. def get_default_value_created():
  330. return timezone.now()
  331. def get_default_value_due():
  332. return timezone.now() + datetime.timedelta(days=7)
  333. def get_default_value_mref():
  334. return "ONDON" + str(time.time())
  335. class Donation(models.Model):
  336. created = models.DateTimeField(default=get_default_value_created)
  337. name = models.CharField(max_length=255)
  338. iban = models.CharField(max_length=34)
  339. bic = models.CharField(max_length=11)
  340. amount = models.DecimalField(max_digits=8, decimal_places=2)
  341. message = models.CharField(max_length=255, blank=True)
  342. due = models.DateTimeField(default=get_default_value_due)
  343. mref = models.CharField(max_length=32, default=get_default_value_mref)
  344. email = models.EmailField(max_length=255, blank=True)
  345. def save(self, *args, **kwargs):
  346. self.iban = self.iban[:6] + "xxx" # do NOT save account details
  347. super().save(*args, **kwargs) # Call the "real" save() method.
  348. class Meta:
  349. ordering = ('created',)
  350. class RRset(models.Model, mixins.SetterMixin):
  351. id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
  352. created = models.DateTimeField(auto_now_add=True)
  353. updated = models.DateTimeField(null=True)
  354. domain = models.ForeignKey(Domain, on_delete=models.CASCADE)
  355. subname = models.CharField(max_length=178,
  356. blank=True,
  357. validators=[validate_lower,
  358. RegexValidator(regex=r'^[*]?[a-z0-9_.-]*$',
  359. message='Subname malformed.',
  360. code='invalid_subname')
  361. ]
  362. )
  363. type = models.CharField(max_length=10,
  364. validators=[validate_upper,
  365. RegexValidator(regex=r'^[A-Z][A-Z0-9]*$',
  366. message='Type malformed.',
  367. code='invalid_type')
  368. ]
  369. )
  370. ttl = models.PositiveIntegerField(validators=[MinValueValidator(1)])
  371. _dirty = False
  372. DEAD_TYPES = ('ALIAS', 'DNAME')
  373. RESTRICTED_TYPES = ('SOA', 'RRSIG', 'DNSKEY', 'NSEC3PARAM', 'OPT')
  374. class Meta:
  375. unique_together = (("domain", "subname", "type"),)
  376. def __init__(self, *args, **kwargs):
  377. self._dirties = set()
  378. super().__init__(*args, **kwargs)
  379. def setter_domain(self, val):
  380. if val != self.domain:
  381. self._dirties.add('domain')
  382. return val
  383. def setter_subname(self, val):
  384. # On PUT, RRsetSerializer sends None, denoting the unchanged value
  385. if val is None:
  386. return self.subname
  387. if val != self.subname:
  388. self._dirties.add('subname')
  389. return val
  390. def setter_type(self, val):
  391. if val != self.type:
  392. self._dirties.add('type')
  393. return val
  394. def setter_ttl(self, val):
  395. if val != self.ttl:
  396. self._dirties.add('ttl')
  397. return val
  398. def clean(self):
  399. errors = {}
  400. for field in (self._dirties & {'domain', 'subname', 'type'}):
  401. errors[field] = ValidationError(
  402. 'You cannot change the `%s` field.' % field)
  403. if errors:
  404. raise ValidationError(errors)
  405. def get_dirties(self):
  406. return self._dirties
  407. @property
  408. def name(self):
  409. return '.'.join(filter(None, [self.subname, self.domain.name])) + '.'
  410. @transaction.atomic
  411. def delete(self, *args, **kwargs):
  412. self.domain.write_rrsets({self: []})
  413. self._dirties = {}
  414. def save(self, *args, **kwargs):
  415. # If not new, the only thing that can change is the TTL
  416. if self.created is None or 'ttl' in self.get_dirties():
  417. self.updated = timezone.now()
  418. self.full_clean()
  419. # Tell Django to not attempt an update, although the pk is not None
  420. kwargs['force_insert'] = (self.created is None)
  421. super().save(*args, **kwargs)
  422. self._dirties = {}
  423. @staticmethod
  424. def plain_to_rrsets(datas, *, domain):
  425. rrsets = {}
  426. for data in datas:
  427. rrset = RRset(domain=domain, subname=data['subname'],
  428. type=data['type'], ttl=data['ttl'])
  429. rrsets[rrset] = [RR(rrset=rrset, content=content)
  430. for content in data['contents']]
  431. return rrsets
  432. class RR(models.Model):
  433. created = models.DateTimeField(auto_now_add=True)
  434. rrset = models.ForeignKey(RRset, on_delete=models.CASCADE, related_name='records')
  435. # max_length is determined based on the calculation in
  436. # https://lists.isc.org/pipermail/bind-users/2008-April/070148.html
  437. content = models.CharField(max_length=4092)