models.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  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 rest_framework.authtoken.models import Token
  10. class MyUserManager(BaseUserManager):
  11. def create_user(self, email, password=None, registration_remote_ip=None, captcha_required=False, dyn=False):
  12. """
  13. Creates and saves a User with the given email, date of
  14. birth and password.
  15. """
  16. if not email:
  17. raise ValueError('Users must have an email address')
  18. user = self.model(
  19. email=self.normalize_email(email),
  20. registration_remote_ip=registration_remote_ip,
  21. captcha_required=captcha_required,
  22. dyn=dyn,
  23. )
  24. user.set_password(password)
  25. user.save(using=self._db)
  26. return user
  27. def create_superuser(self, email, password):
  28. """
  29. Creates and saves a superuser with the given email, date of
  30. birth and password.
  31. """
  32. user = self.create_user(email,
  33. password=password
  34. )
  35. user.is_admin = True
  36. user.save(using=self._db)
  37. return user
  38. class User(AbstractBaseUser):
  39. email = models.EmailField(
  40. verbose_name='email address',
  41. max_length=191,
  42. unique=True,
  43. )
  44. is_active = models.BooleanField(default=True)
  45. is_admin = models.BooleanField(default=False)
  46. registration_remote_ip = models.CharField(max_length=1024, blank=True)
  47. captcha_required = models.BooleanField(default=False)
  48. created = models.DateTimeField(auto_now_add=True)
  49. limit_domains = models.IntegerField(default=settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT,null=True,blank=True)
  50. dyn = models.BooleanField(default=False)
  51. objects = MyUserManager()
  52. USERNAME_FIELD = 'email'
  53. REQUIRED_FIELDS = []
  54. def get_full_name(self):
  55. return self.email
  56. def get_short_name(self):
  57. return self.email
  58. def get_token(self):
  59. token, created = Token.objects.get_or_create(user=self)
  60. return token.key
  61. def __str__(self):
  62. return self.email
  63. def has_perm(self, perm, obj=None):
  64. "Does the user have a specific permission?"
  65. # Simplest possible answer: Yes, always
  66. return True
  67. def has_module_perms(self, app_label):
  68. "Does the user have permissions to view the app `app_label`?"
  69. # Simplest possible answer: Yes, always
  70. return True
  71. @property
  72. def is_staff(self):
  73. "Is the user a member of staff?"
  74. # Simplest possible answer: All admins are staff
  75. return self.is_admin
  76. def unlock(self):
  77. self.captcha_required = False
  78. for domain in self.domains.all():
  79. domain.deploy(force=True)
  80. self.save()
  81. class Domain(models.Model, mixins.SetterMixin):
  82. created = models.DateTimeField(auto_now_add=True)
  83. name = models.CharField(max_length=191, unique=True)
  84. owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='domains')
  85. _dirtyName = False
  86. def setter_name(self, val):
  87. if val != self.name:
  88. self._dirtyName = True
  89. return val
  90. def clean(self):
  91. if self._dirtyName:
  92. raise ValidationError('You must not change the domain name')
  93. @property
  94. def keys(self):
  95. return pdns.get_keys(self)
  96. @property
  97. def pdns_id(self):
  98. if '/' in self.name or '?' in self.name:
  99. raise SuspiciousOperation('Invalid hostname ' + self.name)
  100. # Transform to be valid pdns API identifiers (:id in their docs). The
  101. # '/' case here is just a safety measure (this case should never occur due
  102. # to the above check).
  103. # See also pdns code, apiZoneNameToId() in ws-api.cc
  104. name = self.name.translate(str.maketrans({'/': '=2F', '_': '=5F'}))
  105. if not name.endswith('.'):
  106. name += '.'
  107. return name
  108. def deploy(self, force=False):
  109. """
  110. Make sure that pdns gets the latest information about this domain/zone.
  111. Re-syncing is relatively expensive and should not happen routinely.
  112. """
  113. # Try to create zone, in case it does not exist yet
  114. try:
  115. pdns.create_zone(self, settings.DEFAULT_NS)
  116. new = True
  117. except pdns.PdnsException as e:
  118. if force and (e.status_code == 422 \
  119. and e.detail.endswith(' already exists')):
  120. new = False
  121. else:
  122. raise e
  123. if new:
  124. # Import RRsets that may have been created (e.g. during captcha lock).
  125. rrsets = self.rrset_set.all()
  126. if rrsets:
  127. pdns.set_rrsets(self, rrsets)
  128. # Make our RRsets consistent with pdns (specifically, NS may exist)
  129. self.sync_from_pdns()
  130. subname, parent_pdns_id = self.pdns_id.split('.', 1)
  131. if parent_pdns_id == 'dedyn.io.':
  132. try:
  133. parent = Domain.objects.get(name='dedyn.io')
  134. except Domain.DoesNotExist:
  135. pass
  136. else:
  137. parent.write_rrsets([
  138. {'subname': subname, 'type': 'NS', 'ttl': 3600,
  139. 'contents': settings.DEFAULT_NS},
  140. {'subname': subname, 'type': 'DS', 'ttl': 60,
  141. 'contents': [ds for k in self.keys for ds in k['ds']]}
  142. ])
  143. else:
  144. # Zone exists. For the case that pdns knows records that we do not
  145. # (e.g. if a locked account has deleted an RRset), it is necessary
  146. # to purge all records here. However, there is currently no way to
  147. # do this through the pdns API (not to mention doing it atomically
  148. # with setting the new RRsets). So for now, we have disabled RRset
  149. # deletion for locked accounts.
  150. pdns.set_rrsets(self, self.rrset_set.all())
  151. @transaction.atomic
  152. def sync_from_pdns(self):
  153. self.rrset_set.all().delete()
  154. rrsets = []
  155. rrs = []
  156. for rrset_data in pdns.get_rrset_datas(self):
  157. if rrset_data['type'] in RRset.RESTRICTED_TYPES:
  158. continue
  159. records = rrset_data.pop('records')
  160. rrset = RRset(**rrset_data)
  161. rrsets.append(rrset)
  162. rrs.extend([RR(rrset=rrset, content=record) for record in records])
  163. RRset.objects.bulk_create(rrsets)
  164. RR.objects.bulk_create(rrs)
  165. def write_rrsets(self, datas):
  166. rrsets = {}
  167. for data in datas:
  168. rrset = RRset(domain=self, subname=data['subname'],
  169. type=data['type'], ttl=data['ttl'])
  170. rrsets[rrset] = [RR(rrset=rrset, content=content)
  171. for content in data['contents']]
  172. self._write_rrsets(rrsets)
  173. @transaction.atomic
  174. def _write_rrsets(self, rrsets):
  175. # Always-false Q object: https://stackoverflow.com/a/35894246/6867099
  176. rrsets_index = {}
  177. q_update = models.Q(pk__isnull=True)
  178. q_delete = models.Q(pk__isnull=True)
  179. # Determine which RRsets need to be updated or deleted
  180. for rrset, rrs in rrsets.items():
  181. if rrset.domain is not self:
  182. raise ValueError('RRset has wrong domain')
  183. if (rrset.subname, rrset.type) in rrsets_index:
  184. raise ValueError('RRset repeated with same subname and type')
  185. if not all(rr.rrset is rrset for rr in rrs):
  186. raise ValueError('RR has wrong parent RRset')
  187. # Book-keeping
  188. rrsets_index[(rrset.subname, rrset.type)] = rrset
  189. q = models.Q(subname=rrset.subname) & models.Q(type=rrset.type)
  190. if rrs:
  191. q_update |= q
  192. else:
  193. q_delete |= q
  194. # Lock RRsets
  195. RRset.objects.filter(q_update | q_delete, domain=self).select_for_update()
  196. # Figure out which RRsets are unchanged and can be excluded
  197. exclude_from_update = []
  198. qs_update = RRset.objects.filter(q_update, domain=self)
  199. for rrset_old in qs_update.prefetch_related('records').all():
  200. rrset_new = rrsets_index[(rrset_old.subname, rrset_old.type)]
  201. if rrset_old.ttl != rrset_new.ttl:
  202. continue
  203. rrs_new = {rr.content for rr in rrsets[rrset_new]}
  204. rrs_old = {rr.content for rr in rrset_old.records.all()}
  205. if rrs_new != rrs_old:
  206. continue
  207. # Old and new contents do not differ, so we can skip this RRset
  208. del rrsets[rrset_new]
  209. exclude_from_update.append(rrset_old)
  210. # Do not process new RRsets that are empty (and did not exist before)
  211. # This is to avoid unnecessary pdns requests like (A: ...; AAAA: None)
  212. qs_delete = RRset.objects.filter(q_delete, domain=self)
  213. qs_delete_values = qs_delete.values_list('subname', 'type')
  214. # We modify the rrsets dictionary and thus loop over a copy of it
  215. for rrset, rrs in list(rrsets.items()):
  216. if rrs or (rrset.subname, rrset.type) in qs_delete_values:
  217. continue
  218. # RRset up for deletion does not exist
  219. del rrsets[rrset]
  220. # Clear or delete RRsets
  221. RR.objects.filter(rrset__in=qs_update).exclude(rrset__in=exclude_from_update).delete()
  222. RRset.objects.filter(q_delete, domain=self).delete()
  223. # Prepare and save new RRset contents
  224. # We modify the rrsets dictionary and thus loop over a copy of it
  225. for rrset, rrs in list(rrsets.items()):
  226. if not rrs:
  227. continue
  228. # (Create and) get correct RRset and update dictionary accordingly
  229. del rrsets[rrset]
  230. (rrset, _) = RRset.objects.get_or_create(domain=self,
  231. subname=rrset.subname,
  232. type=rrset.type,
  233. ttl=rrset.ttl)
  234. rrsets[rrset] = [RR(rrset=rrset, content=rr.content) for rr in rrs]
  235. RR.objects.bulk_create([rr for rrs in rrsets.values() for rr in rrs])
  236. # Send changed RRsets to pdns
  237. if rrsets and not self.owner.captcha_required:
  238. pdns.set_rrsets(self, rrsets)
  239. @transaction.atomic
  240. def delete(self, *args, **kwargs):
  241. # Delete delegation for dynDNS domains (direct child of dedyn.io)
  242. subname, parent_pdns_id = self.pdns_id.split('.', 1)
  243. if parent_pdns_id == 'dedyn.io.':
  244. try:
  245. parent = Domain.objects.get(name='dedyn.io')
  246. except Domain.DoesNotExist:
  247. pass
  248. else:
  249. rrsets = parent.rrset_set.filter(subname=subname,
  250. type__in=['NS', 'DS']).all()
  251. # Need to go RRset by RRset to trigger pdns sync
  252. # TODO can optimize using write_rrsets()
  253. for rrset in rrsets:
  254. rrset.delete()
  255. # Delete domain
  256. super().delete(*args, **kwargs)
  257. pdns.delete_zone(self)
  258. @transaction.atomic
  259. def save(self, *args, **kwargs):
  260. new = self.pk is None
  261. self.clean()
  262. super().save(*args, **kwargs)
  263. if new and not self.owner.captcha_required:
  264. self.deploy()
  265. def __str__(self):
  266. """
  267. Return domain name. Needed for serialization via StringRelatedField.
  268. (Must be unique.)
  269. """
  270. return self.name
  271. class Meta:
  272. ordering = ('created',)
  273. def get_default_value_created():
  274. return timezone.now()
  275. def get_default_value_due():
  276. return timezone.now() + datetime.timedelta(days=7)
  277. def get_default_value_mref():
  278. return "ONDON" + str((timezone.now() - timezone.datetime(1970,1,1,tzinfo=timezone.utc)).total_seconds())
  279. class Donation(models.Model):
  280. created = models.DateTimeField(default=get_default_value_created)
  281. name = models.CharField(max_length=255)
  282. iban = models.CharField(max_length=34)
  283. bic = models.CharField(max_length=11)
  284. amount = models.DecimalField(max_digits=8,decimal_places=2)
  285. message = models.CharField(max_length=255, blank=True)
  286. due = models.DateTimeField(default=get_default_value_due)
  287. mref = models.CharField(max_length=32,default=get_default_value_mref)
  288. email = models.EmailField(max_length=255, blank=True)
  289. def save(self, *args, **kwargs):
  290. self.iban = self.iban[:6] + "xxx" # do NOT save account details
  291. super().save(*args, **kwargs) # Call the "real" save() method.
  292. class Meta:
  293. ordering = ('created',)
  294. def validate_upper(value):
  295. if value != value.upper():
  296. raise ValidationError('Invalid value (not uppercase): %(value)s',
  297. code='invalid',
  298. params={'value': value})
  299. class RRset(models.Model, mixins.SetterMixin):
  300. id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
  301. created = models.DateTimeField(auto_now_add=True)
  302. updated = models.DateTimeField(null=True)
  303. domain = models.ForeignKey(Domain, on_delete=models.CASCADE)
  304. subname = models.CharField(max_length=178, blank=True)
  305. type = models.CharField(max_length=10, validators=[validate_upper])
  306. ttl = models.PositiveIntegerField(validators=[MinValueValidator(1)])
  307. _dirty = False
  308. RESTRICTED_TYPES = ('SOA', 'RRSIG', 'DNSKEY', 'NSEC3PARAM')
  309. class Meta:
  310. unique_together = (("domain","subname","type"),)
  311. def __init__(self, *args, **kwargs):
  312. self._dirties = set()
  313. super().__init__(*args, **kwargs)
  314. def setter_domain(self, val):
  315. if val != self.domain:
  316. self._dirties.add('domain')
  317. return val
  318. def setter_subname(self, val):
  319. # On PUT, RRsetSerializer sends None, denoting the unchanged value
  320. if val is None:
  321. return self.subname
  322. if val != self.subname:
  323. self._dirties.add('subname')
  324. return val
  325. def setter_type(self, val):
  326. if val != self.type:
  327. self._dirties.add('type')
  328. return val
  329. def setter_ttl(self, val):
  330. if val != self.ttl:
  331. self._dirties.add('ttl')
  332. return val
  333. def clean(self):
  334. errors = {}
  335. for field in (self._dirties & {'domain', 'subname', 'type'}):
  336. errors[field] = ValidationError(
  337. 'You cannot change the `%s` field.' % field)
  338. if errors:
  339. raise ValidationError(errors)
  340. def get_dirties(self):
  341. return self._dirties
  342. @property
  343. def name(self):
  344. return '.'.join(filter(None, [self.subname, self.domain.name])) + '.'
  345. @transaction.atomic
  346. def set_rrs(self, contents, sync=True, notify=True):
  347. self.records.all().delete()
  348. self.records.set([RR(content=x) for x in contents], bulk=False)
  349. if sync and not self.domain.owner.captcha_required:
  350. pdns.set_rrset(self, notify=notify)
  351. @transaction.atomic
  352. def delete(self, *args, **kwargs):
  353. assert not self.domain.owner.captcha_required
  354. super().delete(*args, **kwargs)
  355. pdns.set_rrset(self)
  356. self._dirties = {}
  357. def save(self, *args, **kwargs):
  358. # If not new, the only thing that can change is the TTL
  359. if self.created is None or 'ttl' in self.get_dirties():
  360. self.updated = timezone.now()
  361. self.full_clean()
  362. super().save(*args, **kwargs)
  363. self._dirties = {}
  364. class RR(models.Model):
  365. created = models.DateTimeField(auto_now_add=True)
  366. rrset = models.ForeignKey(RRset, on_delete=models.CASCADE, related_name='records')
  367. # max_length is determined based on the calculation in
  368. # https://lists.isc.org/pipermail/bind-users/2008-April/070148.html
  369. content = models.CharField(max_length=4092)