test_domains.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. from django.conf import settings
  2. from django.core import mail
  3. from django.core.exceptions import ValidationError
  4. from psl_dns.exceptions import UnsupportedRule
  5. from rest_framework import status
  6. from desecapi.models import Domain
  7. from desecapi.pdns_change_tracker import PDNSChangeTracker
  8. from desecapi.tests.base import DesecTestCase, DomainOwnerTestCase
  9. class UnauthenticatedDomainTests(DesecTestCase):
  10. def test_unauthorized_access(self):
  11. for url in [
  12. self.reverse('v1:domain-list'),
  13. self.reverse('v1:domain-detail', name='example.com.')
  14. ]:
  15. for method in [self.client.put, self.client.delete]:
  16. self.assertStatus(method(url), status.HTTP_401_UNAUTHORIZED)
  17. class DomainOwnerTestCase1(DomainOwnerTestCase):
  18. def test_name_validity(self):
  19. for name in [
  20. 'FOO.BAR.com',
  21. 'tEst.dedyn.io',
  22. 'ORG',
  23. '--BLAH.example.com',
  24. '_ASDF.jp',
  25. ]:
  26. with self.assertRaises(ValidationError):
  27. Domain(owner=self.owner, name=name).save()
  28. for name in [
  29. '_example.com', '_.example.com',
  30. '-dedyn.io', '--dedyn.io', '-.dedyn123.io',
  31. 'foobar.io', 'exam_ple.com',
  32. ]:
  33. with self.assertPdnsRequests(
  34. self.requests_desec_domain_creation(name=name)[:-1] # no serializer, no cryptokeys API call
  35. ), PDNSChangeTracker():
  36. Domain(owner=self.owner, name=name).save()
  37. def test_list_domains(self):
  38. with self.assertPdnsNoRequestsBut(self.request_pdns_zone_retrieve_crypto_keys()):
  39. response = self.client.get(self.reverse('v1:domain-list'))
  40. self.assertStatus(response, status.HTTP_200_OK)
  41. self.assertEqual(len(response.data), self.NUM_OWNED_DOMAINS)
  42. for i in range(self.NUM_OWNED_DOMAINS):
  43. self.assertEqual(response.data[i]['name'], self.my_domains[i].name)
  44. def test_delete_my_domain(self):
  45. url = self.reverse('v1:domain-detail', name=self.my_domain.name)
  46. with self.assertPdnsRequests(self.requests_desec_domain_deletion()):
  47. response = self.client.delete(url)
  48. self.assertStatus(response, status.HTTP_204_NO_CONTENT)
  49. self.assertFalse(Domain.objects.filter(pk=self.my_domain.pk).exists())
  50. response = self.client.get(url)
  51. self.assertStatus(response, status.HTTP_404_NOT_FOUND)
  52. def test_delete_other_domain(self):
  53. url = self.reverse('v1:domain-detail', name=self.other_domain.name)
  54. response = self.client.delete(url)
  55. self.assertStatus(response, status.HTTP_204_NO_CONTENT)
  56. self.assertTrue(Domain.objects.filter(pk=self.other_domain.pk).exists())
  57. def test_retrieve_my_domain(self):
  58. url = self.reverse('v1:domain-detail', name=self.my_domain.name)
  59. with self.assertPdnsRequests(
  60. self.request_pdns_zone_retrieve_crypto_keys(name=self.my_domain.name)
  61. ):
  62. response = self.client.get(url)
  63. self.assertStatus(response, status.HTTP_200_OK)
  64. self.assertEqual(response.data['name'], self.my_domain.name)
  65. self.assertTrue(isinstance(response.data['keys'], list))
  66. def test_retrieve_other_domains(self):
  67. for domain in self.other_domains:
  68. response = self.client.get(self.reverse('v1:domain-detail', name=domain.name))
  69. self.assertStatus(response, status.HTTP_404_NOT_FOUND)
  70. def test_update_my_domain_name(self):
  71. url = self.reverse('v1:domain-detail', name=self.my_domain.name)
  72. with self.assertPdnsRequests(self.request_pdns_zone_retrieve_crypto_keys(name=self.my_domain.name)):
  73. response = self.client.get(url)
  74. self.assertStatus(response, status.HTTP_200_OK)
  75. response.data['name'] = self.random_domain_name()
  76. response = self.client.put(url, response.data, format='json')
  77. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  78. with self.assertPdnsRequests(self.request_pdns_zone_retrieve_crypto_keys(name=self.my_domain.name)):
  79. response = self.client.get(url)
  80. self.assertStatus(response, status.HTTP_200_OK)
  81. self.assertEqual(response.data['name'], self.my_domain.name)
  82. def test_update_my_domain_immutable(self):
  83. url = self.reverse('v1:domain-detail', name=self.my_domain.name)
  84. with self.assertPdnsRequests(self.request_pdns_zone_retrieve_crypto_keys(name=self.my_domain.name)):
  85. response = self.client.get(url)
  86. self.assertStatus(response, status.HTTP_200_OK)
  87. created = response.data['created']
  88. keys = response.data['keys']
  89. published = response.data['published']
  90. response.data['created'] = '2019-08-07T18:34:39.249227Z'
  91. response.data['published'] = '2019-08-07T18:34:39.249227Z'
  92. response.data['keys'] = [{'dnskey': '257 3 13 badefefe'}]
  93. self.assertNotEqual(response.data['created'], created)
  94. self.assertNotEqual(response.data['published'], published)
  95. self.assertNotEqual(response.data['keys'], keys)
  96. with self.assertPdnsRequests(self.request_pdns_zone_retrieve_crypto_keys(name=self.my_domain.name)):
  97. response = self.client.put(url, response.data, format='json')
  98. self.assertStatus(response, status.HTTP_200_OK)
  99. self.assertEqual(response.data['created'], created)
  100. self.assertEqual(response.data['published'], published)
  101. self.assertEqual(response.data['keys'], keys)
  102. def test_update_other_domains(self):
  103. url = self.reverse('v1:domain-detail', name=self.other_domain.name)
  104. response = self.client.put(url, {}, format='json')
  105. self.assertStatus(response, status.HTTP_404_NOT_FOUND)
  106. def test_create_domains(self):
  107. self.owner.limit_domains = 100
  108. self.owner.save()
  109. for name in [
  110. '0.8.0.0.0.1.c.a.2.4.6.0.c.e.e.d.4.4.0.1.a.0.1.0.8.f.4.0.1.0.a.2.ip6.arpa',
  111. 'very.long.domain.name.' + self.random_domain_name(),
  112. self.random_domain_name(),
  113. 'very.long.domain.name.with_underscore.' + self.random_domain_name(),
  114. ]:
  115. with self.assertPdnsRequests(self.requests_desec_domain_creation(name)):
  116. response = self.client.post(self.reverse('v1:domain-list'), {'name': name})
  117. self.assertStatus(response, status.HTTP_201_CREATED)
  118. self.assertEqual(len(mail.outbox), 0)
  119. with self.assertPdnsRequests(self.request_pdns_zone_retrieve_crypto_keys(name)):
  120. self.assertStatus(
  121. self.client.get(self.reverse('v1:domain-detail', name=name), {'name': name}),
  122. status.HTTP_200_OK
  123. )
  124. response = self.client.get_rr_sets(name, type='NS', subname='')
  125. self.assertStatus(response, status.HTTP_200_OK)
  126. self.assertContainsRRSets(response.data, [dict(subname='', records=settings.DEFAULT_NS, type='NS')])
  127. def test_create_api_known_domain(self):
  128. url = self.reverse('v1:domain-list')
  129. for name in [
  130. self.random_domain_name(),
  131. 'www.' + self.my_domain.name,
  132. ]:
  133. with self.assertPdnsRequests(self.requests_desec_domain_creation(name)):
  134. response = self.client.post(url, {'name': name})
  135. self.assertStatus(response, status.HTTP_201_CREATED)
  136. response = self.client.post(url, {'name': name})
  137. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  138. def test_create_domain_with_whitespace(self):
  139. for name in [
  140. ' ' + self.random_domain_name(),
  141. self.random_domain_name() + ' ',
  142. ]:
  143. self.assertResponse(
  144. self.client.post(self.reverse('v1:domain-list'), {'name': name}),
  145. status.HTTP_400_BAD_REQUEST,
  146. {'name': ['Invalid value (not a DNS name).']},
  147. )
  148. def test_create_public_suffixes(self):
  149. for name in self.PUBLIC_SUFFIXES:
  150. response = self.client.post(self.reverse('v1:domain-list'), {'name': name})
  151. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  152. self.assertEqual(response.data['name'][0].code, 'name_unavailable')
  153. def test_create_domain_under_public_suffix_with_private_parent(self):
  154. name = 'amazonaws.com'
  155. with self.assertPdnsRequests(self.requests_desec_domain_creation(name)[:-1]), PDNSChangeTracker():
  156. Domain(owner=self.create_user(), name=name).save()
  157. self.assertTrue(Domain.objects.filter(name=name).exists())
  158. # If amazonaws.com is owned by another user, we cannot register test.s4.amazonaws.com
  159. name = 'test.s4.amazonaws.com'
  160. response = self.client.post(self.reverse('v1:domain-list'), {'name': name})
  161. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  162. self.assertEqual(response.data['name'][0].code, 'name_unavailable')
  163. # s3.amazonaws.com is a public suffix. Therefore, test.s3.amazonaws.com can be
  164. # registered even if the parent zone amazonaws.com is owned by another user
  165. name = 'test.s3.amazonaws.com'
  166. psl_cm = self.get_psl_context_manager('s3.amazonaws.com')
  167. with psl_cm, self.assertPdnsRequests(self.requests_desec_domain_creation(name)):
  168. response = self.client.post(self.reverse('v1:domain-list'), {'name': name})
  169. self.assertStatus(response, status.HTTP_201_CREATED)
  170. def test_create_domain_under_unsupported_public_suffix_rule(self):
  171. # Show lenience if the PSL library produces an UnsupportedRule exception
  172. name = 'unsupported.wildcard.test'
  173. psl_cm = self.get_psl_context_manager(UnsupportedRule)
  174. with psl_cm, self.assertPdnsRequests():
  175. response = self.client.post(self.reverse('v1:domain-list'), {'name': name})
  176. self.assertStatus(response, status.HTTP_503_SERVICE_UNAVAILABLE)
  177. def test_create_domain_policy(self):
  178. name = '*.' + self.random_domain_name()
  179. response = self.client.post(self.reverse('v1:domain-list'), {'name': name})
  180. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  181. self.assertTrue("Invalid value (not a DNS name)." in response.data['name'][0])
  182. def test_create_domain_other_parent(self):
  183. name = 'something.' + self.other_domain.name
  184. response = self.client.post(self.reverse('v1:domain-list'), {'name': name})
  185. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  186. self.assertEqual(response.data['name'][0].code, 'name_unavailable')
  187. def test_create_domain_atomicity(self):
  188. name = self.random_domain_name()
  189. with self.assertPdnsRequests(self.request_pdns_zone_create_422()):
  190. self.client.post(self.reverse('v1:domain-list'), {'name': name})
  191. self.assertFalse(Domain.objects.filter(name=name).exists())
  192. def test_create_domain_punycode(self):
  193. names = ['公司.cn', 'aéroport.ci']
  194. for name in names:
  195. self.assertStatus(
  196. self.client.post(self.reverse('v1:domain-list'), {'name': name}),
  197. status.HTTP_400_BAD_REQUEST
  198. )
  199. for name in [n.encode('idna').decode('ascii') for n in names]:
  200. with self.assertPdnsRequests(self.requests_desec_domain_creation(name=name)):
  201. self.assertStatus(
  202. self.client.post(self.reverse('v1:domain-list'), {'name': name}),
  203. status.HTTP_201_CREATED
  204. )
  205. def test_create_domain_name_validation(self):
  206. for name in [
  207. 'with space.dedyn.io',
  208. 'another space.de',
  209. ' spaceatthebeginning.com',
  210. 'percentage%sign.com',
  211. '%percentagesign.dedyn.io',
  212. 'slash/desec.io',
  213. '/slashatthebeginning.dedyn.io',
  214. '\\backslashatthebeginning.dedyn.io',
  215. 'backslash\\inthemiddle.at',
  216. '@atsign.com',
  217. 'at@sign.com',
  218. ]:
  219. response = self.client.post(self.reverse('v1:domain-list'), {'name': name})
  220. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  221. self.assertEqual(len(mail.outbox), 0)
  222. def test_domain_minimum_ttl(self):
  223. url = self.reverse('v1:domain-list')
  224. name = self.random_domain_name()
  225. with self.assertPdnsRequests(self.requests_desec_domain_creation(name=name)):
  226. response = self.client.post(url, {'name': name})
  227. self.assertStatus(response, status.HTTP_201_CREATED)
  228. self.assertEqual(response.data['minimum_ttl'], settings.MINIMUM_TTL_DEFAULT)
  229. class AutoDelegationDomainOwnerTests(DomainOwnerTestCase):
  230. DYN = True
  231. def test_delete_my_domain(self):
  232. url = self.reverse('v1:domain-detail', name=self.my_domain.name)
  233. with self.assertPdnsRequests(
  234. self.requests_desec_domain_deletion_auto_delegation(name=self.my_domain.name)
  235. ):
  236. response = self.client.delete(url)
  237. self.assertStatus(response, status.HTTP_204_NO_CONTENT)
  238. response = self.client.get(url)
  239. self.assertStatus(response, status.HTTP_404_NOT_FOUND)
  240. def test_delete_other_domains(self):
  241. url = self.reverse('v1:domain-detail', name=self.other_domain.name)
  242. with self.assertPdnsRequests():
  243. response = self.client.delete(url)
  244. self.assertStatus(response, status.HTTP_204_NO_CONTENT)
  245. self.assertTrue(Domain.objects.filter(pk=self.other_domain.pk).exists())
  246. def test_create_auto_delegated_domains(self):
  247. for i, suffix in enumerate(self.AUTO_DELEGATION_DOMAINS):
  248. name = self.random_domain_name(suffix)
  249. with self.assertPdnsRequests(self.requests_desec_domain_creation_auto_delegation(name=name)):
  250. response = self.client.post(self.reverse('v1:domain-list'), {'name': name})
  251. self.assertStatus(response, status.HTTP_201_CREATED)
  252. self.assertEqual(len(mail.outbox), i + 1)
  253. email = str(mail.outbox[0].message())
  254. self.assertTrue(name in email)
  255. self.assertTrue(self.token.key in email)
  256. self.assertFalse(self.user.plain_password in email)
  257. def test_domain_limit(self):
  258. url = self.reverse('v1:domain-list')
  259. user_quota = settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT - self.NUM_OWNED_DOMAINS
  260. for i in range(user_quota):
  261. name = self.random_domain_name(self.AUTO_DELEGATION_DOMAINS)
  262. with self.assertPdnsRequests(self.requests_desec_domain_creation_auto_delegation(name)):
  263. response = self.client.post(url, {'name': name})
  264. self.assertStatus(response, status.HTTP_201_CREATED)
  265. self.assertEqual(len(mail.outbox), i + 1)
  266. response = self.client.post(url, {'name': self.random_domain_name(self.AUTO_DELEGATION_DOMAINS)})
  267. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  268. self.assertEqual(response.data['non_field_errors'][0].code, 'domain_limit')
  269. self.assertEqual(len(mail.outbox), user_quota)
  270. def test_domain_minimum_ttl(self):
  271. url = self.reverse('v1:domain-list')
  272. name = self.random_domain_name(self.AUTO_DELEGATION_DOMAINS)
  273. with self.assertPdnsRequests(self.requests_desec_domain_creation_auto_delegation(name)):
  274. response = self.client.post(url, {'name': name})
  275. self.assertStatus(response, status.HTTP_201_CREATED)
  276. self.assertEqual(response.data['minimum_ttl'], 60)