diff --git a/api/desecapi/migrations/0018_tlsaidentity.py b/api/desecapi/migrations/0018_tlsaidentity.py new file mode 100644 index 000000000..2bb7f3c2f --- /dev/null +++ b/api/desecapi/migrations/0018_tlsaidentity.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.7 on 2021-09-25 15:30 + +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(related_name='identities', to='desecapi.RR')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/api/desecapi/models.py b/api/desecapi/models.py index 427df053c..d374eab51 100644 --- a/api/desecapi/models.py +++ b/api/desecapi/models.py @@ -12,10 +12,12 @@ from datetime import timedelta from functools import cached_property from hashlib import sha256 +from typing import Set, List, Tuple import dns import psl_dns import rest_framework.authtoken.models +from cryptography import x509, hazmat from django.conf import settings from django.contrib.auth.hashers import make_password from django.contrib.auth.models import AbstractBaseUser, AnonymousUser, BaseUserManager @@ -217,6 +219,14 @@ 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]: + try: + domain = self.filter_qname(fqdn).order_by('-name_length')[0] + except IndexError: + raise Domain.DoesNotExist + subname = fqdn[:-len(domain.name)].removesuffix('.') + return domain, subname + class Domain(ExportModelOperationsMixin('Domain'), models.Model): @staticmethod @@ -982,3 +992,196 @@ def verify(self, solution: str): and age <= settings.CAPTCHA_VALIDITY_PERIOD # not expired ) + + +class Identity(models.Model): + rr_type = None + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + name = models.CharField(max_length=24, default="") + 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') # TODO OneToMany? + scheduled_removal = models.DateTimeField(null=True) + + class Meta: + abstract = True + + 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(): + rr.rrset.save() + rr.save() + self.rrs.add(rr) + 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: + 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? + 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: + return RRset(domain=domain, subname=subname, type=self.rr_type, ttl=self.default_ttl) + + # 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: + return RR(rrset=rrset, content=content) + + +class TLSIdentity(Identity): + rr_type = 'TLSA' + + class CertificateUsage(models.IntegerChoices): + CA_CONSTRAINT = 0 + SERVICE_CERTIFICATE_CONSTRAINT = 1 + TRUST_ANCHOR_ASSERTION = 2 + DOMAIN_ISSUED_CERTIFICATE = 3 + + class Selector(models.IntegerChoices): + FULL_CERTIFICATE = 0 + SUBJECT_PUBLIC_KEY_INFO = 1 + + class MatchingType(models.IntegerChoices): + NO_HASH_USED = 0 + SHA256 = 1 + SHA512 = 2 + + class Protocol(models.TextChoices): + TCP = 'tcp' + UDP = 'udp' + SCTP = 'sctp' + + certificate = models.TextField() + + tlsa_selector = models.IntegerField(choices=Selector.choices, default=Selector.SUBJECT_PUBLIC_KEY_INFO) + tlsa_matching_type = models.IntegerField(choices=MatchingType.choices, default=MatchingType.SHA256) + tlsa_certificate_usage = models.IntegerField(choices=CertificateUsage.choices, + default=CertificateUsage.DOMAIN_ISSUED_CERTIFICATE) + + port = models.IntegerField(default=443) + protocol = models.TextField(choices=Protocol.choices, default=Protocol.TCP) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if 'not_valid_after' not in kwargs: + self.scheduled_removal = self.not_valid_after # TODO check timezone + + def get_record_content(self) -> str: + # choose hash function + if self.tlsa_matching_type == self.MatchingType.SHA256: + hash_function = hazmat.primitives.hashes.SHA256() + elif self.tlsa_matching_type == self.MatchingType.SHA512: + hash_function = hazmat.primitives.hashes.SHA512() + else: + raise NotImplementedError + + # choose data to hash + if self.tlsa_selector == self.Selector.SUBJECT_PUBLIC_KEY_INFO: + to_be_hashed = self._cert.public_key().public_bytes( + hazmat.primitives.serialization.Encoding.DER, + hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo + ) + else: + raise NotImplementedError + + # compute the hash + h = hazmat.primitives.hashes.Hash(hash_function) + h.update(to_be_hashed) + hash = h.finalize().hex() + + # create TLSA record content + return f"{self.tlsa_certificate_usage} {self.tlsa_selector} {self.tlsa_matching_type} {hash}" + + @property + def _cert(self) -> x509.Certificate: + return x509.load_pem_x509_certificate(self.certificate.encode()) + + @property + def fingerprint(self) -> str: + return self._cert.fingerprint(hazmat.primitives.hashes.SHA256()).hex() + + @property + def subject_names(self) -> Set[str]: + subject_names = { + x.value for x in + self._cert.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME) + } + + try: + subject_alternative_names = { + x for x in + self._cert.extensions.get_extension_for_oid( + x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME).value.get_values_for_type(x509.DNSName) + } + except x509.extensions.ExtensionNotFound: + subject_alternative_names = set() + + return subject_names | subject_alternative_names + + @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('.') # TODO publish wildcard TLSA record? + + # filter names for valid domain names + try: + validate_domain_name[1](name) + except ValidationError: + continue + + clean.add(name) + return clean + + def get_rrs(self) -> List[RR]: + 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): + return self._cert.not_valid_before + + @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 db75c7ef4..4b7a5967f 100644 --- a/api/desecapi/serializers.py +++ b/api/desecapi/serializers.py @@ -840,3 +840,40 @@ class AuthenticatedRenewDomainBasicUserActionSerializer(AuthenticatedDomainBasic class Meta(AuthenticatedDomainBasicUserActionSerializer.Meta): model = models.AuthenticatedRenewDomainBasicUserAction + + +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 [ + f"{rrset.type}/{rrset.name}" + 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', + 'domains', + 'covered_names', + + # TLSAIdentity fields + 'certificate', + 'tlsa_selector', 'tlsa_matching_type', 'tlsa_certificate_usage', + + 'port', 'protocol', + + 'fingerprint', 'not_valid_before', 'not_valid_after', 'subject_names', + ) + 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 new file mode 100644 index 000000000..b5d501846 --- /dev/null +++ b/api/desecapi/tests/test_identities.py @@ -0,0 +1,154 @@ +from desecapi import models +from desecapi.tests.base import DesecTestCase + +CERTIFICATE = r"""-----BEGIN CERTIFICATE----- +MIIFXzCCBEegAwIBAgISBCpjR1Aco6QfGqTdBrLb3uFZMA0GCSqGSIb3DQEBCwUA +MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD +EwJSMzAeFw0yMTAxMTQwNzQyMDlaFw0yMTA0MTQwNzQyMDlaMB0xGzAZBgNVBAMM +EiouZXhhbXBsZS5kZWR5bi5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAMn/csv0cxjfsDZQvbkAm+owpSaRKob+bsaBNCKsP79XetMdebL7Qp7uFaQp +gcYAD08HMtCv85JoA8N4HFPWQB+ppjXHaDqG6fQkDUPhP+IglqLKhiFNHrcZwNVq +3OLxT/Sjg7TP0zWKPQRaflz/hMpqYCsXvpdsfeSHcCOBOb8d7gjmmhpaghhsWE12 +jKEHVLHjotc8nRHp3ufxXIHu5Z0XblP/ohnDWKT/eg8lDD/lE95PAgsxpuKQUP8W +eihOyg2GBRmlCaSadyKslOpK8Bhve5utPTYkWP7dshpzprL/gponuI44h+KXe9Se +58u0acqqYEPrPPk6bIpIcHvG1o0CAwEAAaOCAoIwggJ+MA4GA1UdDwEB/wQEAwIF +oDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAd +BgNVHQ4EFgQUf1fUPkIolo4+mV11XDLl6R499y4wHwYDVR0jBBgwFoAUFC6zF7dY +VsuuUAlA5h+vnYsUwsYwVQYIKwYBBQUHAQEESTBHMCEGCCsGAQUFBzABhhVodHRw +Oi8vcjMuby5sZW5jci5vcmcwIgYIKwYBBQUHMAKGFmh0dHA6Ly9yMy5pLmxlbmNy +Lm9yZy8wUQYDVR0RBEowSIIYKi5kZWR5bi5leGFtcGxlLmRlZHluLmlvghgqLmRl +c2VjLmV4YW1wbGUuZGVkeW4uaW+CEiouZXhhbXBsZS5kZWR5bi5pbzBMBgNVHSAE +RTBDMAgGBmeBDAECATA3BgsrBgEEAYLfEwEBATAoMCYGCCsGAQUFBwIBFhpodHRw +Oi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCCAQUGCisGAQQB1nkCBAIEgfYEgfMA8QB2 +APZclC/RdzAiFFQYCDCUVo7jTRMZM7/fDC8gC8xO8WTjAAABdwAPKecAAAQDAEcw +RQIgMTqvW5jK+wy/77A7G+8ty4bjAMcqTSVA11cbYoBjx2gCIQD9KYwutem+G/Vu +nrTAMzIhoK4ckyOFft5nszGaBwgcTgB3AG9Tdqwx8DEZ2JkApFEV/3cVHBHZAsEA +KQaNsgiaN9kTAAABdwAPKrIAAAQDAEgwRgIhAMChRP6aIpAgK/0RhxJy5BHwxINO +rI5aH606hLqr1adwAiEAhxxBTD8hI/X73E6f/dWgPeBXIodH3WFnTtpG6+Ex0L0w +DQYJKoZIhvcNAQELBQADggEBADbPwMfqA3wd5iFBFtppEIgqPVdyA3rIci1DDo2D +NwF4gjfJQEWPkSEAbgl/EpeDQzDOLwwHMYAqupqf9RfteN38i00fSNcGVPSExdcU +/9bzGxHpXmoCMKsgoM0rgnlLNXPK9WlRCyun4VsJzsT2g/CDrYm+qysMXjUg5BWV +GHsBo84w0KXj3TvpWOSMlBZyORdugu3Bix4R8F/A5jv9gh2LT5Nc0hzhQn1g7r2x +qWiksTqmAMBtBKY7CaGXevaygr42XMp8FhIt7bU3ndr0yHu/gxPJB0qLHn/hvz5F +VqgFUBTfxglKMUZ09W+6rBYqEKplpOhKgjXdrnChWgc/5ZM= +-----END CERTIFICATE-----""" +SUBJECT_NAMES = {'*.dedyn.example.dedyn.io', '*.desec.example.dedyn.io', '*.example.dedyn.io'} + + +class TLSAIdentityTest(DesecTestCase): + # TODO load invalid cert + + def read_subject_names(self): + id = models.TLSIdentity(certificate=CERTIFICATE, owner=self.user) + self.assertEqual(id.subject_names, SUBJECT_NAMES) + + def test_generated_rrs_many_rrsets(self): + domain = models.Domain(name='example.dedyn.io', owner=self.user) + domain.save() + + id = models.TLSIdentity(certificate=CERTIFICATE, owner=self.user, protocol=models.TLSIdentity.Protocol.SCTP) + + self.assertEqual(id.subject_names, SUBJECT_NAMES) + + rrs = id.get_rrs() + self.assertEqual(len(rrs), 3) + + for rr in rrs: + self.assertEqual(rr.rrset.type, "TLSA") + self.assertEqual(rr.content, "3 1 1 9a3a491fe2dd6e4e0b7bf20a5bb62dd6212337642dcd7f4449d0b43dee4a8642") + + self.assertEqual( + {rr.rrset.subname for rr in rrs}, + {"_443._sctp", "_443._sctp.desec", "_443._sctp.dedyn"}, + ) + + def test_generated_rrs_one_rrset(self): + domain = models.Domain(name='desec.example.dedyn.io', owner=self.user) + domain.save() + + id = models.TLSIdentity(certificate=CERTIFICATE, owner=self.user, port=123) + + rrs = id.get_rrs() + self.assertEqual(len(rrs), 1) + rr = rrs[0] + + self.assertEqual(rr.rrset.type, "TLSA") + self.assertEqual(rr.rrset.subname, '_123._tcp') + self.assertEqual(rr.content, "3 1 1 9a3a491fe2dd6e4e0b7bf20a5bb62dd6212337642dcd7f4449d0b43dee4a8642") + + def test_generated_rr_params(self): + domain = models.Domain(name='desec.example.dedyn.io', owner=self.user) + domain.save() + rrs = models.TLSIdentity(certificate=CERTIFICATE, owner=self.user, port=123, + tlsa_matching_type=models.TLSIdentity.MatchingType.SHA512, + tlsa_certificate_usage=models.TLSIdentity.CertificateUsage.TRUST_ANCHOR_ASSERTION + ).get_rrs() + self.assertEqual( + rrs[0].content, + "2 1 2 7e0c4276239bae692e17c748a53facf67599c05297ccd139f3b99822891ed" + "a1278fcd0d2cc5c932e3e3c38e5f5155038bf7135fb41c3afa0bc0abb245c4f2e62" + ) + + def test_create_delete_rrs(self): + domain = models.Domain(name='desec.example.dedyn.io', owner=self.user) + domain.save() + + rrset = models.RRset(domain=domain, type='TLSA', subname='_123._tcp', ttl=1234) + rrset.save() + + custom_rr = models.RR(rrset=rrset, content="3 1 1 deadbeef") + custom_rr.save() + + id = models.TLSIdentity( + certificate=CERTIFICATE, owner=self.user, port=123, + tlsa_matching_type=models.TLSIdentity.MatchingType.SHA512, + tlsa_certificate_usage=models.TLSIdentity.CertificateUsage.TRUST_ANCHOR_ASSERTION, + ) + id.save() + + rrset = models.RRset.objects.get(domain__name='desec.example.dedyn.io', type='TLSA', subname='_123._tcp') + self.assertEqual(len(rrset.records.all()), 2) + + 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() + + def count_tlsa_rrsets(): + return models.RRset.objects.filter( + 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() + + # 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_rrsets(), 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_rrsets(), 0) diff --git a/api/desecapi/urls/version_1.py b/api/desecapi/urls/version_1.py index 1824afcf7..264f5de98 100644 --- a/api/desecapi/urls/version_1.py +++ b/api/desecapi/urls/version_1.py @@ -6,6 +6,9 @@ tokens_router = SimpleRouter() tokens_router.register(r'', views.TokenViewSet, basename='token') +identities_router = SimpleRouter() +identities_router.register(r'tls', views.TLSIdentityViewSet, basename='identities-tls') + auth_urls = [ # User management path('', views.AccountCreateView.as_view(), name='register'), @@ -56,6 +59,9 @@ # CAPTCHA path('captcha/', views.CaptchaView.as_view(), name='captcha'), + + # Identities management + path('identities/', include(identities_router.urls)), ] app_name = 'desecapi' diff --git a/api/desecapi/views.py b/api/desecapi/views.py index 191af9829..614c52666 100644 --- a/api/desecapi/views.py +++ b/api/desecapi/views.py @@ -771,3 +771,21 @@ def finalize(self): class CaptchaView(generics.CreateAPIView): serializer_class = serializers.CaptchaSerializer throttle_scope = 'account_management_passive' + + +class IdentityViewSet(IdempotentDestroyMixin, viewsets.ModelViewSet): + permission_classes = (IsAuthenticated, IsOwner,) + + def perform_create(self, serializer): + serializer.save(owner=self.request.user) + + +class TLSIdentityViewSet(IdentityViewSet): + serializer_class = serializers.TLSIdentitySerializer + + @property + def throttle_scope(self): + 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/docker-compose.dev.yml b/docker-compose.dev.yml index 2d74ca226..7c5eb29c3 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -71,10 +71,7 @@ services: volumes: - ./webapp/:/usr/src/app/ working_dir: /usr/src/app/ - command: "npm run serve --fix" - environment: - - HOST=0.0.0.0 - - PORT=443 + command: bash -c "npm install && npm run serve" networks: - rearwebapp logging: diff --git a/webapp/Dockerfile b/webapp/Dockerfile index fbf1115b8..e8d7077bf 100644 --- a/webapp/Dockerfile +++ b/webapp/Dockerfile @@ -1,4 +1,4 @@ -FROM node:latest +FROM node:16 RUN \ apt-get update \ && apt-get -y install gettext-base \ diff --git a/webapp/package.json b/webapp/package.json index 37179098b..760dc469b 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "serve": "vue-cli-service serve", + "serve": "vue-cli-service serve --port 8080", "build": "vue-cli-service build", "test:unit": "vue-cli-service test:unit", "test:e2e": "vue-cli-service test:e2e", diff --git a/webapp/src/App.vue b/webapp/src/App.vue index 44cf974b8..2750dcc94 100644 --- a/webapp/src/App.vue +++ b/webapp/src/App.vue @@ -57,6 +57,13 @@ :to="{name: item.name}" > {{ item.text }} + + BETA + + + + + + + diff --git a/webapp/src/components/Field/MultilineText.vue b/webapp/src/components/Field/MultilineText.vue new file mode 100644 index 000000000..3efbe32cc --- /dev/null +++ b/webapp/src/components/Field/MultilineText.vue @@ -0,0 +1,78 @@ + + + + + \ No newline at end of file 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/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 0e6bf0370..64a81231f 100644 --- a/webapp/src/views/CrudList.vue +++ b/webapp/src/views/CrudList.vue @@ -1,336 +1,339 @@ diff --git a/webapp/src/views/TLSIdentityList.vue b/webapp/src/views/TLSIdentityList.vue new file mode 100644 index 000000000..c3806753c --- /dev/null +++ b/webapp/src/views/TLSIdentityList.vue @@ -0,0 +1,202 @@ + diff --git a/www/90-desec.static.dev.content b/www/90-desec.static.dev.content index 37dc7d3f1..271b58e84 100644 --- a/www/90-desec.static.dev.content +++ b/www/90-desec.static.dev.content @@ -9,4 +9,4 @@ proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; -proxy_pass http://webapp:443; # FIXME Note that we use 443 for plain text communication, to make webpack dev tools use port 443, too. A vue-cli upgrade may help. +proxy_pass http://webapp:8080;