models.py 19 KB

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