models.py 19 KB

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