From cf7242ce29d78c2082a728d9a3d997d4cdaa4f35 Mon Sep 17 00:00:00 2001 From: Nils Wisiol Date: Sat, 30 Jan 2021 15:14:22 +0100 Subject: [PATCH 1/9] feat(webapp): removes margin around domain, token, RR set lists --- webapp/src/views/CrudList.vue | 588 +++++++++++++++++----------------- 1 file changed, 291 insertions(+), 297 deletions(-) diff --git a/webapp/src/views/CrudList.vue b/webapp/src/views/CrudList.vue index 0e6bf0370..9a04667fe 100644 --- a/webapp/src/views/CrudList.vue +++ b/webapp/src/views/CrudList.vue @@ -1,336 +1,330 @@ + + \ No newline at end of file diff --git a/webapp/src/router/index.js b/webapp/src/router/index.js index 907072f52..80803b3fb 100644 --- a/webapp/src/router/index.js +++ b/webapp/src/router/index.js @@ -123,6 +123,12 @@ const routes = [ component: () => import(/* webpackChunkName: "gui" */ '../views/Domain/CrudDomain.vue'), meta: {guest: false}, }, + { + path: '/dane', + name: 'dane', + component: () => import(/* webpackChunkName: "gui" */ '../views/DaneHome.vue'), + meta: {guest: false}, + }, ] const router = new VueRouter({ diff --git a/webapp/src/views/CrudList.vue b/webapp/src/views/CrudList.vue index 9a04667fe..f3c79d3a5 100644 --- a/webapp/src/views/CrudList.vue +++ b/webapp/src/views/CrudList.vue @@ -338,6 +338,7 @@ import Record from '@/components/Field/Record'; import RecordList from '@/components/Field/RecordList'; import Switchbox from '@/components/Field/Switchbox'; import TTL from '@/components/Field/TTL'; +import MultilineText from "@/components/Field/MultilineText"; const filter = function (obj, predicate) { const result = {}; @@ -366,6 +367,7 @@ export default { Record, RecordList, TTL, + MultilineText, }, data() { return { createDialog: false, diff --git a/webapp/src/views/DaneHome.vue b/webapp/src/views/DaneHome.vue new file mode 100644 index 000000000..d7c2019f3 --- /dev/null +++ b/webapp/src/views/DaneHome.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/webapp/src/views/TLSIdentityList.vue b/webapp/src/views/TLSIdentityList.vue new file mode 100644 index 000000000..49b304148 --- /dev/null +++ b/webapp/src/views/TLSIdentityList.vue @@ -0,0 +1,151 @@ + From 2f9e9103e3f4f755db7ebb49b0d1bb921a2786bc Mon Sep 17 00:00:00 2001 From: Nils Wisiol Date: Sun, 19 Sep 2021 22:05:12 +0200 Subject: [PATCH 4/9] streamline code --- api/desecapi/migrations/0015_identities.py | 29 ------ api/desecapi/migrations/0016_identities.py | 43 --------- .../migrations/0017_auto_20210213_1840.py | 33 ------- api/desecapi/migrations/0018_tlsidentity.py | 37 ++++++++ api/desecapi/models.py | 95 +++++++------------ 5 files changed, 70 insertions(+), 167 deletions(-) delete mode 100644 api/desecapi/migrations/0015_identities.py delete mode 100644 api/desecapi/migrations/0016_identities.py delete mode 100644 api/desecapi/migrations/0017_auto_20210213_1840.py create mode 100644 api/desecapi/migrations/0018_tlsidentity.py diff --git a/api/desecapi/migrations/0015_identities.py b/api/desecapi/migrations/0015_identities.py deleted file mode 100644 index ee4a33153..000000000 --- a/api/desecapi/migrations/0015_identities.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 3.1.5 on 2021-01-30 15:24 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('desecapi', '0014_replication'), - ] - - operations = [ - migrations.CreateModel( - name='TLSIdentity', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=24)), - ('created', models.DateTimeField(auto_now_add=True)), - ('certificate', models.TextField()), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='identities', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/api/desecapi/migrations/0016_identities.py b/api/desecapi/migrations/0016_identities.py deleted file mode 100644 index 814357238..000000000 --- a/api/desecapi/migrations/0016_identities.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 3.1.5 on 2021-01-31 13:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('desecapi', '0015_identities'), - ] - - operations = [ - migrations.AddField( - model_name='tlsidentity', - name='port', - field=models.IntegerField(default=443), - ), - migrations.AddField( - model_name='tlsidentity', - name='protocol', - field=models.TextField(choices=[('tcp', 'Tcp'), ('udp', 'Udp'), ('sctp', 'Sctp')], default='tcp'), - ), - migrations.AddField( - model_name='tlsidentity', - name='scheduled_removal', - field=models.DateTimeField(null=True), - ), - migrations.AddField( - model_name='tlsidentity', - name='tlsa_certificate_usage', - field=models.IntegerField(choices=[(0, 'Ca Constraint'), (1, 'Service Certificate Constraint'), (2, 'Trust Anchor Assertion'), (3, 'Domain Issued Certificate')], default=3), - ), - migrations.AddField( - model_name='tlsidentity', - name='tlsa_matching_type', - field=models.IntegerField(choices=[(0, 'No Hash Used'), (1, 'Sha256'), (2, 'Sha512')], default=1), - ), - migrations.AddField( - model_name='tlsidentity', - name='tlsa_selector', - field=models.IntegerField(choices=[(0, 'Full Certificate'), (1, 'Subject Public Key Info')], default=1), - ), - ] diff --git a/api/desecapi/migrations/0017_auto_20210213_1840.py b/api/desecapi/migrations/0017_auto_20210213_1840.py deleted file mode 100644 index 938e3359c..000000000 --- a/api/desecapi/migrations/0017_auto_20210213_1840.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.1.6 on 2021-02-13 18:40 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('desecapi', '0016_identities'), - ] - - operations = [ - migrations.AddField( - model_name='tlsidentity', - name='default_ttl', - field=models.PositiveIntegerField(default=300), - ), - migrations.AlterField( - model_name='tlsidentity', - name='tlsa_certificate_usage', - field=models.IntegerField(choices=[(0, 'Ca Constraint'), (1, 'Service Certificate Constraint'), (2, 'Trust Anchor Assertion'), (3, 'Domain Issued Certificate')], default=3), - ), - migrations.AlterField( - model_name='tlsidentity', - name='tlsa_matching_type', - field=models.IntegerField(choices=[(0, 'No Hash Used'), (1, 'Sha256'), (2, 'Sha512')], default=1), - ), - migrations.AlterField( - model_name='tlsidentity', - name='tlsa_selector', - field=models.IntegerField(choices=[(0, 'Full Certificate'), (1, 'Subject Public Key Info')], default=1), - ), - ] diff --git a/api/desecapi/migrations/0018_tlsidentity.py b/api/desecapi/migrations/0018_tlsidentity.py new file mode 100644 index 000000000..703c4b1a8 --- /dev/null +++ b/api/desecapi/migrations/0018_tlsidentity.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.7 on 2021-09-19 19:53 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('desecapi', '0017_alter_user_limit_domains'), + ] + + operations = [ + migrations.CreateModel( + name='TLSIdentity', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(default='', max_length=24)), + ('created', models.DateTimeField(auto_now_add=True)), + ('default_ttl', models.PositiveIntegerField(default=300)), + ('scheduled_removal', models.DateTimeField(null=True)), + ('certificate', models.TextField()), + ('tlsa_selector', models.IntegerField(choices=[(0, 'Full Certificate'), (1, 'Subject Public Key Info')], default=1)), + ('tlsa_matching_type', models.IntegerField(choices=[(0, 'No Hash Used'), (1, 'Sha256'), (2, 'Sha512')], default=1)), + ('tlsa_certificate_usage', models.IntegerField(choices=[(0, 'Ca Constraint'), (1, 'Service Certificate Constraint'), (2, 'Trust Anchor Assertion'), (3, 'Domain Issued Certificate')], default=3)), + ('port', models.IntegerField(default=443)), + ('protocol', models.TextField(choices=[('tcp', 'Tcp'), ('udp', 'Udp'), ('sctp', 'Sctp')], default='tcp')), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='identities', to=settings.AUTH_USER_MODEL)), + ('rrs', models.ManyToManyField(to='desecapi.RR')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/api/desecapi/models.py b/api/desecapi/models.py index c7117b21a..70c1cf0e3 100644 --- a/api/desecapi/models.py +++ b/api/desecapi/models.py @@ -219,6 +219,11 @@ def filter_qname(self, qname: str, **kwargs) -> models.query.QuerySet: name_length=Length('name'), ).filter(dotted_qname__endswith=F('dotted_name'), **kwargs) + def most_specific_zone(self, fqdn: str) -> Tuple[Domain, str]: + domain = self.filter_qname(fqdn).order_by('-name_length').first() + subname = fqdn[:-len(domain.name)].rstrip('.') + return domain, subname + class Domain(ExportModelOperationsMixin('Domain'), models.Model): @staticmethod @@ -994,37 +999,39 @@ class Identity(models.Model): created = models.DateTimeField(auto_now_add=True) owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='identities') default_ttl = models.PositiveIntegerField(default=300) + rrs = models.ManyToManyField(to=RR) + scheduled_removal = models.DateTimeField(null=True) class Meta: abstract = True - def get_record_contents(self) -> List[str]: - raise NotImplementedError - - def save_rrs(self): + def get_rrs(self) -> List[RR]: raise NotImplementedError def save(self, *args, **kwargs): - self.save_rrs() + for rr in self.get_rrs(): + self.rrs.add(rr) + rr.rrset.save() + rr.save() return super().save(*args, **kwargs) - def delete_rrs(self): - raise NotImplementedError - def delete(self, using=None, keep_parents=False): - # TODO this will delete also RRs that may be covered by other identities - self.delete_rrs() + for rr in self.rrs.all(): # TODO use one query + if len(rr.identities) == 1: + rr.delete() return super().delete(using, keep_parents) + # TODO move to RRset / RRset manager? def get_or_create_rr_set(self, domain: Domain, subname: str) -> RRset: try: return RRset.objects.get(domain=domain, subname=subname, type=self.rr_type) except RRset.DoesNotExist: - # TODO save this RRset? return RRset(domain=domain, subname=subname, type=self.rr_type, ttl=self.default_ttl) - @staticmethod - def get_or_create_rr(rrset: RRset, content: str) -> RR: + # TODO move to RR / RR manager? + def get_or_create_rr(self, fqdn: str, content: str) -> RR: + domain, subname = self.owner.domains.most_specific_zone(fqdn) + rrset = self.get_or_create_rr_set(domain, subname) try: return RR.objects.get(rrset=rrset, content=content) except RR.DoesNotExist: @@ -1064,8 +1071,6 @@ class Protocol(models.TextChoices): port = models.IntegerField(default=443) protocol = models.TextField(choices=Protocol.choices, default=Protocol.TCP) - scheduled_removal = models.DateTimeField(null=True) - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if 'not_valid_after' not in kwargs: @@ -1123,20 +1128,9 @@ def subject_names(self) -> Set[str]: return subject_names | subject_alternative_names - @staticmethod - def get_closest_ancestor(domain_name, owner: User) -> Optional[Domain]: - # TODO move to Domain? - labels = domain_name.split('.') - ancestor_names = ['.'.join(labels[i:]) for i in range(len(labels))] - for ancestor_name in ancestor_names: # TODO do this with one query - try: - return Domain.objects.get(name=ancestor_name, owner=owner) - except Domain.DoesNotExist: - continue - return None - - def domains_subnames(self) -> Set[Tuple[Domain, str]]: - domains_subnames = set() + @property + def subject_names_clean(self) -> Set[str]: + clean = set() for name in self.subject_names: # cut off any wildcard prefix name = name.lstrip('*').lstrip('.') @@ -1147,41 +1141,18 @@ def domains_subnames(self) -> Set[Tuple[Domain, str]]: except ValidationError: continue - # find user-owned parent domain - domain = self.get_closest_ancestor(name, self.owner) - if not domain: - continue - subname = name[:-len(domain.name)].rstrip('.') - - # return subname, domain pair - domains_subnames.add((domain, f"_{self.port:n}._{self.protocol}.{subname}".rstrip('.'))) - return domains_subnames - - def get_rrsets(self) -> List[RRset]: - rrsets = [] - for domain, subname in self.domains_subnames(): - rrsets.append(self.get_or_create_rr_set(domain, subname)) - return rrsets + clean.add(name) + return clean def get_rrs(self) -> List[RR]: - rrs = [] - for domain, subname in self.domains_subnames(): - rrset = self.get_or_create_rr_set(domain, subname) - for content in self.get_record_contents(): - rrs.append(self.get_or_create_rr(rrset=rrset, content=content)) - return rrs - - def save_rrs(self): - for rr in self.get_rrs(): - rr.rrset.save() - rr.save() - - def delete_rrs(self): - for domain, subname in self.domains_subnames(): - rrset = self.get_or_create_rr_set(domain, subname) - rrset.records.filter(content__in=self.get_record_contents()).delete() - if not len(rrset.records.all()): - rrset.delete() + return [ + self.get_or_create_rr( + fqdn=f"_{self.port:n}._{self.protocol}.{qname}", + content=content, + ) + for qname in self.subject_names_clean + for content in self.get_record_contents() + ] @property def not_valid_before(self): From bf57f20c40c4612594d2eb6d201ab806cafe3c4d Mon Sep 17 00:00:00 2001 From: Nils Wisiol Date: Sun, 19 Sep 2021 22:44:25 +0200 Subject: [PATCH 5/9] TLSA tests pass --- api/desecapi/models.py | 34 +++++++++++++---------- api/desecapi/tests/test_identities.py | 40 +++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/api/desecapi/models.py b/api/desecapi/models.py index 70c1cf0e3..29168fe14 100644 --- a/api/desecapi/models.py +++ b/api/desecapi/models.py @@ -220,7 +220,10 @@ def filter_qname(self, qname: str, **kwargs) -> models.query.QuerySet: ).filter(dotted_qname__endswith=F('dotted_name'), **kwargs) def most_specific_zone(self, fqdn: str) -> Tuple[Domain, str]: - domain = self.filter_qname(fqdn).order_by('-name_length').first() + try: + domain = self.filter_qname(fqdn).order_by('-name_length')[0] + except IndexError: + raise Domain.DoesNotExist subname = fqdn[:-len(domain.name)].rstrip('.') return domain, subname @@ -999,7 +1002,7 @@ class Identity(models.Model): created = models.DateTimeField(auto_now_add=True) owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='identities') default_ttl = models.PositiveIntegerField(default=300) - rrs = models.ManyToManyField(to=RR) + rrs = models.ManyToManyField(to=RR, related_name='identities') scheduled_removal = models.DateTimeField(null=True) class Meta: @@ -1010,14 +1013,14 @@ def get_rrs(self) -> List[RR]: def save(self, *args, **kwargs): for rr in self.get_rrs(): - self.rrs.add(rr) rr.rrset.save() rr.save() + self.rrs.add(rr) return super().save(*args, **kwargs) def delete(self, using=None, keep_parents=False): for rr in self.rrs.all(): # TODO use one query - if len(rr.identities) == 1: + if len(rr.identities.all()) == 1: rr.delete() return super().delete(using, keep_parents) @@ -1076,7 +1079,7 @@ def __init__(self, *args, **kwargs): if 'not_valid_after' not in kwargs: self.scheduled_removal = self.not_valid_after - def get_record_contents(self) -> List[str]: + def get_record_content(self) -> str: # choose hash function if self.tlsa_matching_type == self.MatchingType.SHA256: hash_function = hazmat.primitives.hashes.SHA256() @@ -1100,7 +1103,7 @@ def get_record_contents(self) -> List[str]: hash = h.finalize().hex() # create TLSA record content - return [f"{self.tlsa_certificate_usage} {self.tlsa_selector} {self.tlsa_matching_type} {hash}"] + return f"{self.tlsa_certificate_usage} {self.tlsa_selector} {self.tlsa_matching_type} {hash}" @property def _cert(self) -> x509.Certificate: @@ -1145,14 +1148,17 @@ def subject_names_clean(self) -> Set[str]: return clean def get_rrs(self) -> List[RR]: - return [ - self.get_or_create_rr( - fqdn=f"_{self.port:n}._{self.protocol}.{qname}", - content=content, - ) - for qname in self.subject_names_clean - for content in self.get_record_contents() - ] + rrs = [] + content = self.get_record_content() + for qname in self.subject_names_clean: + try: + rrs.append(self.get_or_create_rr( + fqdn=f"_{self.port:n}._{self.protocol}.{qname}", + content=content, + )) + except Domain.DoesNotExist: + pass + return rrs @property def not_valid_before(self): diff --git a/api/desecapi/tests/test_identities.py b/api/desecapi/tests/test_identities.py index 2d00cbccd..3ce7bec1f 100644 --- a/api/desecapi/tests/test_identities.py +++ b/api/desecapi/tests/test_identities.py @@ -47,10 +47,7 @@ def test_generated_rrs_many_rrsets(self): id = models.TLSIdentity(certificate=CERTIFICATE, owner=self.user, protocol=models.TLSIdentity.Protocol.SCTP) - self.assertEqual( - id.domains_subnames(), - {(domain, '_443._sctp'), (domain, '_443._sctp.desec'), (domain, '_443._sctp.dedyn')}, - ) + self.assertEqual(id.subject_names, SUBJECT_NAMES) rrs = id.get_rrs() self.assertEqual(len(rrs), 3) @@ -69,7 +66,6 @@ def test_generated_rrs_one_rrset(self): domain.save() id = models.TLSIdentity(certificate=CERTIFICATE, owner=self.user, port=123) - self.assertEqual(id.domains_subnames(), {(domain, '_123._tcp')}) rrs = id.get_rrs() self.assertEqual(len(rrs), 1) @@ -115,3 +111,37 @@ def test_create_delete_rrs(self): id.delete() rrset = models.RRset.objects.get(domain__name='desec.example.dedyn.io', type='TLSA', subname='_123._tcp') self.assertEqual(len(rrset.records.all()), 1) + + def test_duplicate_record(self): + def count_tlsa_records(): + return models.RRset.objects.get( + domain__name='desec.example.dedyn.io', + type='TLSA', subname='_443._tcp' + ).records.count() + + domain = models.Domain(name='desec.example.dedyn.io', owner=self.user) + domain.save() + + # insert first cert, insert second, delete first, delete second + id1 = models.TLSIdentity(certificate=CERTIFICATE, owner=self.user) + id2 = models.TLSIdentity(certificate=CERTIFICATE, owner=self.user) + id1.save() + self.assertEqual(count_tlsa_records(), 1) + id2.save() + self.assertEqual(count_tlsa_records(), 1) + id1.delete() + self.assertEqual(count_tlsa_records(), 1) + id2.delete() + self.assertEqual(count_tlsa_records(), 0) + + # insert first cert, insert second, delete second, delete first + id1 = models.TLSIdentity(certificate=CERTIFICATE, owner=self.user) + id2 = models.TLSIdentity(certificate=CERTIFICATE, owner=self.user) + id1.save() + self.assertEqual(count_tlsa_records(), 1) + id2.save() + self.assertEqual(count_tlsa_records(), 1) + id2.delete() + self.assertEqual(count_tlsa_records(), 1) + id1.delete() + self.assertEqual(count_tlsa_records(), 0) From 8ccdc3d80ac38912ea539eb87bb0b987425ce9b0 Mon Sep 17 00:00:00 2001 From: Nils Wisiol Date: Sat, 25 Sep 2021 18:03:00 +0200 Subject: [PATCH 6/9] clean code, fix RRset deletion (when empty), fix serializer, view, webapp --- .../{0018_tlsidentity.py => 0018_tlsaidentity.py} | 4 ++-- api/desecapi/models.py | 12 ++++++++---- api/desecapi/serializers.py | 8 +++++--- api/desecapi/tests/test_identities.py | 8 ++++++++ api/desecapi/views.py | 2 +- webapp/src/views/TLSIdentityList.vue | 10 ++++++++++ 6 files changed, 34 insertions(+), 10 deletions(-) rename api/desecapi/migrations/{0018_tlsidentity.py => 0018_tlsaidentity.py} (92%) diff --git a/api/desecapi/migrations/0018_tlsidentity.py b/api/desecapi/migrations/0018_tlsaidentity.py similarity index 92% rename from api/desecapi/migrations/0018_tlsidentity.py rename to api/desecapi/migrations/0018_tlsaidentity.py index 703c4b1a8..2bb7f3c2f 100644 --- a/api/desecapi/migrations/0018_tlsidentity.py +++ b/api/desecapi/migrations/0018_tlsaidentity.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.7 on 2021-09-19 19:53 +# Generated by Django 3.2.7 on 2021-09-25 15:30 from django.conf import settings from django.db import migrations, models @@ -28,7 +28,7 @@ class Migration(migrations.Migration): ('port', models.IntegerField(default=443)), ('protocol', models.TextField(choices=[('tcp', 'Tcp'), ('udp', 'Udp'), ('sctp', 'Sctp')], default='tcp')), ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='identities', to=settings.AUTH_USER_MODEL)), - ('rrs', models.ManyToManyField(to='desecapi.RR')), + ('rrs', models.ManyToManyField(related_name='identities', to='desecapi.RR')), ], options={ 'abstract': False, diff --git a/api/desecapi/models.py b/api/desecapi/models.py index 29168fe14..fb430c7a0 100644 --- a/api/desecapi/models.py +++ b/api/desecapi/models.py @@ -12,7 +12,7 @@ from datetime import timedelta from functools import cached_property from hashlib import sha256 -from typing import Set, List, Optional, Tuple, Dict +from typing import Set, List, Tuple import dns import psl_dns @@ -224,7 +224,7 @@ def most_specific_zone(self, fqdn: str) -> Tuple[Domain, str]: domain = self.filter_qname(fqdn).order_by('-name_length')[0] except IndexError: raise Domain.DoesNotExist - subname = fqdn[:-len(domain.name)].rstrip('.') + subname = fqdn[:-len(domain.name)].removesuffix('.') return domain, subname @@ -1012,16 +1012,20 @@ def get_rrs(self) -> List[RR]: raise NotImplementedError def save(self, *args, **kwargs): + ret = super().save(*args, **kwargs) for rr in self.get_rrs(): rr.rrset.save() rr.save() self.rrs.add(rr) - return super().save(*args, **kwargs) + return ret def delete(self, using=None, keep_parents=False): for rr in self.rrs.all(): # TODO use one query if len(rr.identities.all()) == 1: - rr.delete() + if (len(rr.rrset.records.all())) == 1: + rr.rrset.delete() + else: + rr.delete() return super().delete(using, keep_parents) # TODO move to RRset / RRset manager? diff --git a/api/desecapi/serializers.py b/api/desecapi/serializers.py index 20217d4c2..1759b9170 100644 --- a/api/desecapi/serializers.py +++ b/api/desecapi/serializers.py @@ -848,23 +848,25 @@ class TLSIdentitySerializer(serializers.ModelSerializer): def get_published_at(self, tls_identity: models.TLSIdentity): return [ f"{rrset.type}/{rrset.name}" - for rrset in tls_identity.get_rrsets() + for rrset in {rr.rrset for rr in tls_identity.rrs.all()} # TODO improve query ] class Meta: model = models.TLSIdentity fields = ( + # Identity fields 'id', 'name', 'created', 'default_ttl', + 'scheduled_removal', + 'published_at', + # TLSAIdentity fields 'certificate', 'tlsa_selector', 'tlsa_matching_type', 'tlsa_certificate_usage', 'port', 'protocol', 'fingerprint', 'not_valid_before', 'not_valid_after', 'subject_names', - - 'published_at', ) read_only_fields = list(filter(lambda f: f not in ('name', 'certificate'), fields)) diff --git a/api/desecapi/tests/test_identities.py b/api/desecapi/tests/test_identities.py index 3ce7bec1f..2e3f686bf 100644 --- a/api/desecapi/tests/test_identities.py +++ b/api/desecapi/tests/test_identities.py @@ -119,6 +119,12 @@ def count_tlsa_records(): type='TLSA', subname='_443._tcp' ).records.count() + def count_tlsa_rrsets(): + return models.RRset.objects.get( + domain__name='desec.example.dedyn.io', + type='TLSA', subname='_443._tcp' + ).count() + domain = models.Domain(name='desec.example.dedyn.io', owner=self.user) domain.save() @@ -133,6 +139,7 @@ def count_tlsa_records(): self.assertEqual(count_tlsa_records(), 1) id2.delete() self.assertEqual(count_tlsa_records(), 0) + self.assertEqual(count_tlsa_rrsets(), 0) # insert first cert, insert second, delete second, delete first id1 = models.TLSIdentity(certificate=CERTIFICATE, owner=self.user) @@ -145,3 +152,4 @@ def count_tlsa_records(): self.assertEqual(count_tlsa_records(), 1) id1.delete() self.assertEqual(count_tlsa_records(), 0) + self.assertEqual(count_tlsa_rrsets(), 0) diff --git a/api/desecapi/views.py b/api/desecapi/views.py index 43b0c1b81..614c52666 100644 --- a/api/desecapi/views.py +++ b/api/desecapi/views.py @@ -785,7 +785,7 @@ class TLSIdentityViewSet(IdentityViewSet): @property def throttle_scope(self): - return 'dns_api_read' if self.request.method in SAFE_METHODS else 'dns_api_write' + return 'dns_api_read' if self.request.method in SAFE_METHODS else 'dns_api_write_rrsets' def get_queryset(self): return self.request.user.identities.all() # TODO filter for TLS diff --git a/webapp/src/views/TLSIdentityList.vue b/webapp/src/views/TLSIdentityList.vue index 49b304148..78ee76d96 100644 --- a/webapp/src/views/TLSIdentityList.vue +++ b/webapp/src/views/TLSIdentityList.vue @@ -95,6 +95,16 @@ export default { readonly: true, datatype: 'MultilineText', }, + scheduled_removal: { + name: 'item.scheduled_removal', + text: 'Scheduled Removal', + align: 'left', + sortable: true, + value: 'scheduled_removal', + readonly: true, + datatype: 'TimeAgo', + searchable: false, + }, created: { name: 'item.created', text: 'Created', From 048048188547f14a2b325bd817cf3bada7dcfd41 Mon Sep 17 00:00:00 2001 From: Nils Wisiol Date: Sat, 25 Sep 2021 18:28:57 +0200 Subject: [PATCH 7/9] fix tests --- api/desecapi/tests/test_identities.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api/desecapi/tests/test_identities.py b/api/desecapi/tests/test_identities.py index 2e3f686bf..593bf3ab6 100644 --- a/api/desecapi/tests/test_identities.py +++ b/api/desecapi/tests/test_identities.py @@ -120,7 +120,7 @@ def count_tlsa_records(): ).records.count() def count_tlsa_rrsets(): - return models.RRset.objects.get( + return models.RRset.objects.filter( domain__name='desec.example.dedyn.io', type='TLSA', subname='_443._tcp' ).count() @@ -138,7 +138,6 @@ def count_tlsa_rrsets(): id1.delete() self.assertEqual(count_tlsa_records(), 1) id2.delete() - self.assertEqual(count_tlsa_records(), 0) self.assertEqual(count_tlsa_rrsets(), 0) # insert first cert, insert second, delete second, delete first @@ -151,5 +150,4 @@ def count_tlsa_rrsets(): id2.delete() self.assertEqual(count_tlsa_records(), 1) id1.delete() - self.assertEqual(count_tlsa_records(), 0) self.assertEqual(count_tlsa_rrsets(), 0) From ac6dd31013ef180619fa8755d6f7f072fbdfefec Mon Sep 17 00:00:00 2001 From: Nils Wisiol Date: Mon, 27 Sep 2021 13:59:32 +0200 Subject: [PATCH 8/9] wip --- api/desecapi/models.py | 20 ++- api/desecapi/serializers.py | 9 +- api/desecapi/tests/test_identities.py | 1 + webapp/src/components/Field/DomainList.vue | 26 ++++ webapp/src/components/Field/TLSAIdentity.vue | 119 ++++++++++++++++ webapp/src/views/CrudList.vue | 15 +++ webapp/src/views/DaneHome.vue | 4 +- webapp/src/views/TLSIdentityList.vue | 135 ++++++++++++------- 8 files changed, 276 insertions(+), 53 deletions(-) create mode 100644 webapp/src/components/Field/DomainList.vue create mode 100644 webapp/src/components/Field/TLSAIdentity.vue diff --git a/api/desecapi/models.py b/api/desecapi/models.py index fb430c7a0..d374eab51 100644 --- a/api/desecapi/models.py +++ b/api/desecapi/models.py @@ -1002,7 +1002,7 @@ class Identity(models.Model): created = models.DateTimeField(auto_now_add=True) owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='identities') default_ttl = models.PositiveIntegerField(default=300) - rrs = models.ManyToManyField(to=RR, related_name='identities') + rrs = models.ManyToManyField(to=RR, related_name='identities') # TODO OneToMany? scheduled_removal = models.DateTimeField(null=True) class Meta: @@ -1011,6 +1011,16 @@ class Meta: def get_rrs(self) -> List[RR]: raise NotImplementedError + @property + def covered_names(self) -> List[str]: + raise NotImplementedError + + def domains(self) -> List[Domain]: + # TODO improve query + return list({ + d.name: d for d in [rr.rrset.domain for rr in self.rrs.all()] + }.values()) + def save(self, *args, **kwargs): ret = super().save(*args, **kwargs) for rr in self.get_rrs(): @@ -1081,7 +1091,7 @@ class Protocol(models.TextChoices): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if 'not_valid_after' not in kwargs: - self.scheduled_removal = self.not_valid_after + self.scheduled_removal = self.not_valid_after # TODO check timezone def get_record_content(self) -> str: # choose hash function @@ -1140,7 +1150,7 @@ def subject_names_clean(self) -> Set[str]: clean = set() for name in self.subject_names: # cut off any wildcard prefix - name = name.lstrip('*').lstrip('.') + name = name.lstrip('*').lstrip('.') # TODO publish wildcard TLSA record? # filter names for valid domain names try: @@ -1171,3 +1181,7 @@ def not_valid_before(self): @property def not_valid_after(self): return self._cert.not_valid_after + + @property + def covered_names(self) -> Set[str]: + return {rr.rrset.name.split('.', 2)[-1].removesuffix('.') for rr in self.rrs.all()} diff --git a/api/desecapi/serializers.py b/api/desecapi/serializers.py index 1759b9170..4b7a5967f 100644 --- a/api/desecapi/serializers.py +++ b/api/desecapi/serializers.py @@ -844,6 +844,11 @@ class Meta(AuthenticatedDomainBasicUserActionSerializer.Meta): class TLSIdentitySerializer(serializers.ModelSerializer): published_at = serializers.SerializerMethodField(read_only=True) + domains = serializers.SlugRelatedField( + many=True, + read_only=True, + slug_field='name' + ) def get_published_at(self, tls_identity: models.TLSIdentity): return [ @@ -860,6 +865,8 @@ class Meta: 'default_ttl', 'scheduled_removal', 'published_at', + 'domains', + 'covered_names', # TLSAIdentity fields 'certificate', @@ -869,4 +876,4 @@ class Meta: 'fingerprint', 'not_valid_before', 'not_valid_after', 'subject_names', ) - read_only_fields = list(filter(lambda f: f not in ('name', 'certificate'), fields)) + read_only_fields = list(filter(lambda f: f not in ('name', 'certificate'), fields)) # TODO add tlsa_*, port, proto diff --git a/api/desecapi/tests/test_identities.py b/api/desecapi/tests/test_identities.py index 593bf3ab6..b5d501846 100644 --- a/api/desecapi/tests/test_identities.py +++ b/api/desecapi/tests/test_identities.py @@ -36,6 +36,7 @@ class TLSAIdentityTest(DesecTestCase): + # TODO load invalid cert def read_subject_names(self): id = models.TLSIdentity(certificate=CERTIFICATE, owner=self.user) diff --git a/webapp/src/components/Field/DomainList.vue b/webapp/src/components/Field/DomainList.vue new file mode 100644 index 000000000..0b17911b7 --- /dev/null +++ b/webapp/src/components/Field/DomainList.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/webapp/src/components/Field/TLSAIdentity.vue b/webapp/src/components/Field/TLSAIdentity.vue new file mode 100644 index 000000000..f8421a85c --- /dev/null +++ b/webapp/src/components/Field/TLSAIdentity.vue @@ -0,0 +1,119 @@ + + + + + \ No newline at end of file diff --git a/webapp/src/views/CrudList.vue b/webapp/src/views/CrudList.vue index f3c79d3a5..64a81231f 100644 --- a/webapp/src/views/CrudList.vue +++ b/webapp/src/views/CrudList.vue @@ -24,6 +24,7 @@ :loading="$store.getters.working || createDialogWorking || destroyDialogWorking" class="elevation-1" @click:row="rowClick" + :show-expand="expandable" > + @@ -339,6 +348,8 @@ import RecordList from '@/components/Field/RecordList'; import Switchbox from '@/components/Field/Switchbox'; import TTL from '@/components/Field/TTL'; import MultilineText from "@/components/Field/MultilineText"; +import TLSAIdentity from "../components/Field/TLSAIdentity"; +import DomainList from "../components/Field/DomainList"; const filter = function (obj, predicate) { const result = {}; @@ -368,6 +379,8 @@ export default { RecordList, TTL, MultilineText, + TLSAIdentity, + DomainList, }, data() { return { createDialog: false, @@ -415,6 +428,8 @@ export default { }, columns: {}, actions: [], + expandable: false, + expandComponentName: undefined, // resource paths: { list: 'needs/to/be/overwritten/', diff --git a/webapp/src/views/DaneHome.vue b/webapp/src/views/DaneHome.vue index d7c2019f3..e6e226ac2 100644 --- a/webapp/src/views/DaneHome.vue +++ b/webapp/src/views/DaneHome.vue @@ -46,8 +46,8 @@ export default { components: {TLSIdentityList}, data: () => ({ 'types': [ - {'id': 'tls', 'rdtype': 'TLSA', 'name': 'TLS Keys'}, - {'id': 'smime', 'rdtype': 'SMIMEA', 'name': 'S/MIME Keys'}, + {'id': 'tls', 'rdtype': 'TLSA', 'name': 'TLS Certificates'}, + {'id': 'smime', 'rdtype': 'SMIMEA', 'name': 'S/MIME Certificates'}, {'id': 'OPENPGP', 'rdtype': 'OPENPGPKEY', 'name': 'OPEN PGP Keys'}, {'id': 'SSH', 'rdtype': 'SSHFP', 'name': 'SSH Fingerprints'}, ] diff --git a/webapp/src/views/TLSIdentityList.vue b/webapp/src/views/TLSIdentityList.vue index 78ee76d96..c3806753c 100644 --- a/webapp/src/views/TLSIdentityList.vue +++ b/webapp/src/views/TLSIdentityList.vue @@ -1,5 +1,4 @@