models.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  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
  8. from django.core.validators import MinValueValidator
  9. from rest_framework.authtoken.models import Token
  10. from collections import Counter
  11. class MyUserManager(BaseUserManager):
  12. def create_user(self, email, password=None, registration_remote_ip=None, captcha_required=False, dyn=False):
  13. """
  14. Creates and saves a User with the given email, date of
  15. birth and password.
  16. """
  17. if not email:
  18. raise ValueError('Users must have an email address')
  19. user = self.model(
  20. email=self.normalize_email(email),
  21. registration_remote_ip=registration_remote_ip,
  22. captcha_required=captcha_required,
  23. dyn=dyn,
  24. )
  25. user.set_password(password)
  26. user.save(using=self._db)
  27. return user
  28. def create_superuser(self, email, password):
  29. """
  30. Creates and saves a superuser with the given email, date of
  31. birth and password.
  32. """
  33. user = self.create_user(email,
  34. password=password
  35. )
  36. user.is_admin = True
  37. user.save(using=self._db)
  38. return user
  39. class User(AbstractBaseUser):
  40. email = models.EmailField(
  41. verbose_name='email address',
  42. max_length=191,
  43. unique=True,
  44. )
  45. is_active = models.BooleanField(default=True)
  46. is_admin = models.BooleanField(default=False)
  47. registration_remote_ip = models.CharField(max_length=1024, blank=True)
  48. captcha_required = models.BooleanField(default=False)
  49. created = models.DateTimeField(auto_now_add=True)
  50. limit_domains = models.IntegerField(default=settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT,null=True,blank=True)
  51. dyn = models.BooleanField(default=False)
  52. objects = MyUserManager()
  53. USERNAME_FIELD = 'email'
  54. REQUIRED_FIELDS = []
  55. def get_full_name(self):
  56. return self.email
  57. def get_short_name(self):
  58. return self.email
  59. def get_token(self):
  60. token, created = Token.objects.get_or_create(user=self)
  61. return token.key
  62. def __str__(self):
  63. return self.email
  64. def has_perm(self, perm, obj=None):
  65. "Does the user have a specific permission?"
  66. # Simplest possible answer: Yes, always
  67. return True
  68. def has_module_perms(self, app_label):
  69. "Does the user have permissions to view the app `app_label`?"
  70. # Simplest possible answer: Yes, always
  71. return True
  72. @property
  73. def is_staff(self):
  74. "Is the user a member of staff?"
  75. # Simplest possible answer: All admins are staff
  76. return self.is_admin
  77. def unlock(self):
  78. self.captcha_required = False
  79. for domain in self.domains.all():
  80. domain.sync_to_pdns()
  81. self.save()
  82. class Domain(models.Model, mixins.SetterMixin):
  83. created = models.DateTimeField(auto_now_add=True)
  84. name = models.CharField(max_length=191, unique=True)
  85. owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='domains')
  86. _dirtyName = False
  87. _ns_records_data = [{'content': 'ns1.desec.io.'},
  88. {'content': 'ns2.desec.io.'}]
  89. def setter_name(self, val):
  90. if val != self.name:
  91. self._dirtyName = True
  92. return val
  93. def clean(self):
  94. if self._dirtyName:
  95. raise ValidationError('You must not change the domain name')
  96. @property
  97. def keys(self):
  98. return pdns.get_keys(self)
  99. @property
  100. def pdns_id(self):
  101. if '/' in self.name or '?' in self.name:
  102. raise SuspiciousOperation('Invalid hostname ' + self.name)
  103. # Transform to be valid pdns API identifiers (:id in their docs). The
  104. # '/' case here is just a safety measure (this case should never occur due
  105. # to the above check).
  106. # See also pdns code, apiZoneNameToId() in ws-api.cc
  107. name = self.name.translate(str.maketrans({'/': '=2F', '_': '=5F'}))
  108. if not name.endswith('.'):
  109. name += '.'
  110. return name
  111. # When this is made a property, looping over Domain.rrsets breaks
  112. def get_rrsets(self):
  113. return RRset.objects.filter(domain=self)
  114. def _create_pdns_zone(self):
  115. """
  116. Create zone on pdns. This will also import any RRsets that may have
  117. been created already.
  118. """
  119. pdns.create_zone(self, settings.DEFAULT_NS)
  120. # Import RRsets that may have been created (e.g. during captcha lock).
  121. # Don't perform if we do not know of any RRsets (it would delete all
  122. # existing records from pdns).
  123. rrsets = self.get_rrsets()
  124. if rrsets:
  125. pdns.set_rrsets(self, rrsets)
  126. # Make our RRsets consistent with pdns (specifically, NS may exist)
  127. self.sync_from_pdns()
  128. def sync_to_pdns(self):
  129. """
  130. Make sure that pdns gets the latest information about this domain/zone.
  131. Re-Syncing is relatively expensive and should not happen routinely.
  132. """
  133. # Try to create zone, in case it does not exist yet
  134. try:
  135. self._create_pdns_zone()
  136. except pdns.PdnsException as e:
  137. if (e.status_code == 422 and e.detail.endswith(' already exists')):
  138. # Zone exists, purge it by deleting all RRsets and sync
  139. pdns.set_rrsets(self, [], notify=False)
  140. pdns.set_rrsets(self, self.get_rrsets())
  141. else:
  142. raise e
  143. @transaction.atomic
  144. def sync_from_pdns(self):
  145. RRset.objects.filter(domain=self).delete()
  146. rrset_datas = [rrset_data for rrset_data in pdns.get_rrset_datas(self)
  147. if rrset_data['type'] not in RRset.RESTRICTED_TYPES]
  148. # Can't do bulk create because we need records creation in RRset.save()
  149. for rrset_data in rrset_datas:
  150. RRset(**rrset_data).save(sync=False)
  151. @transaction.atomic
  152. def set_rrsets(self, rrsets):
  153. """
  154. Writes the provided RRsets to the database, overriding any existing
  155. RRsets of the same subname and type. If the user account is not locked
  156. for captcha, also inform pdns about the new RRsets.
  157. """
  158. for rrset in rrsets:
  159. if rrset.domain != self:
  160. raise ValueError(
  161. 'Cannot set RRset for domain %s on domain %s.' % (
  162. rrset.domain.name, self.name))
  163. if rrset.type in RRset.RESTRICTED_TYPES:
  164. raise ValueError(
  165. 'You cannot tinker with the %s RRset.' % rrset.type)
  166. pdns_rrsets = []
  167. for rrset in rrsets:
  168. # Look up old RRset to see if it needs updating. If exists and
  169. # outdated, delete it so that we can bulk-create it later.
  170. try:
  171. old_rrset = self.rrset_set.get(subname=rrset.subname,
  172. type=rrset.type)
  173. old_rrset.ttl = rrset.ttl
  174. old_rrset.records_data = rrset.records_data
  175. rrset = old_rrset
  176. except RRset.DoesNotExist:
  177. pass
  178. # At this point, rrset is an RRset to be created or possibly to be
  179. # updated. RRset.save() will decide what to write to the database.
  180. if rrset.pk is None or 'records' in rrset.get_dirties():
  181. pdns_rrsets.append(rrset)
  182. rrset.save(sync=False)
  183. if not self.owner.captcha_required:
  184. pdns.set_rrsets(self, pdns_rrsets)
  185. @transaction.atomic
  186. def delete(self, *args, **kwargs):
  187. # Delete delegation for dynDNS domains (direct child of dedyn.io)
  188. subname, parent_pdns_id = self.pdns_id.split('.', 1)
  189. if parent_pdns_id == 'dedyn.io.':
  190. parent = Domain.objects.filter(name='dedyn.io').first()
  191. if parent:
  192. rrsets = RRset.objects.filter(domain=parent, subname=subname,
  193. type__in=['NS', 'DS']).all()
  194. for rrset in rrsets:
  195. rrset.records_data = []
  196. parent.set_rrsets(rrsets)
  197. # Delete domain
  198. super().delete(*args, **kwargs)
  199. pdns.delete_zone(self)
  200. def save(self, *args, **kwargs):
  201. with transaction.atomic():
  202. new = self.pk is None
  203. self.clean()
  204. super().save(*args, **kwargs)
  205. if new and not self.owner.captcha_required:
  206. self._create_pdns_zone()
  207. if not new:
  208. return
  209. # If the domain is a direct subdomain of dedyn.io, set NS records in
  210. # parent. Don't notify slaves (we first have to enable DNSSEC).
  211. subname, parent_pdns_id = self.pdns_id.split('.', 1)
  212. if parent_pdns_id == 'dedyn.io.':
  213. parent = Domain.objects.filter(name='dedyn.io').first()
  214. if parent:
  215. records_data = [('content', x) for x in settings.DEFAULT_NS]
  216. rrset = RRset(domain=parent, subname=subname, type='NS',
  217. ttl=60, records_data=records_data)
  218. rrset.save(notify=False)
  219. def __str__(self):
  220. """
  221. Return domain name. Needed for serialization via StringRelatedField.
  222. (Must be unique.)
  223. """
  224. return self.name
  225. class Meta:
  226. ordering = ('created',)
  227. def get_default_value_created():
  228. return timezone.now()
  229. def get_default_value_due():
  230. return timezone.now() + datetime.timedelta(days=7)
  231. def get_default_value_mref():
  232. return "ONDON" + str((timezone.now() - timezone.datetime(1970,1,1,tzinfo=timezone.utc)).total_seconds())
  233. class Donation(models.Model):
  234. created = models.DateTimeField(default=get_default_value_created)
  235. name = models.CharField(max_length=255)
  236. iban = models.CharField(max_length=34)
  237. bic = models.CharField(max_length=11)
  238. amount = models.DecimalField(max_digits=8,decimal_places=2)
  239. message = models.CharField(max_length=255, blank=True)
  240. due = models.DateTimeField(default=get_default_value_due)
  241. mref = models.CharField(max_length=32,default=get_default_value_mref)
  242. email = models.EmailField(max_length=255, blank=True)
  243. def save(self, *args, **kwargs):
  244. self.iban = self.iban[:6] + "xxx" # do NOT save account details
  245. super().save(*args, **kwargs) # Call the "real" save() method.
  246. class Meta:
  247. ordering = ('created',)
  248. def validate_upper(value):
  249. if value != value.upper():
  250. raise ValidationError('Invalid value (not uppercase): %(value)s',
  251. code='invalid',
  252. params={'value': value})
  253. class RRset(models.Model, mixins.SetterMixin):
  254. created = models.DateTimeField(auto_now_add=True)
  255. updated = models.DateTimeField(null=True)
  256. domain = models.ForeignKey(Domain, on_delete=models.CASCADE)
  257. subname = models.CharField(max_length=178, blank=True)
  258. type = models.CharField(max_length=10, validators=[validate_upper])
  259. ttl = models.PositiveIntegerField(validators=[MinValueValidator(1)])
  260. _dirty = False
  261. RESTRICTED_TYPES = ('SOA', 'RRSIG', 'DNSKEY', 'NSEC3PARAM')
  262. class Meta:
  263. unique_together = (("domain","subname","type"),)
  264. def __init__(self, *args, records_data=None, **kwargs):
  265. self.records_data = records_data
  266. self._dirties = set()
  267. super().__init__(*args, **kwargs)
  268. def setter_domain(self, val):
  269. if val != self.domain:
  270. self._dirties.add('domain')
  271. return val
  272. def setter_subname(self, val):
  273. # On PUT, RRsetSerializer sends None, denoting the unchanged value
  274. if val is None:
  275. return self.subname
  276. if val != self.subname:
  277. self._dirties.add('subname')
  278. return val
  279. def setter_type(self, val):
  280. if val != self.type:
  281. self._dirties.add('type')
  282. return val
  283. def setter_ttl(self, val):
  284. if val != self.ttl:
  285. self._dirties.add('ttl')
  286. return val
  287. def clean(self):
  288. errors = {}
  289. for field in (self._dirties & {'domain', 'subname', 'type'}):
  290. errors[field] = ValidationError(
  291. 'You cannot change the `%s` field.' % field)
  292. if errors:
  293. raise ValidationError(errors)
  294. def get_dirties(self):
  295. if self.records_data is not None and 'records' not in self._dirties \
  296. and (self.pk is None
  297. or Counter([x['content'] for x in self.records_data])
  298. != Counter(self.records.values_list('content', flat=True))
  299. ):
  300. self._dirties.add('records')
  301. return self._dirties
  302. @property
  303. def name(self):
  304. return '.'.join(filter(None, [self.subname, self.domain.name])) + '.'
  305. @transaction.atomic
  306. def delete(self, *args, **kwargs):
  307. super().delete(*args, **kwargs)
  308. pdns.set_rrset(self)
  309. self.records_data = None
  310. self._dirties = {}
  311. @transaction.atomic
  312. def save(self, sync=True, notify=True, *args, **kwargs):
  313. new = self.pk is None
  314. # Empty records data means deletion
  315. if self.records_data == []:
  316. if not new:
  317. self.delete()
  318. return
  319. # The only thing that can change is the TTL
  320. if new or 'ttl' in self.get_dirties():
  321. self.updated = timezone.now()
  322. self.full_clean()
  323. super().save(*args, **kwargs)
  324. # Create RRset contents
  325. if 'records' in self.get_dirties():
  326. self.records.all().delete()
  327. records = [RR(rrset=self, **data) for data in self.records_data]
  328. self.records.bulk_create(records)
  329. self.records_data = None
  330. # Sync to pdns if new or anything is dirty
  331. if sync and not self.domain.owner.captcha_required \
  332. and (new or self.get_dirties()):
  333. pdns.set_rrset(self, notify=notify)
  334. self._dirties = {}
  335. class RR(models.Model):
  336. created = models.DateTimeField(auto_now_add=True)
  337. rrset = models.ForeignKey(RRset, on_delete=models.CASCADE, related_name='records')
  338. # max_length is determined based on the calculation in
  339. # https://lists.isc.org/pipermail/bind-users/2008-April/070148.html
  340. content = models.CharField(max_length=4092)