From 5af1143f55d0da230584d6ddba9db29edb928abc Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 2 Oct 2023 09:52:44 -0400 Subject: [PATCH 01/65] initial working branch for dnssec --- src/registrar/config/urls.py | 10 +++++++++ src/registrar/templates/domain_dns.html | 22 ++++++++++++++++++++ src/registrar/templates/domain_dnssec.html | 12 +++++++++++ src/registrar/templates/domain_sidebar.html | 23 +++++++++++++++++++-- src/registrar/views/__init__.py | 2 ++ src/registrar/views/domain.py | 14 +++++++++++++ 6 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 src/registrar/templates/domain_dns.html create mode 100644 src/registrar/templates/domain_dnssec.html diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 9c3624c2c..3cf6d2dfc 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -80,11 +80,21 @@ urlpatterns = [ ), path("domain/", views.DomainView.as_view(), name="domain"), path("domain//users", views.DomainUsersView.as_view(), name="domain-users"), + path( + "domain//dns", + views.DomainDNSView.as_view(), + name="dns", + ), path( "domain//nameservers", views.DomainNameserversView.as_view(), name="domain-nameservers", ), + path( + "domain//dnssec", + views.DomainDNSSECView.as_view(), + name="domain-dnssec", + ), path( "domain//your-contact-information", views.DomainYourContactInformationView.as_view(), diff --git a/src/registrar/templates/domain_dns.html b/src/registrar/templates/domain_dns.html new file mode 100644 index 000000000..dafede300 --- /dev/null +++ b/src/registrar/templates/domain_dns.html @@ -0,0 +1,22 @@ +{% extends "domain_base.html" %} +{% load static field_helpers url_helpers %} + +{% block title %}DNS | {{ domain.name }} | {% endblock %} + +{% block domain_content %} + +

DNS

+ +

The Domain Name System (DNS) is the internet service that translates your domain name into an IP address. Before your .gov domain can be used, you'll need to connect it to your DNS hosting service and provide us with your name server information.

+ +

You can enter your name services, as well as other DNS-related information, in the following sections:

+ + {% url 'domain-nameservers' pk=domain.id as url %} +

DNS name servers

+ +

Subdomain records

+ + {% url 'domain-dnssec' pk=domain.id as url %} +

DNSSEC

+ +{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html new file mode 100644 index 000000000..1c774f83a --- /dev/null +++ b/src/registrar/templates/domain_dnssec.html @@ -0,0 +1,12 @@ +{% extends "domain_base.html" %} +{% load static field_helpers url_helpers %} + +{% block title %}DNSSEC | {{ domain.name }} | {% endblock %} + +{% block domain_content %} + +

DNSSEC

+ +

DNSSEC, or DNS Security Extensions, is additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your website, they can be certain that it's connecting to the correct server, preventing potential hijacking or tampering with your domain's records. Read more about DNSSEC and why it is important.

+ +{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 1e4cd1882..672052ed6 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -13,12 +13,31 @@
  • - {% url 'domain-nameservers' pk=domain.id as url %} + {% url 'dns' pk=domain.id as url %} - DNS name servers + DNS +
  • diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index f37d2724a..27522d40e 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -3,7 +3,9 @@ from .domain import ( DomainView, DomainAuthorizingOfficialView, DomainOrgNameAddressView, + DomainDNSView, DomainNameserversView, + DomainDNSSECView, DomainYourContactInformationView, DomainSecurityEmailView, DomainUsersView, diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index ca7cee4ac..788b084eb 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -128,6 +128,13 @@ class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin): return super().form_valid(form) +class DomainDNSView(DomainPermissionView): + + """DNS Information View.""" + + template_name = "domain_dns.html" + + class DomainNameserversView(DomainPermissionView, FormMixin): """Domain nameserver editing view.""" @@ -207,6 +214,13 @@ class DomainNameserversView(DomainPermissionView, FormMixin): return super().form_valid(formset) +class DomainDNSSECView(DomainPermissionView): + + """Domain DNSSEC editing view.""" + + template_name = "domain_dnssec.html" + + class DomainYourContactInformationView(DomainPermissionView, FormMixin): """Domain your contact information editing view.""" From 8bc0c9486bcf7d2a3cdf01e3c914d39114ec2e9d Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 2 Oct 2023 10:23:04 -0400 Subject: [PATCH 02/65] roughed in subdomains --- src/registrar/config/urls.py | 5 +++++ src/registrar/templates/domain_dns.html | 3 ++- src/registrar/templates/domain_dnssec.html | 2 +- src/registrar/templates/domain_sidebar.html | 8 ++++++++ src/registrar/templates/domain_subdomains.html | 16 ++++++++++++++++ src/registrar/views/__init__.py | 1 + src/registrar/views/domain.py | 7 +++++++ 7 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 src/registrar/templates/domain_subdomains.html diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 3cf6d2dfc..30d531c10 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -90,6 +90,11 @@ urlpatterns = [ views.DomainNameserversView.as_view(), name="domain-nameservers", ), + path( + "domain//subdomains", + views.DomainSubdomainsView.as_view(), + name="domain-subdomains", + ), path( "domain//dnssec", views.DomainDNSSECView.as_view(), diff --git a/src/registrar/templates/domain_dns.html b/src/registrar/templates/domain_dns.html index dafede300..9b3b7500b 100644 --- a/src/registrar/templates/domain_dns.html +++ b/src/registrar/templates/domain_dns.html @@ -14,7 +14,8 @@ {% url 'domain-nameservers' pk=domain.id as url %}

    DNS name servers

    -

    Subdomain records

    + {% url 'domain-subdomains' pk=domain.id as url %} +

    Subdomain records

    {% url 'domain-dnssec' pk=domain.id as url %}

    DNSSEC

    diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index 1c774f83a..25ea8857d 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -7,6 +7,6 @@

    DNSSEC

    -

    DNSSEC, or DNS Security Extensions, is additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your website, they can be certain that it's connecting to the correct server, preventing potential hijacking or tampering with your domain's records. Read more about DNSSEC and why it is important.

    +

    DNSSEC, or DNS Security Extensions, is additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your website, they can be certain that it's connecting to the correct server, preventing potential hijacking or tampering with your domain's records. Read more about DNSSEC and why it is important.

    {% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 672052ed6..dd056f40f 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -29,6 +29,14 @@
  • +
  • + {% url 'domain-subdomains' pk=domain.id as url %} + + Subdomain records + +
  • {% url 'domain-dnssec' pk=domain.id as url %} Subdomain records + +

    Subdomains allow you to host multiple services under the same domain and create subsets of your website. For example, blogs are often set up as a subdomain (blog.example.gov).

    + +

    Subdomains cannot be requested through the .gov registrar. Those must be set up ... NEED ADDITIONAL COPY HERE

    + +

    If you use subdomains, enter those records (also known as glue records) below.

    + +{% endblock %} {# domain_content #} diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index 27522d40e..18441205d 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -5,6 +5,7 @@ from .domain import ( DomainOrgNameAddressView, DomainDNSView, DomainNameserversView, + DomainSubdomainsView, DomainDNSSECView, DomainYourContactInformationView, DomainSecurityEmailView, diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 788b084eb..9dae2cb68 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -214,6 +214,13 @@ class DomainNameserversView(DomainPermissionView, FormMixin): return super().form_valid(formset) +class DomainSubdomainsView(DomainPermissionView): + + """Domain subdomains editing view.""" + + template_name = "domain_subdomains.html" + + class DomainDNSSECView(DomainPermissionView): """Domain DNSSEC editing view.""" From edc0f967371b3790844416ac8738123fd31e7e57 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 2 Oct 2023 10:44:31 -0400 Subject: [PATCH 03/65] roughed in dsdata and keydata --- src/registrar/config/urls.py | 10 ++++++++++ src/registrar/templates/domain_dsdata.html | 10 ++++++++++ src/registrar/templates/domain_keydata.html | 10 ++++++++++ src/registrar/templates/domain_sidebar.html | 20 ++++++++++++++++++++ src/registrar/views/__init__.py | 2 ++ src/registrar/views/domain.py | 14 ++++++++++++++ 6 files changed, 66 insertions(+) create mode 100644 src/registrar/templates/domain_dsdata.html create mode 100644 src/registrar/templates/domain_keydata.html diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 30d531c10..772a353b3 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -100,6 +100,16 @@ urlpatterns = [ views.DomainDNSSECView.as_view(), name="domain-dnssec", ), + path( + "domain//dsdata", + views.DomainDsdataView.as_view(), + name="domain-dsdata", + ), + path( + "domain//keydata", + views.DomainKeydataView.as_view(), + name="domain-keydata", + ), path( "domain//your-contact-information", views.DomainYourContactInformationView.as_view(), diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html new file mode 100644 index 000000000..08a6bf74e --- /dev/null +++ b/src/registrar/templates/domain_dsdata.html @@ -0,0 +1,10 @@ +{% extends "domain_base.html" %} +{% load static field_helpers url_helpers %} + +{% block title %}DS Data | {{ domain.name }} | {% endblock %} + +{% block domain_content %} + +

    DS Data

    + +{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_keydata.html b/src/registrar/templates/domain_keydata.html new file mode 100644 index 000000000..6433d0c80 --- /dev/null +++ b/src/registrar/templates/domain_keydata.html @@ -0,0 +1,10 @@ +{% extends "domain_base.html" %} +{% load static field_helpers url_helpers %} + +{% block title %}Key Data | {{ domain.name }} | {% endblock %} + +{% block domain_content %} + +

    Key Data

    + +{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index dd056f40f..127c40532 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -37,6 +37,7 @@ Subdomain records
  • +
  • {% url 'domain-dnssec' pk=domain.id as url %} DNSSEC +
  • diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index 18441205d..8a04fa186 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -7,6 +7,8 @@ from .domain import ( DomainNameserversView, DomainSubdomainsView, DomainDNSSECView, + DomainDsdataView, + DomainKeydataView, DomainYourContactInformationView, DomainSecurityEmailView, DomainUsersView, diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 9dae2cb68..977a0fee3 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -228,6 +228,20 @@ class DomainDNSSECView(DomainPermissionView): template_name = "domain_dnssec.html" +class DomainDsdataView(DomainPermissionView): + + """Domain DNSSEC ds data editing view.""" + + template_name = "domain_dsdata.html" + + +class DomainKeydataView(DomainPermissionView): + + """Domain DNSSEC key data editing view.""" + + template_name = "domain_keydata.html" + + class DomainYourContactInformationView(DomainPermissionView, FormMixin): """Domain your contact information editing view.""" From 68e17a770d657ee10b79b94c698d76e72319f605 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 2 Oct 2023 11:51:41 -0400 Subject: [PATCH 04/65] minor formatting of domain_sidebar --- src/registrar/templates/domain_sidebar.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 127c40532..be012ed02 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -19,7 +19,7 @@ > DNS -
      +
      • {% url 'domain-nameservers' pk=domain.id as url %} DNSSEC -
          +
          • {% url 'domain-dsdata' pk=domain.id as url %} Date: Mon, 2 Oct 2023 14:01:36 -0400 Subject: [PATCH 05/65] adding hierarchy to dns urls --- src/registrar/config/urls.py | 22 ++++++++++----------- src/registrar/templates/domain_sidebar.html | 12 +++++------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 772a353b3..1487d372a 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -83,32 +83,32 @@ urlpatterns = [ path( "domain//dns", views.DomainDNSView.as_view(), - name="dns", + name="domain-dns", ), path( - "domain//nameservers", + "domain//dns/nameservers", views.DomainNameserversView.as_view(), - name="domain-nameservers", + name="domain-dns-nameservers", ), path( - "domain//subdomains", + "domain//dns/subdomains", views.DomainSubdomainsView.as_view(), - name="domain-subdomains", + name="domain-dns-subdomains", ), path( - "domain//dnssec", + "domain//dns/dnssec", views.DomainDNSSECView.as_view(), - name="domain-dnssec", + name="domain-dns-dnssec", ), path( - "domain//dsdata", + "domain//dns/dnssec/dsdata", views.DomainDsdataView.as_view(), - name="domain-dsdata", + name="domain-dns-dnssec-dsdata", ), path( - "domain//keydata", + "domain//dns/dnssec/keydata", views.DomainKeydataView.as_view(), - name="domain-keydata", + name="domain-dns-dnssec-keydata", ), path( "domain//your-contact-information", diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index be012ed02..6f09424f3 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -13,7 +13,7 @@
          • - {% url 'dns' pk=domain.id as url %} + {% url 'domain-dns' pk=domain.id as url %} @@ -21,7 +21,7 @@
            • - {% url 'domain-nameservers' pk=domain.id as url %} + {% url 'domain-dns-nameservers' pk=domain.id as url %} @@ -30,7 +30,7 @@
            • - {% url 'domain-subdomains' pk=domain.id as url %} + {% url 'domain-dns-subdomains' pk=domain.id as url %} @@ -39,7 +39,7 @@
            • - {% url 'domain-dnssec' pk=domain.id as url %} + {% url 'domain-dns-dnssec' pk=domain.id as url %} @@ -47,7 +47,7 @@
              • - {% url 'domain-dsdata' pk=domain.id as url %} + {% url 'domain-dns-dnssec-dsdata' pk=domain.id as url %} @@ -56,7 +56,7 @@
              • - {% url 'domain-keydata' pk=domain.id as url %} + {% url 'domain-dns-dnssec-keydata' pk=domain.id as url %} From 846b2a266e1ceae6734cb7c4a8764148e8830cf8 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 2 Oct 2023 14:41:20 -0400 Subject: [PATCH 06/65] faux hierarchy to sidebar for dns --- src/registrar/templates/domain_sidebar.html | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 6f09424f3..d3dc90a55 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -14,10 +14,8 @@
              • {% url 'domain-dns' pk=domain.id as url %} - - DNS + + DNS
                • @@ -41,7 +39,7 @@
                • {% url 'domain-dns-dnssec' pk=domain.id as url %} DNSSEC From e0bfd3e67c878334c5053ef70d41ca4ec3f85769 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 2 Oct 2023 15:42:08 -0400 Subject: [PATCH 07/65] broad brush strokes for dns data form --- src/registrar/forms/__init__.py | 1 + src/registrar/forms/domain.py | 63 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/registrar/forms/__init__.py b/src/registrar/forms/__init__.py index 13f75563f..186b24362 100644 --- a/src/registrar/forms/__init__.py +++ b/src/registrar/forms/__init__.py @@ -5,4 +5,5 @@ from .domain import ( DomainSecurityEmailForm, DomainOrgNameAddressForm, ContactForm, + DNSSECDSDataFormset, ) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index f14448bcf..a1b7b57d7 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -139,3 +139,66 @@ class DomainOrgNameAddressForm(forms.ModelForm): self.fields[field_name].required = True self.fields["state_territory"].widget.attrs.pop("maxlength", None) self.fields["zipcode"].widget.attrs.pop("maxlength", None) + + +class DomainDNSSECDSDataForm(forms.Form): + + """Form for adding or editing a security email to a domain.""" + + # Q: What are the options? + ALGORITHM_CHOICES = [ + ("ECC Ghost", "ECC Ghost"), + ] + # Q: What are the options? + DIGEST_TYPE_CHOICES = [ + ("SHA-256", "SHA-256"), + ] + + has_ds_key_data = forms.TypedChoiceField( + required=True, + label="DS Data record type", + choices=[(False, "DS Data"), (True, "DS Data with Key Data")], + ) + + key_tag = forms.IntegerField( + required=True, + label="Key tag", + validators=[ + RegexValidator( + "^[0-9]{5}(?:-[0-9]{4})?$|^$", + message="Accepted range 0-65535.", + ) + ], + ) + + algorithm = forms.TypedChoiceField( + label="Algorithm", + choices=[("", "--Select--")] + ALGORITHM_CHOICES, + # Q: Is this even needed or is a required=True sufficient? + error_messages={ + "required": ( + "You must select an Algorithm" + ) + }, + ) + # Q: Is ChoiceFiled right? Or do we need to data types other than strings + # (TypedChoiceField) + digest_type = forms.TypedChoiceField( + label="Digest Type", + choices=[("", "--Select--")] + DIGEST_TYPE_CHOICES, + # Q: Is this even needed or is a required=True sufficient? + error_messages={ + "required": ( + "You must select a Digest Type" + ) + }, + ) + digest = forms.CharField(label="Digest") + + # Conditional DS Key Data fields + + +DNSSECDSDataFormset = formset_factory( + DomainDNSSECDSDataForm, + extra=1, +) \ No newline at end of file From ef2b8494cbf69bbf25423efbd91f3426f665e231 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 2 Oct 2023 16:18:07 -0400 Subject: [PATCH 08/65] broad brush strokes for view and template --- src/registrar/forms/__init__.py | 2 +- src/registrar/forms/domain.py | 62 ++++++++++++------ src/registrar/templates/domain_dsdata.html | 36 +++++++++++ src/registrar/views/domain.py | 74 ++++++++++++++++++++++ 4 files changed, 153 insertions(+), 21 deletions(-) diff --git a/src/registrar/forms/__init__.py b/src/registrar/forms/__init__.py index 186b24362..e889405c9 100644 --- a/src/registrar/forms/__init__.py +++ b/src/registrar/forms/__init__.py @@ -5,5 +5,5 @@ from .domain import ( DomainSecurityEmailForm, DomainOrgNameAddressForm, ContactForm, - DNSSECDSDataFormset, + DomainDsdataFormset, ) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index a1b7b57d7..ea74db467 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -141,24 +141,29 @@ class DomainOrgNameAddressForm(forms.ModelForm): self.fields["zipcode"].widget.attrs.pop("maxlength", None) -class DomainDNSSECDSDataForm(forms.Form): +class DomainDsdataForm(forms.Form): """Form for adding or editing a security email to a domain.""" # Q: What are the options? ALGORITHM_CHOICES = [ - ("ECC Ghost", "ECC Ghost"), + (1, "ERSA/MD5 [RSAMD5]"), + (2 , "Diffie-Hellman [DH]"), + (3 ,"DSA/SHA-1 [DSA]"), + (5 ,"RSA/SHA-1 [RSASHA1]"), ] # Q: What are the options? DIGEST_TYPE_CHOICES = [ - ("SHA-256", "SHA-256"), + (0, "Reserved"), + (1, "SHA-256"), ] - has_ds_key_data = forms.TypedChoiceField( - required=True, - label="DS Data record type", - choices=[(False, "DS Data"), (True, "DS Data with Key Data")], - ) + # TODO: ds key data + # has_ds_key_data = forms.TypedChoiceField( + # required=True, + # label="DS Data record type", + # choices=[(False, "DS Data"), (True, "DS Data with Key Data")], + # ) key_tag = forms.IntegerField( required=True, @@ -172,20 +177,21 @@ class DomainDNSSECDSDataForm(forms.Form): ) algorithm = forms.TypedChoiceField( + required=True, label="Algorithm", - choices=[("", "--Select--")] + ALGORITHM_CHOICES, + choices=[(-1, "--Select--")] + ALGORITHM_CHOICES, # Q: Is this even needed or is a required=True sufficient? - error_messages={ - "required": ( - "You must select an Algorithm" - ) - }, + # error_messages={ + # "required": ( + # "You must select an Algorithm" + # ) + # }, ) # Q: Is ChoiceFiled right? Or do we need to data types other than strings # (TypedChoiceField) digest_type = forms.TypedChoiceField( label="Digest Type", - choices=[("", "--Select--")] + DIGEST_TYPE_CHOICES, + choices=[(-1, "--Select--")] + DIGEST_TYPE_CHOICES, # Q: Is this even needed or is a required=True sufficient? error_messages={ "required": ( @@ -193,12 +199,28 @@ class DomainDNSSECDSDataForm(forms.Form): ) }, ) - digest = forms.CharField(label="Digest") + digest = forms.CharField( + required=True, + label="Digest", + # validators=[ + # RegexValidator( + # "^[0-9]{5}(?:-[0-9]{4})?$|^$", + # message="Accepted range 0-65535.", + # ) + # ], + ) - # Conditional DS Key Data fields + # TODO: Conditional DS Key Data fields + -DNSSECDSDataFormset = formset_factory( - DomainDNSSECDSDataForm, +DomainDsdataFormset = formset_factory( + DomainDsdataForm, extra=1, -) \ No newline at end of file +) + + +# TODO: +# class DomainKeyDataForm(forms.Form): + +# """""" \ No newline at end of file diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 08a6bf74e..147fcde64 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -7,4 +7,40 @@

                  DS Data

                  + {% include "includes/required_fields.html" %} + +
                  + {% csrf_token %} + {{ formset.management_form }} + + {% for form in formset %} +
                  + {% with attr_required=True %} + {% input_with_errors form.key_tag %} + {% endwith %} + {% with attr_required=True %} + {% input_with_errors form.algorithm %} + {% endwith %} + {% with attr_required=True %} + {% input_with_errors form.digest_type %} + {% endwith %} + {% with attr_required=True %} + {% input_with_errors form.digest %} + {% endwith %} +
                  + {% endfor %} + + + + +
                  + {% endblock %} {# domain_content #} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 977a0fee3..ec940568a 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -28,7 +28,14 @@ from ..forms import ( DomainAddUserForm, DomainSecurityEmailForm, NameserverFormset, + DomainDsdataFormset, ) + +from epplibwrapper import ( + common, + extensions, +) + from ..utility.email import send_templated_email, EmailSendingError from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView @@ -233,6 +240,73 @@ class DomainDsdataView(DomainPermissionView): """Domain DNSSEC ds data editing view.""" template_name = "domain_dsdata.html" + form_class = DomainDsdataFormset + + def get_initial(self): + """The initial value for the form (which is a formset here).""" + domain = self.get_object() + dnssecdata: extensions.DNSSECExtension = domain.dnssecdata + initial_data = [] + + if dnssecdata.keyData is not None: + # TODO: Throw an error + pass + + if dnssecdata.dsData is not None: + # Add existing nameservers as initial data + # TODO: create context for each element in the record + # key_tag + # algorithm + # digest_type + # digest + initial_data.extend({"dsrecord": record} for record in dnssecdata.dsData) + + return initial_data + + def get_success_url(self): + """Redirect to the DS Data page for the domain.""" + return reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.object.pk}) + + def get_context_data(self, **kwargs): + """Adjust context from FormMixin for formsets.""" + context = super().get_context_data(**kwargs) + # use "formset" instead of "form" for the key + context["formset"] = context.pop("form") + return context + + def post(self, request, *args, **kwargs): + """Formset submission posts to this view.""" + self.object = self.get_object() + formset = self.get_form() + + if formset.is_valid(): + return self.form_valid(formset) + else: + return self.form_invalid(formset) + + def form_valid(self, formset): + """The formset is valid, perform something with it.""" + + # Set the nameservers from the formset + dnssecdata = [] + for form in formset: + try: + # TODO: build the right list of dicts to be passed + dsrecord = (form.cleaned_data["dsrecord"],) + dnssecdata.append(dsrecord) + except KeyError: + # no server information in this field, skip it + pass + domain = self.get_object() + domain.dnssecdata = dnssecdata + + messages.success( + self.request, "The DS Data records for this domain have been updated." + ) + + # superclass has the redirect + return super().form_valid(formset) + class DomainKeydataView(DomainPermissionView): From 12ae675772ddf87eb64f701915fb959bae331ac3 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 2 Oct 2023 17:13:51 -0400 Subject: [PATCH 09/65] minimal getter building form for dsdata --- src/registrar/models/domain.py | 8 +++++++- src/registrar/views/domain.py | 26 ++++++++++++++------------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 1b4ce2435..004df9d3c 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -282,7 +282,13 @@ class Domain(TimeStampedModel, DomainHelper): @Cache def dnssecdata(self) -> extensions.DNSSECExtension: - return self._get_property("dnssecdata") + try: + return self._get_property("dnssecdata") + except Exception as err: + # Don't throw error as this is normal for a new domain + # TODO - 433 error handling ticket should address this + logger.info("Domain does not have dnssec data defined %s" % err) + return None @dnssecdata.setter # type: ignore def dnssecdata(self, _dnssecdata: extensions.DNSSECExtension): diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index ec940568a..62f22707a 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -235,7 +235,7 @@ class DomainDNSSECView(DomainPermissionView): template_name = "domain_dnssec.html" -class DomainDsdataView(DomainPermissionView): +class DomainDsdataView(DomainPermissionView, FormMixin): """Domain DNSSEC ds data editing view.""" @@ -248,18 +248,20 @@ class DomainDsdataView(DomainPermissionView): dnssecdata: extensions.DNSSECExtension = domain.dnssecdata initial_data = [] - if dnssecdata.keyData is not None: - # TODO: Throw an error - pass + if dnssecdata is not None: - if dnssecdata.dsData is not None: - # Add existing nameservers as initial data - # TODO: create context for each element in the record - # key_tag - # algorithm - # digest_type - # digest - initial_data.extend({"dsrecord": record} for record in dnssecdata.dsData) + if dnssecdata.keyData is not None: + # TODO: Throw an error + pass + + if dnssecdata.dsData is not None: + # Add existing nameservers as initial data + # TODO: create context for each element in the record + # key_tag + # algorithm + # digest_type + # digest + initial_data.extend({"key_tag": record.keyTag, "algorithm": record.alg, "digest_type": record.digestType, "digest": record.digest} for record in dnssecdata.dsData) return initial_data From 99f272cd2190a889a0013c891d53a0b02acb67da Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 2 Oct 2023 17:58:05 -0400 Subject: [PATCH 10/65] setter now working in rudimentary form --- src/registrar/views/domain.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 62f22707a..3b72563b6 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -290,12 +290,18 @@ class DomainDsdataView(DomainPermissionView, FormMixin): """The formset is valid, perform something with it.""" # Set the nameservers from the formset - dnssecdata = [] + dnssecdata = {"dsData":[]} + for form in formset: try: # TODO: build the right list of dicts to be passed - dsrecord = (form.cleaned_data["dsrecord"],) - dnssecdata.append(dsrecord) + dsrecord = { + "keyTag": form.cleaned_data["key_tag"], + "alg": form.cleaned_data["algorithm"], + "digestType": form.cleaned_data["digest_type"], + "digest": form.cleaned_data["digest"], + } + dnssecdata["dsData"].append(common.DSData(**dsrecord)) except KeyError: # no server information in this field, skip it pass From 4399aa058cabceaa31e6623c8e47c63dac8db015 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 2 Oct 2023 19:57:09 -0400 Subject: [PATCH 11/65] scss refactor, DS record layout, WIP add new ds record js --- src/registrar/assets/js/get-gov.js | 40 +- src/registrar/assets/sass/_theme/_alerts.scss | 17 + src/registrar/assets/sass/_theme/_base.scss | 105 ++++ .../assets/sass/_theme/_buttons.scss | 73 +++ .../assets/sass/_theme/_fieldsets.scss | 11 + src/registrar/assets/sass/_theme/_forms.scss | 15 + .../assets/sass/_theme/_register-form.scss | 80 +++ .../assets/sass/_theme/_sidenav.scss | 30 ++ src/registrar/assets/sass/_theme/_tables.scss | 93 ++++ .../assets/sass/_theme/_typography.scss | 24 + .../_theme/_uswds-theme-custom-styles.scss | 457 ------------------ src/registrar/assets/sass/_theme/styles.scss | 10 +- src/registrar/forms/__init__.py | 1 + src/registrar/forms/domain.py | 5 + src/registrar/templates/domain_dsdata.html | 46 +- src/registrar/views/domain.py | 11 +- 16 files changed, 537 insertions(+), 481 deletions(-) create mode 100644 src/registrar/assets/sass/_theme/_alerts.scss create mode 100644 src/registrar/assets/sass/_theme/_base.scss create mode 100644 src/registrar/assets/sass/_theme/_buttons.scss create mode 100644 src/registrar/assets/sass/_theme/_fieldsets.scss create mode 100644 src/registrar/assets/sass/_theme/_forms.scss create mode 100644 src/registrar/assets/sass/_theme/_register-form.scss create mode 100644 src/registrar/assets/sass/_theme/_sidenav.scss create mode 100644 src/registrar/assets/sass/_theme/_tables.scss create mode 100644 src/registrar/assets/sass/_theme/_typography.scss delete mode 100644 src/registrar/assets/sass/_theme/_uswds-theme-custom-styles.scss diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 57dc6d2e3..1ffdeb1c6 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -236,14 +236,15 @@ function handleValidationClick(e) { * Only does something on a single page, but it should be fast enough to run * it everywhere. */ -(function prepareForms() { +(function prepareNameserverForms() { let serverForm = document.querySelectorAll(".server-form") let container = document.querySelector("#form-container") let addButton = document.querySelector("#add-form") let totalForms = document.querySelector("#id_form-TOTAL_FORMS") let formNum = serverForm.length-1 - addButton.addEventListener('click', addForm) + if (addButton) + addButton.addEventListener('click', addForm) function addForm(e){ let newForm = serverForm[2].cloneNode(true) @@ -261,3 +262,38 @@ function handleValidationClick(e) { totalForms.setAttribute('value', `${formNum+1}`) } })(); + + +/** + * An IIFE that attaches a click handler for our dynamic DNSSEC forms + * + */ +(function prepareDNSSECForms() { + let serverForm2 = document.querySelectorAll(".ds-record") + let container = document.querySelector("#form-container") + let addButton = document.querySelector("#add-form2") + let totalForms = document.querySelector("#id_form-TOTAL_FORMS") + + let formNum = serverForm2.length-1 + if (addButton) { + console.log('add button exists') + addButton.addEventListener('click', addForm) + } + + function addForm(e){ + console.log('add button clicked' + serverForm2) + let newForm = serverForm2[0].cloneNode(true) + let formNumberRegex = RegExp(`form-(\\d){1}-`,'g') + let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g') + // let formExampleRegex = RegExp(`ns(\\d){1}`, 'g') + + formNum++ + newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum}-`) + newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `DS Data Record ${formNum+1}`) + // newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum+1}`) + container.insertBefore(newForm, addButton) + newForm.querySelector("input").value = "" + + totalForms.setAttribute('value', `${formNum+1}`) + } +})(); diff --git a/src/registrar/assets/sass/_theme/_alerts.scss b/src/registrar/assets/sass/_theme/_alerts.scss new file mode 100644 index 000000000..077708990 --- /dev/null +++ b/src/registrar/assets/sass/_theme/_alerts.scss @@ -0,0 +1,17 @@ +// Fixes some font size disparities with the Figma +// for usa-alert alert elements +.usa-alert { + .usa-alert__heading.larger-font-sizing { + font-size: units(3); + } +} + +// The icon was off center for some reason +// Fixes that issue +@media (min-width: 64em){ + .usa-alert--warning{ + .usa-alert__body::before { + left: 1rem !important; + } + } +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss new file mode 100644 index 000000000..fb3b051f6 --- /dev/null +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -0,0 +1,105 @@ +@use "uswds-core" as *; + +/* Styles for making visible to screen reader / AT users only. */ +.sr-only { + @include sr-only; +} + +* { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +#wrapper { + flex-grow: 1; + padding-top: units(3); + padding-bottom: units(6) * 2 ; //Workaround because USWDS units jump from 10 to 15 +} + +#wrapper.dashboard { + background-color: color('primary-lightest'); + padding-top: units(5); +} + +.usa-logo { + @include at-media(desktop) { + margin-top: units(2); + } +} + +.usa-logo__text { + @include typeset('sans', 'xl', 2); + color: color('primary-darker'); +} + +.usa-nav__primary { + margin-top: units(1); +} + +.section--outlined { + background-color: color('white'); + border: 1px solid color('base-lighter'); + border-radius: 4px; + padding: 0 units(2) units(3); + margin-top: units(3); + + h2 { + color: color('primary-dark'); + margin-top: units(2); + margin-bottom: units(2); + } + + p { + margin-bottom: 0; + } + + @include at-media(mobile-lg) { + margin-top: units(5); + + h2 { + margin-bottom: 0; + } + } +} + +.break-word { + word-break: break-word; +} + +.dotgov-status-box { + background-color: color('primary-lightest'); + border-color: color('accent-cool-lighter'); +} + +.dotgov-status-box--action-need { + background-color: color('warning-lighter'); + border-color: color('warning'); +} + +footer { + border-top: 1px solid color('primary-darker'); +} + +.usa-footer__secondary-section { + background-color: color('primary-lightest'); +} + +.usa-footer__secondary-section a { + color: color('primary'); +} + +.usa-identifier__logo { + height: units(7); +} + +abbr[title] { + // workaround for underlining abbr element + border-bottom: none; + text-decoration: none; +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss new file mode 100644 index 000000000..2996aa023 --- /dev/null +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -0,0 +1,73 @@ +@use "uswds-core" as *; + +/* Make "placeholder" links visually obvious */ +a[href$="todo"]::after { + background-color: yellow; + color: color(blue-80v); + content: " [link TBD]"; + font-style: italic; +} + +a.breadcrumb__back { + display:flex; + align-items: center; + margin-bottom: units(2.5); + &:visited { + color: color('primary'); + } + + @include at-media('tablet') { + //align to top of sidebar + margin-top: units(-0.5); + } +} + +a.withdraw { + background-color: color('error'); +} + +a.withdraw_outline, +a.withdraw_outline:visited { + box-shadow: inset 0 0 0 2px color('error'); + color: color('error'); +} + +a.withdraw_outline:hover, +a.withdraw_outline:focus { + box-shadow: inset 0 0 0 2px color('error-dark'); + color: color('error-dark'); +} + +a.withdraw_outline:active { + box-shadow: inset 0 0 0 2px color('error-darker'); + color: color('error-darker'); +} + +a.withdraw:hover, +a.withdraw:focus { + background-color: color('error-dark'); +} + +a.withdraw:active { + background-color: color('error-darker'); +} + +.usa-button--unstyled .usa-icon { + vertical-align: bottom; +} + +a.usa-button--unstyled:visited { + color: color('primary'); +} + +.dotgov-button--green { + background-color: color('success-dark'); + + &:hover { + background-color: color('success-darker'); + } + + &:active { + background-color: color('green-80v'); + } +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_fieldsets.scss b/src/registrar/assets/sass/_theme/_fieldsets.scss new file mode 100644 index 000000000..9aab8010c --- /dev/null +++ b/src/registrar/assets/sass/_theme/_fieldsets.scss @@ -0,0 +1,11 @@ +@use "uswds-core" as *; + +fieldset { + border: solid 1px color('base-lighter'); + padding: units(3); +} + +fieldset legend { + font-weight: font-weight('bold'); + color: color('primary') +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss new file mode 100644 index 000000000..0929a5091 --- /dev/null +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -0,0 +1,15 @@ +@use "uswds-core" as *; + +.usa-form .usa-button { + margin-top: units(3); +} + +.usa-form--extra-large { + max-width: none; +} + +.usa-textarea { + @include at-media('tablet') { + height: units('mobile'); + } +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_register-form.scss b/src/registrar/assets/sass/_theme/_register-form.scss new file mode 100644 index 000000000..7bf4eebf3 --- /dev/null +++ b/src/registrar/assets/sass/_theme/_register-form.scss @@ -0,0 +1,80 @@ +@use "uswds-core" as *; +@use "typography" as *; + +.register-form-step > h1 { + //align to top of sidebar on first page of the form + margin-top: units(-1); +} + + //Tighter spacing when H2 is immediatly after H1 +.register-form-step .usa-fieldset:first-of-type h2:first-of-type, +.register-form-step h1 + h2 { + margin-top: units(1); +} + +.register-form-step h3 { + color: color('primary-dark'); + letter-spacing: $letter-space--xs; + margin-top: units(3); + margin-bottom: 0; + + + p { + margin-top: units(0.5); + } +} + +.register-form-step h4 { + margin-bottom: 0; + + + p { + margin-top: units(0.5); + } +} + +.register-form-step a { + color: color('primary'); + + &:visited { + color: color('violet-70v'); //USWDS default + } +} +.register-form-step .usa-form-group:first-of-type, +.register-form-step .usa-label:first-of-type { + margin-top: units(1); +} + +.ao_example p { + margin-top: units(1); +} + +.domain_example { + p { + margin-bottom: 0; + } + + .usa-list { + margin-top: units(0.5); + } +} + +.review__step { + margin-top: units(3); +} + + .summary-item hr, +.review__step hr { + border: none; //reset + border-top: 1px solid color('primary-dark'); + margin-top: 0; + margin-bottom: units(0.5); +} + +.review__step__title a:visited { + color: color('primary'); +} + +.review__step__name { + color: color('primary-dark'); + font-weight: font-weight('semibold'); + margin-bottom: units(0.5); +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_sidenav.scss b/src/registrar/assets/sass/_theme/_sidenav.scss new file mode 100644 index 000000000..c1be03a00 --- /dev/null +++ b/src/registrar/assets/sass/_theme/_sidenav.scss @@ -0,0 +1,30 @@ +@use "uswds-core" as *; + +.usa-sidenav { + .usa-sidenav__item { + span { + a.link_usa-checked { + padding: 0; + } + } + } +} + +.sidenav__step--locked { + color: color('base-darker'); + span { + display: flex; + align-items: flex-start; + padding: units(1); + + .usa-icon { + flex-shrink: 0; + //align lock body to x-height + margin: units('2px') units(1) 0 0; + } + } +} + +.stepnav { + margin-top: units(2); +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/sass/_theme/_tables.scss new file mode 100644 index 000000000..947c75005 --- /dev/null +++ b/src/registrar/assets/sass/_theme/_tables.scss @@ -0,0 +1,93 @@ +@use "uswds-core" as *; + +.dotgov-table--stacked { + td, th { + padding: units(1) units(2) units(2px) 0; + border: none; + } + + tr:first-child th:first-child { + border-top: none; + } + + tr { + border-bottom: none; + border-top: 2px solid color('base-light'); + margin-top: units(2); + + &:first-child { + margin-top: 0; + } + } + + td[data-label]:before, + th[data-label]:before { + color: color('primary-darker'); + padding-bottom: units(2px); + } +} + +.dotgov-table { + width: 100%; + + a { + display: flex; + align-items: flex-start; + color: color('primary'); + + &:visited { + color: color('primary'); + } + + .usa-icon { + // align icon with x height + margin-top: units(0.5); + margin-right: units(0.5); + } + } + + th[data-sortable]:not([aria-sort]) .usa-table__header__button { + right: auto; + } + + tbody th { + word-break: break-word; + } + + @include at-media(mobile-lg) { + + margin-top: units(1); + + tr { + border: none; + } + + td, th { + border-bottom: 1px solid color('base-light'); + } + + thead th { + color: color('primary-darker'); + border-bottom: 2px solid color('base-light'); + } + + tbody tr:last-of-type { + td, th { + border-bottom: 0; + } + } + + td, th, + .usa-tabel th{ + padding: units(2) units(2) units(2) 0; + } + + th:first-of-type { + padding-left: 0; + } + + thead tr:first-child th:first-child { + border-top: none; + } + } +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_typography.scss b/src/registrar/assets/sass/_theme/_typography.scss new file mode 100644 index 000000000..e13e9ee3b --- /dev/null +++ b/src/registrar/assets/sass/_theme/_typography.scss @@ -0,0 +1,24 @@ +@use "uswds-core" as *; + +// Finer grained letterspacing adjustments +$letter-space--xs: .0125em; + +p, +address, +.usa-list li { + @include typeset('sans', 'sm', 5); + max-width: measure(5); +} + +h1 { + @include typeset('sans', '2xl', 2); + margin: 0 0 units(2); + color: color('primary-darker'); +} + +h2 { + font-weight: font-weight('semibold'); + line-height: line-height('heading', 3); + margin: units(4) 0 units(1); + color: color('primary-darker'); +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_uswds-theme-custom-styles.scss b/src/registrar/assets/sass/_theme/_uswds-theme-custom-styles.scss deleted file mode 100644 index e69b36bb8..000000000 --- a/src/registrar/assets/sass/_theme/_uswds-theme-custom-styles.scss +++ /dev/null @@ -1,457 +0,0 @@ -/* -* * * * * ============================== -* * * * * ============================== -* * * * * ============================== -* * * * * ============================== -======================================== -======================================== -======================================== ----------------------------------------- -USWDS THEME CUSTOM STYLES ----------------------------------------- -!! Copy this file to your project's - sass root. Don't edit the version - in node_modules. ----------------------------------------- -Custom project SASS goes here. - -i.e. -@include u-padding-right('05'); ----------------------------------------- -*/ - -// Finer grained letterspacing adjustments -$letter-space--xs: .0125em; - -@use "uswds-core" as *; - -/* Styles for making visible to screen reader / AT users only. */ -.sr-only { - @include sr-only; - } - - * { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -#wrapper { - flex-grow: 1; -} - -.usa-logo { - @include at-media(desktop) { - margin-top: units(2); - } -} - -.usa-logo__text { - @include typeset('sans', 'xl', 2); - color: color('primary-darker'); -} - -.usa-nav__primary { - margin-top: units(1); -} - -p, -address, -.usa-list li { - @include typeset('sans', 'sm', 5); - max-width: measure(5); -} - -h1 { - @include typeset('sans', '2xl', 2); - margin: 0 0 units(2); - color: color('primary-darker'); -} - -h2 { - font-weight: font-weight('semibold'); - line-height: line-height('heading', 3); - margin: units(4) 0 units(1); - color: color('primary-darker'); -} - -.register-form-step > h1 { - //align to top of sidebar on first page of the form - margin-top: units(-1); -} - - //Tighter spacing when H2 is immediatly after H1 -.register-form-step .usa-fieldset:first-of-type h2:first-of-type, -.register-form-step h1 + h2 { - margin-top: units(1); -} - -.register-form-step h3 { - color: color('primary-dark'); - letter-spacing: $letter-space--xs; - margin-top: units(3); - margin-bottom: 0; - - + p { - margin-top: units(0.5); - } -} - -.register-form-step h4 { - margin-bottom: 0; - - + p { - margin-top: units(0.5); - } -} - - -.register-form-step a { - color: color('primary'); - - &:visited { - color: color('violet-70v'); //USWDS default - } -} -.register-form-step .usa-form-group:first-of-type, -.register-form-step .usa-label:first-of-type { - margin-top: units(1); -} - -/* Make "placeholder" links visually obvious */ -a[href$="todo"]::after { - background-color: yellow; - color: color(blue-80v); - content: " [link TBD]"; - font-style: italic; -} - -a.breadcrumb__back { - display:flex; - align-items: center; - margin-bottom: units(2.5); - &:visited { - color: color('primary'); - } - - @include at-media('tablet') { - //align to top of sidebar - margin-top: units(-0.5); - } -} - -a.withdraw { - background-color: color('error'); -} - -a.withdraw_outline, -a.withdraw_outline:visited { - box-shadow: inset 0 0 0 2px color('error'); - color: color('error'); -} - -a.withdraw_outline:hover, -a.withdraw_outline:focus { - box-shadow: inset 0 0 0 2px color('error-dark'); - color: color('error-dark'); -} - -a.withdraw_outline:active { - box-shadow: inset 0 0 0 2px color('error-darker'); - color: color('error-darker'); -} -a.withdraw:hover, -a.withdraw:focus { - background-color: color('error-dark'); -} - -a.withdraw:active { - background-color: color('error-darker'); -} - -.usa-sidenav { - .usa-sidenav__item { - span { - a.link_usa-checked { - padding: 0; - } - } - } -} - -.sidenav__step--locked { - color: color('base-darker'); - span { - display: flex; - align-items: flex-start; - padding: units(1); - - .usa-icon { - flex-shrink: 0; - //align lock body to x-height - margin: units('2px') units(1) 0 0; - } - } -} - - -.stepnav { - margin-top: units(2); -} - -.ao_example p { - margin-top: units(1); -} - -.domain_example { - p { - margin-bottom: 0; - } - - .usa-list { - margin-top: units(0.5); - } -} - -.review__step { - margin-top: units(3); -} - -.summary-item hr, -.review__step hr { - border: none; //reset - border-top: 1px solid color('primary-dark'); - margin-top: 0; - margin-bottom: units(0.5); -} - -.review__step__title a:visited { - color: color('primary'); -} - -.review__step__name { - color: color('primary-dark'); - font-weight: font-weight('semibold'); - margin-bottom: units(0.5); -} - -.usa-form .usa-button { - margin-top: units(3); -} - -.usa-button--unstyled .usa-icon { - vertical-align: bottom; -} - -a.usa-button--unstyled:visited { - color: color('primary'); -} - -.dotgov-button--green { - background-color: color('success-dark'); - - &:hover { - background-color: color('success-darker'); - } - - &:active { - background-color: color('green-80v'); - } -} - -/** ---- DASHBOARD ---- */ - -#wrapper.dashboard { - background-color: color('primary-lightest'); - padding-top: units(5); -} - -.section--outlined { - background-color: color('white'); - border: 1px solid color('base-lighter'); - border-radius: 4px; - padding: 0 units(2) units(3); - margin-top: units(3); - - h2 { - color: color('primary-dark'); - margin-top: units(2); - margin-bottom: units(2); - } - - p { - margin-bottom: 0; - } - - @include at-media(mobile-lg) { - margin-top: units(5); - - h2 { - margin-bottom: 0; - } - } -} - -.dotgov-table--stacked { - td, th { - padding: units(1) units(2) units(2px) 0; - border: none; - } - - tr:first-child th:first-child { - border-top: none; - } - - tr { - border-bottom: none; - border-top: 2px solid color('base-light'); - margin-top: units(2); - - &:first-child { - margin-top: 0; - } - } - - td[data-label]:before, - th[data-label]:before { - color: color('primary-darker'); - padding-bottom: units(2px); - } -} - -.dotgov-table { - width: 100%; - - a { - display: flex; - align-items: flex-start; - color: color('primary'); - - &:visited { - color: color('primary'); - } - - .usa-icon { - // align icon with x height - margin-top: units(0.5); - margin-right: units(0.5); - } - } - - th[data-sortable]:not([aria-sort]) .usa-table__header__button { - right: auto; - } - - tbody th { - word-break: break-word; - } - - - @include at-media(mobile-lg) { - - margin-top: units(1); - - tr { - border: none; - } - - td, th { - border-bottom: 1px solid color('base-light'); - } - - thead th { - color: color('primary-darker'); - border-bottom: 2px solid color('base-light'); - } - - tbody tr:last-of-type { - td, th { - border-bottom: 0; - } - } - - td, th, - .usa-tabel th{ - padding: units(2) units(2) units(2) 0; - } - - th:first-of-type { - padding-left: 0; - } - - thead tr:first-child th:first-child { - border-top: none; - } - } -} - -.break-word { - word-break: break-word; -} - -.dotgov-status-box { - background-color: color('primary-lightest'); - border-color: color('accent-cool-lighter'); -} - -.dotgov-status-box--action-need { - background-color: color('warning-lighter'); - border-color: color('warning'); -} - -#wrapper { - padding-top: units(3); - padding-bottom: units(6) * 2 ; //Workaround because USWDS units jump from 10 to 15 -} - - -footer { - border-top: 1px solid color('primary-darker'); -} - -.usa-footer__secondary-section { - background-color: color('primary-lightest'); -} - -.usa-footer__secondary-section a { - color: color('primary'); -} - -.usa-identifier__logo { - height: units(7); -} - -abbr[title] { - // workaround for underlining abbr element - border-bottom: none; - text-decoration: none; -} - -.usa-textarea { - @include at-media('tablet') { - height: units('mobile'); - } -} - -// Fixes some font size disparities with the Figma -// for usa-alert alert elements -.usa-alert { - .usa-alert__heading.larger-font-sizing { - font-size: units(3); - } -} - -// The icon was off center for some reason -// Fixes that issue -@media (min-width: 64em){ - .usa-alert--warning{ - .usa-alert__body::before { - left: 1rem !important; - } - } -} diff --git a/src/registrar/assets/sass/_theme/styles.scss b/src/registrar/assets/sass/_theme/styles.scss index 27d844760..8a2e1d2d3 100644 --- a/src/registrar/assets/sass/_theme/styles.scss +++ b/src/registrar/assets/sass/_theme/styles.scss @@ -8,7 +8,15 @@ /*-------------------------------------------------- --- Custom Styles ---------------------------------*/ -@forward "uswds-theme-custom-styles"; +@forward "base"; +@forward "typography"; +@forward "buttons"; +@forward "forms"; +@forward "fieldsets"; +@forward "alerts"; +@forward "tables"; +@forward "sidenav"; +@forward "register-form"; /*-------------------------------------------------- --- Admin ---------------------------------*/ diff --git a/src/registrar/forms/__init__.py b/src/registrar/forms/__init__.py index e889405c9..c83c1e636 100644 --- a/src/registrar/forms/__init__.py +++ b/src/registrar/forms/__init__.py @@ -6,4 +6,5 @@ from .domain import ( DomainOrgNameAddressForm, ContactForm, DomainDsdataFormset, + DomainDsdataForm, ) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index ea74db467..9f3cd2ee1 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -209,6 +209,11 @@ class DomainDsdataForm(forms.Form): # ) # ], ) + + delete = forms.BooleanField( + required=False, + label="Delete", + ) # TODO: Conditional DS Key Data fields diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 147fcde64..13e3debe8 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -9,28 +9,42 @@ {% include "includes/required_fields.html" %} -
                  + {% csrf_token %} {{ formset.management_form }} {% for form in formset %} -
                  - {% with attr_required=True %} - {% input_with_errors form.key_tag %} - {% endwith %} - {% with attr_required=True %} - {% input_with_errors form.algorithm %} - {% endwith %} - {% with attr_required=True %} - {% input_with_errors form.digest_type %} - {% endwith %} - {% with attr_required=True %} - {% input_with_errors form.digest %} - {% endwith %} -
                  +
                  + + DS Data record {{forloop.counter}} + +
                  +
                  + {% with attr_required=True %} + {% input_with_errors form.key_tag %} + {% endwith %} +
                  +
                  + {% with attr_required=True %} + {% input_with_errors form.algorithm %} + {% endwith %} +
                  +
                  + {% with attr_required=True %} + {% input_with_errors form.digest_type %} + {% endwith %} +
                  +
                  + {% with attr_required=True %} + {% input_with_errors form.digest %} + {% endwith %} + + {% comment %} {% input_with_errors form.delete_checkbox %} {% endcomment %} + +
                  {% endfor %} - + {% elif domain.dnssecdata is None %} +

                  Add DS Records

                  +

                  In order to enable DNSSEC and add Delegation Signer (DS) records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.

                  +

                  + Add DS Data + Add DS Data + +

                  + {% else %} +
                  +
                  + In order to fully disable DNSSEC on your domain, you will need to work with your DNS provider to remove your DNSSEC-related records from your zone. +
                  +
                  + + {% endif %} +
                  {% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index d3dc90a55..e81186c43 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -43,6 +43,7 @@ > DNSSEC + {% if domain.dnssec_enabled %}
                  • {% url 'domain-dns-dnssec-dsdata' pk=domain.id as url %} @@ -62,6 +63,7 @@
                  + {% endif %}
              • diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 38928a70f..5094c2bed 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -28,6 +28,7 @@ from ..forms import ( DomainAddUserForm, DomainSecurityEmailForm, NameserverFormset, + DomainDnssecForm, DomainDsdataFormset, DomainDsdataForm, ) @@ -229,11 +230,32 @@ class DomainSubdomainsView(DomainPermissionView): template_name = "domain_subdomains.html" -class DomainDNSSECView(DomainPermissionView): +class DomainDNSSECView(DomainPermissionView, FormMixin): """Domain DNSSEC editing view.""" template_name = "domain_dnssec.html" + form_class = DomainDnssecForm + + def get_success_url(self): + """Redirect to the DNSSEC page for the domain.""" + return reverse("domain-dns-dnssec", kwargs={"pk": self.domain.pk}) + + def post(self, request, *args, **kwargs): + """Form submission posts to this view. + """ + self.domain = self.get_object() + form = self.get_form() + if form.is_valid(): + if 'enable_dnssec' in request.POST: + self.domain.dnssec_enabled = True + self.domain.save() + elif 'disable_dnssec' in request.POST: + self.domain.dnssecdata = {} + self.domain.dnssec_enabled = False + self.domain.save() + + return self.form_valid(form) class DomainDsdataView(DomainPermissionView, FormMixin): From f3fcc19de878e0cc02f59f2d66a7f09de740d726 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 3 Oct 2023 11:16:31 -0400 Subject: [PATCH 14/65] roughed in key data form and template, etc --- src/registrar/forms/__init__.py | 2 + src/registrar/forms/common.py | 19 ++++ src/registrar/forms/domain.py | 74 ++++++++----- src/registrar/templates/domain_dsdata.html | 12 ++- src/registrar/templates/domain_keydata.html | 71 +++++++++++++ src/registrar/views/domain.py | 109 +++++++++++++++++++- 6 files changed, 262 insertions(+), 25 deletions(-) create mode 100644 src/registrar/forms/common.py diff --git a/src/registrar/forms/__init__.py b/src/registrar/forms/__init__.py index c3aa89fed..7d2baf646 100644 --- a/src/registrar/forms/__init__.py +++ b/src/registrar/forms/__init__.py @@ -8,4 +8,6 @@ from .domain import ( DomainDnssecForm, DomainDsdataFormset, DomainDsdataForm, + DomainKeydataFormset, + DomainKeydataForm, ) diff --git a/src/registrar/forms/common.py b/src/registrar/forms/common.py new file mode 100644 index 000000000..695cb9839 --- /dev/null +++ b/src/registrar/forms/common.py @@ -0,0 +1,19 @@ +# common.py +# Q: What are the options? +ALGORITHM_CHOICES = [ + (1, "ERSA/MD5 [RSAMD5]"), + (2 , "Diffie-Hellman [DH]"), + (3 ,"DSA/SHA-1 [DSA]"), + (5 ,"RSA/SHA-1 [RSASHA1]"), +] +# Q: What are the options? +DIGEST_TYPE_CHOICES = [ + (0, "Reserved"), + (1, "SHA-256"), +] +# Flag choices +FLAG_CHOICES = [ + (0, "0"), + (256, "256"), + (257, "257"), +] diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index d7a2bba95..f74c9d345 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -1,13 +1,13 @@ """Forms for domain management.""" from django import forms -from django.core.validators import RegexValidator +from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator from django.forms import formset_factory from phonenumber_field.widgets import RegionalPhoneNumberWidget from ..models import Contact, DomainInformation - +from .common import ALGORITHM_CHOICES, DIGEST_TYPE_CHOICES, FLAG_CHOICES class DomainAddUserForm(forms.Form): @@ -149,20 +149,6 @@ class DomainDnssecForm(forms.Form): class DomainDsdataForm(forms.Form): """Form for adding or editing a security email to a domain.""" - - # Q: What are the options? - ALGORITHM_CHOICES = [ - (1, "ERSA/MD5 [RSAMD5]"), - (2 , "Diffie-Hellman [DH]"), - (3 ,"DSA/SHA-1 [DSA]"), - (5 ,"RSA/SHA-1 [RSASHA1]"), - ] - # Q: What are the options? - DIGEST_TYPE_CHOICES = [ - (0, "Reserved"), - (1, "SHA-256"), - ] - # TODO: ds key data # has_ds_key_data = forms.TypedChoiceField( # required=True, @@ -174,10 +160,8 @@ class DomainDsdataForm(forms.Form): required=True, label="Key tag", validators=[ - RegexValidator( - "^[0-9]{5}(?:-[0-9]{4})?$|^$", - message="Accepted range 0-65535.", - ) + MinValueValidator(0, "Value must be between 0 and 65535"), + MaxValueValidator(65535, "Value must be between 0 and 65535"), ], ) @@ -230,7 +214,51 @@ DomainDsdataFormset = formset_factory( ) -# TODO: -# class DomainKeyDataForm(forms.Form): +class DomainKeydataForm(forms.Form): + + """Form for adding or editing DNSSEC key data.""" + # TODO: ds key data + # has_ds_key_data = forms.TypedChoiceField( + # required=True, + # label="DS Data record type", + # choices=[(False, "DS Data"), (True, "DS Data with Key Data")], + # ) + + flag = forms.TypedChoiceField( + required=True, + label="Flag", + choices=FLAG_CHOICES, + ) + + protocol = forms.IntegerField( + max_value=3, + min_value=3, + initial=3, + required=True, + disabled=True, + ) + + algorithm = forms.TypedChoiceField( + required=True, + label="Algorithm", + choices=[(None, "--Select--")] + ALGORITHM_CHOICES, + ) -# """""" \ No newline at end of file + pub_key = forms.CharField( + required=True, + label="Pub key", + ) + + delete = forms.BooleanField( + required=False, + label="Delete", + ) + + # TODO: Conditional DS Key Data fields + + + +DomainKeydataFormset = formset_factory( + DomainKeydataForm, + extra=1, +) \ No newline at end of file diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 33cb0bc49..e810be6f6 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -7,6 +7,16 @@

                DS Data

                + {% if domain.dnssecdata is not None and domain.dnssecdata.keyData is not None %} +
                +
                +

                Warning, you cannot add DS Data

                +

                + You cannot add DS Data because you have already added Key Data. Delete your Key Data records in order to add DS Data. +

                +
                +
                + {% else %} {% include "includes/required_fields.html" %}
                @@ -67,5 +77,5 @@ >Save
                - + {% endif %} {% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_keydata.html b/src/registrar/templates/domain_keydata.html index 6433d0c80..12c6f9082 100644 --- a/src/registrar/templates/domain_keydata.html +++ b/src/registrar/templates/domain_keydata.html @@ -7,4 +7,75 @@

                Key Data

                + {% if domain.dnssecdata is not None and domain.dnssecdata.dsData is not None %} +
                +
                +

                Warning, you cannot add Key Data

                +

                + You cannot add Key Data because you have already added DS Data. Delete your DS Data records in order to add Key Data. +

                +
                +
                + {% else %} + {% include "includes/required_fields.html" %} + +
                + {% csrf_token %} + {{ formset.management_form }} + + {% for form in formset %} +
                + + Key Data record {{forloop.counter}} + +
                +
                + {% with attr_required=True %} + {% input_with_errors form.flag %} + {% endwith %} +
                +
                + {% with attr_required=True %} + {% input_with_errors form.protocol %} + {% endwith %} +
                +
                + {% with attr_required=True %} + {% input_with_errors form.algorithm %} + {% endwith %} +
                +
                + +
                +
                + {% with attr_required=True %} + {% input_with_errors form.pub_key %} + {% endwith %} +
                +
                + +
                +
                + {% with add_group_class="float-right-tablet" %} + {% input_with_errors form.delete %} + {% endwith %} +
                +
                + +
                + {% endfor %} + + + + +
                + {% endif %} {% endblock %} {# domain_content #} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 5094c2bed..111e75812 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -31,6 +31,8 @@ from ..forms import ( DomainDnssecForm, DomainDsdataFormset, DomainDsdataForm, + DomainKeydataFormset, + DomainKeydataForm, ) from epplibwrapper import ( @@ -371,11 +373,116 @@ class DomainDsdataView(DomainPermissionView, FormMixin): -class DomainKeydataView(DomainPermissionView): +class DomainKeydataView(DomainPermissionView, FormMixin): """Domain DNSSEC key data editing view.""" template_name = "domain_keydata.html" + form_class = DomainKeydataFormset + form = DomainKeydataForm + + def get_initial(self): + """The initial value for the form (which is a formset here).""" + domain = self.get_object() + dnssecdata: extensions.DNSSECExtension = domain.dnssecdata + initial_data = [] + + if dnssecdata is not None: + + if dnssecdata.dsData is not None: + # TODO: Throw an error + pass + + if dnssecdata.keyData is not None: + # Add existing keydata as initial data + initial_data.extend({"flag": record.flags, "protocol": record.protocol, "algorithm": record.alg, "pub_key": record.pubKey} for record in dnssecdata.keyData) + + return initial_data + + def get_success_url(self): + """Redirect to the Key Data page for the domain.""" + return reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.object.pk}) + + def get_context_data(self, **kwargs): + """Adjust context from FormMixin for formsets.""" + context = super().get_context_data(**kwargs) + # use "formset" instead of "form" for the key + context["formset"] = context.pop("form") + return context + + def post(self, request, *args, **kwargs): + """Formset submission posts to this view.""" + self.object = self.get_object() + formset = self.get_form() + + if formset.is_valid(): + return self.form_valid(formset) + else: + # + # + # + # testing delete + try: + for form in formset: + if 'delete' in form.cleaned_data: + logger.debug(f"delete: {form.cleaned_data['delete']}") + else: + logger.debug(f"delete key does not exist, harcoding false") + except KeyError: + logger.debug(f"KeyError: {KeyError}") + # + # + # + # + + return self.form_invalid(formset) + + def form_valid(self, formset): + """The formset is valid, perform something with it.""" + + # Set the nameservers from the formset + dnssecdata = {"keyData":[]} + + for form in formset: + try: + # + # + # + # untested + if 'delete' in form.cleaned_data: + if form.cleaned_data['delete'] == False: + pass + else: + # delete key exists and is true, delete this record + logger.debug(f"delete: {form.cleaned_data['delete']}") + + else: + logger.debug(f"delete key does not exist, pass") + pass + # + # + # + # + + keyrecord = { + "flags": form.cleaned_data["flag"], + "protocol": form.cleaned_data["protocol"], + "alg": form.cleaned_data["algorithm"], + "pubKey": form.cleaned_data["pub_key"], + } + dnssecdata["keyData"].append(common.DNSSECKeyData(**keyrecord)) + except KeyError: + # no server information in this field, skip it + pass + domain = self.get_object() + domain.dnssecdata = dnssecdata + + messages.success( + self.request, "The Key Data records for this domain have been updated." + ) + + # superclass has the redirect + return super().form_valid(formset) class DomainYourContactInformationView(DomainPermissionView, FormMixin): From 0725b3c244203c75524f4ad1afe7e847ba32762e Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 3 Oct 2023 12:26:41 -0400 Subject: [PATCH 15/65] some updates to error handling, deletes, etc --- src/registrar/templates/domain_dnssec.html | 4 +- src/registrar/views/domain.py | 163 ++++++++------------- 2 files changed, 61 insertions(+), 106 deletions(-) diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index 675cbc087..032992014 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -27,11 +27,11 @@

                In order to enable DNSSEC and add Delegation Signer (DS) records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.

                Add DS Data - Add DS Data + Add Key Data

                {% else %} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 111e75812..5d3ca47c8 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -38,6 +38,7 @@ from ..forms import ( from epplibwrapper import ( common, extensions, + RegistryError, ) from ..utility.email import send_templated_email, EmailSendingError @@ -252,8 +253,18 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): if 'enable_dnssec' in request.POST: self.domain.dnssec_enabled = True self.domain.save() + elif 'cancel' in request.POST: + self.domain.dnssec_enabled = False + self.domain.save() elif 'disable_dnssec' in request.POST: - self.domain.dnssecdata = {} + try: + self.domain.dnssecdata = {} + except RegistryError as err: + errmsg = "Error removing existing DNSSEC record(s)." + logger.error(errmsg + ": " + err) + messages.error( + self.request, errmsg + ) self.domain.dnssec_enabled = False self.domain.save() @@ -305,23 +316,6 @@ class DomainDsdataView(DomainPermissionView, FormMixin): if formset.is_valid(): return self.form_valid(formset) else: - # - # - # - # testing delete - try: - for form in formset: - if 'delete' in form.cleaned_data: - logger.debug(f"delete: {form.cleaned_data['delete']}") - else: - logger.debug(f"delete key does not exist, harcoding false") - except KeyError: - logger.debug(f"KeyError: {KeyError}") - # - # - # - # - return self.form_invalid(formset) def form_valid(self, formset): @@ -332,45 +326,34 @@ class DomainDsdataView(DomainPermissionView, FormMixin): for form in formset: try: - # - # - # - # untested - if 'delete' in form.cleaned_data: - if form.cleaned_data['delete'] == False: - pass - else: - # delete key exists and is true, delete this record - logger.debug(f"delete: {form.cleaned_data['delete']}") - - else: - logger.debug(f"delete key does not exist, pass") - pass - # - # - # - # - - dsrecord = { - "keyTag": form.cleaned_data["key_tag"], - "alg": form.cleaned_data["algorithm"], - "digestType": form.cleaned_data["digest_type"], - "digest": form.cleaned_data["digest"], - } - dnssecdata["dsData"].append(common.DSData(**dsrecord)) + if 'delete' in form.cleaned_data and form.cleaned_data['delete'] == True: + dsrecord = { + "keyTag": form.cleaned_data["key_tag"], + "alg": form.cleaned_data["algorithm"], + "digestType": form.cleaned_data["digest_type"], + "digest": form.cleaned_data["digest"], + } + dnssecdata["dsData"].append(common.DSData(**dsrecord)) except KeyError: # no server information in this field, skip it pass domain = self.get_object() - domain.dnssecdata = dnssecdata - - messages.success( - self.request, "The DS Data records for this domain have been updated." - ) - - # superclass has the redirect - return super().form_valid(formset) - + try: + domain.dnssecdata = dnssecdata + except RegistryError as err: + errmsg = "Error updating DNSSEC data in the registry." + logger.error(f"{{ errmsg }}: {{ err }}") + messages.error( + self.request, errmsg + ) + return self.form_invalid(formset) + else: + messages.success( + self.request, "The DS Data records for this domain have been updated." + ) + # superclass has the redirect + return super().form_valid(formset) + class DomainKeydataView(DomainPermissionView, FormMixin): @@ -418,23 +401,6 @@ class DomainKeydataView(DomainPermissionView, FormMixin): if formset.is_valid(): return self.form_valid(formset) else: - # - # - # - # testing delete - try: - for form in formset: - if 'delete' in form.cleaned_data: - logger.debug(f"delete: {form.cleaned_data['delete']}") - else: - logger.debug(f"delete key does not exist, harcoding false") - except KeyError: - logger.debug(f"KeyError: {KeyError}") - # - # - # - # - return self.form_invalid(formset) def form_valid(self, formset): @@ -445,44 +411,33 @@ class DomainKeydataView(DomainPermissionView, FormMixin): for form in formset: try: - # - # - # - # untested - if 'delete' in form.cleaned_data: - if form.cleaned_data['delete'] == False: - pass - else: - # delete key exists and is true, delete this record - logger.debug(f"delete: {form.cleaned_data['delete']}") - - else: - logger.debug(f"delete key does not exist, pass") - pass - # - # - # - # - - keyrecord = { - "flags": form.cleaned_data["flag"], - "protocol": form.cleaned_data["protocol"], - "alg": form.cleaned_data["algorithm"], - "pubKey": form.cleaned_data["pub_key"], - } - dnssecdata["keyData"].append(common.DNSSECKeyData(**keyrecord)) + if 'delete' in form.cleaned_data and form.cleaned_data['delete'] == True: + keyrecord = { + "flags": form.cleaned_data["flag"], + "protocol": form.cleaned_data["protocol"], + "alg": form.cleaned_data["algorithm"], + "pubKey": form.cleaned_data["pub_key"], + } + dnssecdata["keyData"].append(common.DNSSECKeyData(**keyrecord)) except KeyError: # no server information in this field, skip it pass domain = self.get_object() - domain.dnssecdata = dnssecdata - - messages.success( - self.request, "The Key Data records for this domain have been updated." - ) - - # superclass has the redirect - return super().form_valid(formset) + try: + domain.dnssecdata = dnssecdata + except RegistryError as err: + errmsg = "Error updating DNSSEC data in the registry." + logger.error(f"{{ errmsg }}: {{ err }}") + messages.error( + self.request, errmsg + ) + return self.form_invalid(formset) + else: + messages.success( + self.request, "The Key Data records for this domain have been updated." + ) + # superclass has the redirect + return super().form_valid(formset) class DomainYourContactInformationView(DomainPermissionView, FormMixin): From faae7693c4b45cbda0833a607abb25485765fb29 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 3 Oct 2023 14:06:59 -0400 Subject: [PATCH 16/65] implemented new in domain model dnssec_ds_confirmed and dnssec_key_confirmed to control intial blurb msg on the add pages, cleaned up JS --- src/registrar/assets/js/get-gov.js | 41 ++++++++++++++++++- ...034_domain_dnssec_ds_confirmed_and_more.py | 28 +++++++++++++ src/registrar/models/domain.py | 10 +++++ src/registrar/templates/domain_dnssec.html | 2 +- src/registrar/templates/domain_dsdata.html | 10 +++++ src/registrar/templates/domain_keydata.html | 10 +++++ src/registrar/views/domain.py | 16 ++++++++ 7 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 src/registrar/migrations/0034_domain_dnssec_ds_confirmed_and_more.py diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 1ffdeb1c6..109d18b3d 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -292,7 +292,46 @@ function handleValidationClick(e) { newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `DS Data Record ${formNum+1}`) // newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum+1}`) container.insertBefore(newForm, addButton) - newForm.querySelector("input").value = "" + + let inputs = newForm.querySelectorAll("input"); + + // Reset the values of each input to blank + inputs.forEach((input) => { + input.classList.remove("usa-input--error"); + if (input.type === "text" || input.type === "number" || input.type === "password") { + input.value = ""; // Set the value to an empty string + + } else if (input.type === "checkbox" || input.type === "radio") { + input.checked = false; // Uncheck checkboxes and radios + } + }); + + let selects = newForm.querySelectorAll("select"); + + selects.forEach((select) => { + select.classList.remove("usa-input--error"); + select.selectedIndex = 0; // Set the value to an empty string + }); + + + let labels = newForm.querySelectorAll("label"); + labels.forEach((label) => { + label.classList.remove("usa-label--error"); + }); + + + let usaFormGroups = newForm.querySelectorAll(".usa-form-group"); + usaFormGroups.forEach((usaFormGroup) => { + usaFormGroup.classList.remove("usa-form-group--error"); + }); + + let usaErrorMessages = newForm.querySelectorAll(".usa-error-message"); + usaErrorMessages.forEach((usaErrorMessage) => { + let parentDiv = usaErrorMessage.closest('div'); + if (parentDiv) { + parentDiv.remove(); // Remove the parent div if it exists + } + }); totalForms.setAttribute('value', `${formNum+1}`) } diff --git a/src/registrar/migrations/0034_domain_dnssec_ds_confirmed_and_more.py b/src/registrar/migrations/0034_domain_dnssec_ds_confirmed_and_more.py new file mode 100644 index 000000000..72513a401 --- /dev/null +++ b/src/registrar/migrations/0034_domain_dnssec_ds_confirmed_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.1 on 2023-10-03 17:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0033_domain_dnssec_enabled"), + ] + + operations = [ + migrations.AddField( + model_name="domain", + name="dnssec_ds_confirmed", + field=models.BooleanField( + default=False, + help_text="Boolean indicating if DS record adding is confirmed", + ), + ), + migrations.AddField( + model_name="domain", + name="dnssec_key_confirmed", + field=models.BooleanField( + default=False, + help_text="Boolean indicating if Key record adding is confirmed", + ), + ), + ] diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 18ac8262f..0f2436bf7 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -711,6 +711,16 @@ class Domain(TimeStampedModel, DomainHelper): default=False, help_text="Boolean indicating if dnssec is enabled", ) + + dnssec_ds_confirmed = models.BooleanField( + default=False, + help_text="Boolean indicating if DS record adding is confirmed", + ) + + dnssec_key_confirmed = models.BooleanField( + default=False, + help_text="Boolean indicating if Key record adding is confirmed", + ) # ForeignKey on UserDomainRole creates a "permissions" member for # all of the user-roles that are in place for this domain diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index 032992014..95af919d3 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -9,7 +9,7 @@

                DNSSEC, or DNS Security Extensions, is additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your website, they can be certain that it's connecting to the correct server, preventing potential hijacking or tampering with your domain's records. Read more about DNSSEC and why it is important.

                -
                + {% csrf_token %} {% if not domain.dnssec_enabled %}
                diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index e810be6f6..667e9d97e 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -16,6 +16,16 @@

                + {% elif not domain.dnssec_ds_confirmed %} +

                In order to enable DNSSEC and add DS records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.

                + + {% csrf_token %} + +
                {% else %} {% include "includes/required_fields.html" %} diff --git a/src/registrar/templates/domain_keydata.html b/src/registrar/templates/domain_keydata.html index 12c6f9082..3583aeff7 100644 --- a/src/registrar/templates/domain_keydata.html +++ b/src/registrar/templates/domain_keydata.html @@ -16,6 +16,16 @@

                + {% elif not domain.dnssec_key_confirmed %} +

                In order to enable DNSSEC and add DS records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.

                +
                + {% csrf_token %} + +
                {% else %} {% include "includes/required_fields.html" %} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 5d3ca47c8..79d1fdc3f 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -255,6 +255,8 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): self.domain.save() elif 'cancel' in request.POST: self.domain.dnssec_enabled = False + self.domain.dnssec_ds_confirmed = False + self.domain.dnssec_key_confirmed = False self.domain.save() elif 'disable_dnssec' in request.POST: try: @@ -266,6 +268,8 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): self.request, errmsg ) self.domain.dnssec_enabled = False + self.domain.dnssec_ds_confirmed = False + self.domain.dnssec_key_confirmed = False self.domain.save() return self.form_valid(form) @@ -313,6 +317,12 @@ class DomainDsdataView(DomainPermissionView, FormMixin): self.object = self.get_object() formset = self.get_form() + if 'confirm-ds' in request.POST: + self.object.dnssec_ds_confirmed = True + self.object.dnssec_key_confirmed = False + self.object.save() + return super().form_valid(formset) + if formset.is_valid(): return self.form_valid(formset) else: @@ -397,6 +407,12 @@ class DomainKeydataView(DomainPermissionView, FormMixin): """Formset submission posts to this view.""" self.object = self.get_object() formset = self.get_form() + + if 'confirm-key' in request.POST: + self.object.dnssec_key_confirmed = True + self.object.dnssec_ds_confirmed = False + self.object.save() + return super().form_valid(formset) if formset.is_valid(): return self.form_valid(formset) From 35a455f6fa484f8d2b846fc3883ca4442bcee06f Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 3 Oct 2023 15:31:03 -0400 Subject: [PATCH 17/65] tweak layout --- src/registrar/assets/sass/_theme/_base.scss | 4 ++++ src/registrar/templates/domain_dsdata.html | 2 +- src/registrar/templates/domain_keydata.html | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 6c51db103..729049444 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -120,4 +120,8 @@ abbr[title] { .float-left-desktop { float: left; } +} + +.flex-end { + align-items: flex-end; } \ No newline at end of file diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 667e9d97e..6b2e2580f 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -38,7 +38,7 @@ DS Data record {{forloop.counter}} -
                +
                {% with attr_required=True %} {% input_with_errors form.key_tag %} diff --git a/src/registrar/templates/domain_keydata.html b/src/registrar/templates/domain_keydata.html index 3583aeff7..37fbbec51 100644 --- a/src/registrar/templates/domain_keydata.html +++ b/src/registrar/templates/domain_keydata.html @@ -38,7 +38,7 @@ Key Data record {{forloop.counter}} -
                +
                {% with attr_required=True %} {% input_with_errors form.flag %} From 84cc92cb0a0ffaff1d7d1c1e29f883e39958e372 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 3 Oct 2023 16:18:30 -0400 Subject: [PATCH 18/65] fixed form submission and error logging --- src/registrar/views/domain.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 79d1fdc3f..df5c1d9bf 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -336,7 +336,7 @@ class DomainDsdataView(DomainPermissionView, FormMixin): for form in formset: try: - if 'delete' in form.cleaned_data and form.cleaned_data['delete'] == True: + if 'delete' not in form.cleaned_data or form.cleaned_data['delete'] == False: dsrecord = { "keyTag": form.cleaned_data["key_tag"], "alg": form.cleaned_data["algorithm"], @@ -352,7 +352,8 @@ class DomainDsdataView(DomainPermissionView, FormMixin): domain.dnssecdata = dnssecdata except RegistryError as err: errmsg = "Error updating DNSSEC data in the registry." - logger.error(f"{{ errmsg }}: {{ err }}") + logger.error(errmsg) + logger.error(err) messages.error( self.request, errmsg ) @@ -427,7 +428,7 @@ class DomainKeydataView(DomainPermissionView, FormMixin): for form in formset: try: - if 'delete' in form.cleaned_data and form.cleaned_data['delete'] == True: + if 'delete' not in form.cleaned_data or form.cleaned_data['delete'] == False: keyrecord = { "flags": form.cleaned_data["flag"], "protocol": form.cleaned_data["protocol"], @@ -443,7 +444,8 @@ class DomainKeydataView(DomainPermissionView, FormMixin): domain.dnssecdata = dnssecdata except RegistryError as err: errmsg = "Error updating DNSSEC data in the registry." - logger.error(f"{{ errmsg }}: {{ err }}") + logger.error(errmsg) + logger.error(err) messages.error( self.request, errmsg ) From 7c8f5e77f106be95cf9ddfb68c78db1cb1413f0c Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 3 Oct 2023 16:56:12 -0400 Subject: [PATCH 19/65] fixed nameservers path --- src/registrar/templates/domain_detail.html | 2 +- src/registrar/templates/domain_dns.html | 2 +- src/registrar/tests/test_views.py | 12 ++++++------ src/registrar/views/domain.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 6a700b393..9390e1b16 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -27,7 +27,7 @@

                - {% url 'domain-nameservers' pk=domain.id as url %} + {% url 'domain-dns-nameservers' pk=domain.id as url %} {% if domain.nameservers|length > 0 %} {% include "includes/summary_item.html" with title='DNS name servers' value=domain.nameservers list='true' edit_link=url %} {% else %} diff --git a/src/registrar/templates/domain_dns.html b/src/registrar/templates/domain_dns.html index 9b3b7500b..c793df5e9 100644 --- a/src/registrar/templates/domain_dns.html +++ b/src/registrar/templates/domain_dns.html @@ -11,7 +11,7 @@

                You can enter your name services, as well as other DNS-related information, in the following sections:

                - {% url 'domain-nameservers' pk=domain.id as url %} + {% url 'domain-dns-nameservers' pk=domain.id as url %}

                DNS name servers

                {% url 'domain-subdomains' pk=domain.id as url %} diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index c78d3c7fa..c49ec8f3f 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1098,7 +1098,7 @@ class TestDomainPermissions(TestWithDomainPermissions): "domain", "domain-users", "domain-users-add", - "domain-nameservers", + "domain-dns-nameservers", "domain-org-name-address", "domain-authorizing-official", "domain-your-contact-information", @@ -1119,7 +1119,7 @@ class TestDomainPermissions(TestWithDomainPermissions): "domain", "domain-users", "domain-users-add", - "domain-nameservers", + "domain-dns-nameservers", "domain-org-name-address", "domain-authorizing-official", "domain-your-contact-information", @@ -1308,7 +1308,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): def test_domain_nameservers(self): """Can load domain's nameservers page.""" page = self.client.get( - reverse("domain-nameservers", kwargs={"pk": self.domain.id}) + reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}) ) self.assertContains(page, "Domain name servers") @@ -1319,7 +1319,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): Uses self.app WebTest because we need to interact with forms. """ nameservers_page = self.app.get( - reverse("domain-nameservers", kwargs={"pk": self.domain.id}) + reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}) ) session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) @@ -1329,7 +1329,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): self.assertEqual(result.status_code, 302) self.assertEqual( result["Location"], - reverse("domain-nameservers", kwargs={"pk": self.domain.id}), + reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}), ) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) page = result.follow() @@ -1342,7 +1342,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): Uses self.app WebTest because we need to interact with forms. """ nameservers_page = self.app.get( - reverse("domain-nameservers", kwargs={"pk": self.domain.id}) + reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}) ) session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index df5c1d9bf..d19d3401d 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -172,7 +172,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin): def get_success_url(self): """Redirect to the nameservers page for the domain.""" - return reverse("domain-nameservers", kwargs={"pk": self.object.pk}) + return reverse("domain-dns-nameservers", kwargs={"pk": self.object.pk}) def get_context_data(self, **kwargs): """Adjust context from FormMixin for formsets.""" From 150e37fe42d71f53caab6c94a7882324d132ed64 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 3 Oct 2023 17:26:16 -0400 Subject: [PATCH 20/65] polish JS, alg validation, polish layout --- src/registrar/assets/js/get-gov.js | 66 +++++++++---------- .../assets/sass/_theme/_buttons.scss | 43 ++++++++---- .../assets/sass/_theme/_fieldsets.scss | 5 +- src/registrar/assets/sass/_theme/_forms.scss | 6 ++ src/registrar/forms/common.py | 11 +++- src/registrar/forms/domain.py | 27 ++++---- src/registrar/templates/domain_dnssec.html | 2 +- src/registrar/templates/domain_dsdata.html | 18 +++-- src/registrar/templates/domain_keydata.html | 20 +++--- src/registrar/views/domain.py | 32 ++++----- 10 files changed, 134 insertions(+), 96 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 109d18b3d..fe248c442 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -285,53 +285,47 @@ function handleValidationClick(e) { let newForm = serverForm2[0].cloneNode(true) let formNumberRegex = RegExp(`form-(\\d){1}-`,'g') let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g') - // let formExampleRegex = RegExp(`ns(\\d){1}`, 'g') formNum++ newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum}-`) newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `DS Data Record ${formNum+1}`) - // newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum+1}`) container.insertBefore(newForm, addButton) let inputs = newForm.querySelectorAll("input"); + // Reset the values of each input to blank + inputs.forEach((input) => { + input.classList.remove("usa-input--error"); + if (input.type === "text" || input.type === "number" || input.type === "password") { + input.value = ""; // Set the value to an empty string + + } else if (input.type === "checkbox" || input.type === "radio") { + input.checked = false; // Uncheck checkboxes and radios + } + }); - // Reset the values of each input to blank - inputs.forEach((input) => { - input.classList.remove("usa-input--error"); - if (input.type === "text" || input.type === "number" || input.type === "password") { - input.value = ""; // Set the value to an empty string - - } else if (input.type === "checkbox" || input.type === "radio") { - input.checked = false; // Uncheck checkboxes and radios - } - }); + let selects = newForm.querySelectorAll("select"); + selects.forEach((select) => { + select.classList.remove("usa-input--error"); + select.selectedIndex = 0; // Set the value to an empty string + }); - let selects = newForm.querySelectorAll("select"); + let labels = newForm.querySelectorAll("label"); + labels.forEach((label) => { + label.classList.remove("usa-label--error"); + }); - selects.forEach((select) => { - select.classList.remove("usa-input--error"); - select.selectedIndex = 0; // Set the value to an empty string - }); + let usaFormGroups = newForm.querySelectorAll(".usa-form-group"); + usaFormGroups.forEach((usaFormGroup) => { + usaFormGroup.classList.remove("usa-form-group--error"); + }); - - let labels = newForm.querySelectorAll("label"); - labels.forEach((label) => { - label.classList.remove("usa-label--error"); - }); - - - let usaFormGroups = newForm.querySelectorAll(".usa-form-group"); - usaFormGroups.forEach((usaFormGroup) => { - usaFormGroup.classList.remove("usa-form-group--error"); - }); - - let usaErrorMessages = newForm.querySelectorAll(".usa-error-message"); - usaErrorMessages.forEach((usaErrorMessage) => { - let parentDiv = usaErrorMessage.closest('div'); - if (parentDiv) { - parentDiv.remove(); // Remove the parent div if it exists - } - }); + let usaErrorMessages = newForm.querySelectorAll(".usa-error-message"); + usaErrorMessages.forEach((usaErrorMessage) => { + let parentDiv = usaErrorMessage.closest('div'); + if (parentDiv) { + parentDiv.remove(); // Remove the parent div if it exists + } + }); totalForms.setAttribute('value', `${formNum+1}`) } diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 2996aa023..2e58073b0 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -22,6 +22,27 @@ a.breadcrumb__back { } } +a.usa-button { + text-decoration: none; +} + +a.usa-button--outline, +a.usa-button--outline:visited { + box-shadow: inset 0 0 0 2px color('primary'); + color: color('primary'); +} + +a.usa-button--outline:hover, +a.usa-button--outline:focus { + box-shadow: inset 0 0 0 2px color('primary-dark'); + color: color('primary-dark'); +} + +a.usa-button--outline:active { + box-shadow: inset 0 0 0 2px color('primary-darker'); + color: color('primary-darker'); +} + a.withdraw { background-color: color('error'); } @@ -60,14 +81,14 @@ a.usa-button--unstyled:visited { color: color('primary'); } -.dotgov-button--green { - background-color: color('success-dark'); - - &:hover { - background-color: color('success-darker'); - } - - &:active { - background-color: color('green-80v'); - } -} \ No newline at end of file + .dotgov-button--green { + background-color: color('success-dark'); + + &:hover { + background-color: color('success-darker'); + } + + &:active { + background-color: color('green-80v'); + } + } \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_fieldsets.scss b/src/registrar/assets/sass/_theme/_fieldsets.scss index 9aab8010c..e7da538e3 100644 --- a/src/registrar/assets/sass/_theme/_fieldsets.scss +++ b/src/registrar/assets/sass/_theme/_fieldsets.scss @@ -5,7 +5,6 @@ fieldset { padding: units(3); } -fieldset legend { - font-weight: font-weight('bold'); - color: color('primary') +fieldset:not(:first-child) { + margin-top: units(2); } \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index 0929a5091..13e1a7a61 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -12,4 +12,10 @@ @include at-media('tablet') { height: units('mobile'); } +} + +.usa-form-group--unstyled-error { + margin-left: 0; + padding-left: 0; + border-left: none; } \ No newline at end of file diff --git a/src/registrar/forms/common.py b/src/registrar/forms/common.py index 695cb9839..71dbc24b2 100644 --- a/src/registrar/forms/common.py +++ b/src/registrar/forms/common.py @@ -1,10 +1,19 @@ # common.py -# Q: What are the options? +# reference: https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml ALGORITHM_CHOICES = [ (1, "ERSA/MD5 [RSAMD5]"), (2 , "Diffie-Hellman [DH]"), (3 ,"DSA/SHA-1 [DSA]"), (5 ,"RSA/SHA-1 [RSASHA1]"), + (6 ,"DSA-NSEC3-SHA1"), + (7 ,"RSASHA1-NSEC3-SHA1"), + (8 ,"RSA/SHA-256 [RSASHA256]"), + (10 ,"RSA/SHA-512 [RSASHA512]"), + (12 ,"GOST R 34.10-2001 [ECC-GOST]"), + (13 ,"ECDSA Curve P-256 with SHA-256 [ECDSAP256SHA256]"), + (14 ,"ECDSA Curve P-384 with SHA-384 [ECDSAP384SHA384]"), + (15 ,"Ed25519"), + (16 ,"Ed448"), ] # Q: What are the options? DIGEST_TYPE_CHOICES = [ diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index f74c9d345..e716e6f89 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -179,14 +179,15 @@ class DomainDsdataForm(forms.Form): # Q: Is ChoiceFiled right? Or do we need to data types other than strings # (TypedChoiceField) digest_type = forms.TypedChoiceField( + required=True, label="Digest Type", choices=[(None, "--Select--")] + DIGEST_TYPE_CHOICES, # Q: Is this even needed or is a required=True sufficient? - error_messages={ - "required": ( - "You must select a Digest Type" - ) - }, + # error_messages={ + # "required": ( + # "You must select a Digest Type" + # ) + # }, ) digest = forms.CharField( required=True, @@ -199,10 +200,10 @@ class DomainDsdataForm(forms.Form): # ], ) - delete = forms.BooleanField( - required=False, - label="Delete", - ) + # delete = forms.BooleanField( + # required=False, + # label="Delete", + # ) # TODO: Conditional DS Key Data fields @@ -249,10 +250,10 @@ class DomainKeydataForm(forms.Form): label="Pub key", ) - delete = forms.BooleanField( - required=False, - label="Delete", - ) + # delete = forms.BooleanField( + # required=False, + # label="Delete", + # ) # TODO: Conditional DS Key Data fields diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index 95af919d3..e21bb9458 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -30,7 +30,7 @@ Add Key Data

                diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 6b2e2580f..3eeeb8c68 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -27,6 +27,8 @@ >Add DS Data record {% else %} + +

                Enter the values given by your DNS provider for DS Data.

                {% include "includes/required_fields.html" %}
                @@ -36,21 +38,23 @@ {% for form in formset %}
                - DS Data record {{forloop.counter}} + DS Data record {{forloop.counter}} + +

                DS Data record {{forloop.counter}}

                - {% with attr_required=True %} + {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} {% input_with_errors form.key_tag %} {% endwith %}
                - {% with attr_required=True %} + {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} {% input_with_errors form.algorithm %} {% endwith %}
                - {% with attr_required=True %} + {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} {% input_with_errors form.digest_type %} {% endwith %}
                @@ -58,7 +62,7 @@
                - {% with attr_required=True %} + {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} {% input_with_errors form.digest %} {% endwith %}
                @@ -66,9 +70,9 @@
                - {% with add_group_class="float-right-tablet" %} + {% comment %} {% with add_group_class="float-right-tablet" %} {% input_with_errors form.delete %} - {% endwith %} + {% endwith %} {% endcomment %}
                diff --git a/src/registrar/templates/domain_keydata.html b/src/registrar/templates/domain_keydata.html index 37fbbec51..d9941a974 100644 --- a/src/registrar/templates/domain_keydata.html +++ b/src/registrar/templates/domain_keydata.html @@ -27,6 +27,8 @@ >Add DS Key record {% else %} + +

                Enter the values given by your DNS provider for DS Key Data.

                {% include "includes/required_fields.html" %}
                @@ -34,23 +36,25 @@ {{ formset.management_form }} {% for form in formset %} -
                +
                - Key Data record {{forloop.counter}} + Key Data record {{forloop.counter}} + +

                DS Data record {{forloop.counter}}

                - {% with attr_required=True %} + {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} {% input_with_errors form.flag %} {% endwith %}
                - {% with attr_required=True %} + {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} {% input_with_errors form.protocol %} {% endwith %}
                - {% with attr_required=True %} + {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} {% input_with_errors form.algorithm %} {% endwith %}
                @@ -59,16 +63,16 @@
                {% with attr_required=True %} - {% input_with_errors form.pub_key %} + {% input_with_errors form.pub_key add_group_class="usa-form-group--unstyled-error" %} {% endwith %}
                - {% with add_group_class="float-right-tablet" %} + {% comment %} {% with add_group_class="float-right-tablet" %} {% input_with_errors form.delete %} - {% endwith %} + {% endwith %} {% endcomment %}
                diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index df5c1d9bf..19eda2f3d 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -336,14 +336,14 @@ class DomainDsdataView(DomainPermissionView, FormMixin): for form in formset: try: - if 'delete' not in form.cleaned_data or form.cleaned_data['delete'] == False: - dsrecord = { - "keyTag": form.cleaned_data["key_tag"], - "alg": form.cleaned_data["algorithm"], - "digestType": form.cleaned_data["digest_type"], - "digest": form.cleaned_data["digest"], - } - dnssecdata["dsData"].append(common.DSData(**dsrecord)) + # if 'delete' not in form.cleaned_data or form.cleaned_data['delete'] == False: + dsrecord = { + "keyTag": form.cleaned_data["key_tag"], + "alg": form.cleaned_data["algorithm"], + "digestType": form.cleaned_data["digest_type"], + "digest": form.cleaned_data["digest"], + } + dnssecdata["dsData"].append(common.DSData(**dsrecord)) except KeyError: # no server information in this field, skip it pass @@ -428,14 +428,14 @@ class DomainKeydataView(DomainPermissionView, FormMixin): for form in formset: try: - if 'delete' not in form.cleaned_data or form.cleaned_data['delete'] == False: - keyrecord = { - "flags": form.cleaned_data["flag"], - "protocol": form.cleaned_data["protocol"], - "alg": form.cleaned_data["algorithm"], - "pubKey": form.cleaned_data["pub_key"], - } - dnssecdata["keyData"].append(common.DNSSECKeyData(**keyrecord)) + # if 'delete' not in form.cleaned_data or form.cleaned_data['delete'] == False: + keyrecord = { + "flags": form.cleaned_data["flag"], + "protocol": form.cleaned_data["protocol"], + "alg": form.cleaned_data["algorithm"], + "pubKey": form.cleaned_data["pub_key"], + } + dnssecdata["keyData"].append(common.DNSSECKeyData(**keyrecord)) except KeyError: # no server information in this field, skip it pass From 4a20fea492136f22b33833cd11b7af5380ad33f8 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 3 Oct 2023 17:43:39 -0400 Subject: [PATCH 21/65] updated to keydata template --- src/registrar/templates/domain_keydata.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/domain_keydata.html b/src/registrar/templates/domain_keydata.html index d9941a974..59b6846d4 100644 --- a/src/registrar/templates/domain_keydata.html +++ b/src/registrar/templates/domain_keydata.html @@ -62,8 +62,8 @@
                - {% with attr_required=True %} - {% input_with_errors form.pub_key add_group_class="usa-form-group--unstyled-error" %} + {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} + {% input_with_errors form.pub_key %} {% endwith %}
                From 80ef427931dbf02e4c6bfdd8797b5f52de655708 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 3 Oct 2023 18:35:32 -0400 Subject: [PATCH 22/65] delete button and pencil in a cancel button --- src/registrar/assets/js/get-gov.js | 61 ++++++++++++++++--- src/registrar/forms/domain.py | 2 - src/registrar/templates/domain_dsdata.html | 15 ++++- src/registrar/templates/domain_keydata.html | 17 +++++- .../templates/domain_nameservers.html | 2 +- 5 files changed, 82 insertions(+), 15 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index fe248c442..fbb243133 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -239,7 +239,7 @@ function handleValidationClick(e) { (function prepareNameserverForms() { let serverForm = document.querySelectorAll(".server-form") let container = document.querySelector("#form-container") - let addButton = document.querySelector("#add-form") + let addButton = document.querySelector("#add-nameserver-form") let totalForms = document.querySelector("#id_form-TOTAL_FORMS") let formNum = serverForm.length-1 @@ -263,26 +263,43 @@ function handleValidationClick(e) { } })(); +function prepareDeleteButtons() { + let serverForm2 = document.querySelectorAll(".ds-record") + let deleteButtons = document.querySelectorAll(".delete-record") + let totalForms = document.querySelector("#id_form-TOTAL_FORMS") + + let formNum = serverForm2.length-1 + // Loop through each delete button and attach the click event listener + deleteButtons.forEach((deleteButton) => { + deleteButton.addEventListener('click', removeForm); + }); + + function removeForm(e){ + console.log('lets delete stuff') + let formToRemove = e.target.closest(".ds-record") + formToRemove.remove() + formNum-- + totalForms.setAttribute('value', `${formNum+1}`) + } +} /** * An IIFE that attaches a click handler for our dynamic DNSSEC forms * */ (function prepareDNSSECForms() { - let serverForm2 = document.querySelectorAll(".ds-record") + let serverForm = document.querySelectorAll(".ds-record") let container = document.querySelector("#form-container") - let addButton = document.querySelector("#add-form2") + let addButton = document.querySelector("#add-ds-form") let totalForms = document.querySelector("#id_form-TOTAL_FORMS") - let formNum = serverForm2.length-1 + let formNum = serverForm.length-1 if (addButton) { - console.log('add button exists') addButton.addEventListener('click', addForm) } function addForm(e){ - console.log('add button clicked' + serverForm2) - let newForm = serverForm2[0].cloneNode(true) + let newForm = serverForm[0].cloneNode(true) let formNumberRegex = RegExp(`form-(\\d){1}-`,'g') let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g') @@ -328,5 +345,35 @@ function handleValidationClick(e) { }); totalForms.setAttribute('value', `${formNum+1}`) + + prepareDeleteButtons() } })(); + + +/** + * An IIFE that attaches a click handler on the record delete button + * + */ +(function doPrepareDeleteButtons() { + prepareDeleteButtons() +})(); + +/** + * An IIFE that attaches a click handler on form cancel buttons + * + */ +(function prepareCancelButtons() { + const cancelButton = document.querySelector('.btn-cancel'); + + if (cancelButton) { + cancelButton.addEventListener('click', () => { + // Option 1: Reset the form + const form = document.querySelector('form'); + form.reset(); + + // Option 2: Redirect to another page (e.g., the homepage) + // window.location.href = '/'; + }); + } +})(); \ No newline at end of file diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index e716e6f89..5f22e4de0 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -211,7 +211,6 @@ class DomainDsdataForm(forms.Form): DomainDsdataFormset = formset_factory( DomainDsdataForm, - extra=1, ) @@ -261,5 +260,4 @@ class DomainKeydataForm(forms.Form): DomainKeydataFormset = formset_factory( DomainKeydataForm, - extra=1, ) \ No newline at end of file diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 3eeeb8c68..241e45432 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -68,23 +68,34 @@
                -
                +
                {% comment %} {% with add_group_class="float-right-tablet" %} {% input_with_errors form.delete %} {% endwith %} {% endcomment %} +
                {% endfor %} - + +
                @@ -73,18 +73,29 @@ {% comment %} {% with add_group_class="float-right-tablet" %} {% input_with_errors form.delete %} {% endwith %} {% endcomment %} +
                {% endfor %} - + +
                + + {% comment %} {% endcomment %} + {% endfor %} @@ -90,17 +93,21 @@ Add new record - - + +
                + +
                {% endif %} {% endblock %} {# domain_content #} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 4fb6fec83..2c61248b6 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -11,6 +11,7 @@ from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin from django.db import IntegrityError from django.shortcuts import redirect +from django.template import RequestContext from django.urls import reverse from django.views.generic.edit import FormMixin @@ -322,6 +323,9 @@ class DomainDsdataView(DomainPermissionView, FormMixin): self.object.dnssec_key_confirmed = False self.object.save() return super().form_valid(formset) + + if 'btn-cancel-click' in request.POST: + return redirect('/', {'formset': formset},RequestContext(request)) if formset.is_valid(): return self.form_valid(formset) @@ -351,6 +355,7 @@ class DomainDsdataView(DomainPermissionView, FormMixin): try: domain.dnssecdata = dnssecdata except RegistryError as err: + # Alysia: Check client hold error handling and duplicate this here errmsg = "Error updating DNSSEC data in the registry." logger.error(errmsg) logger.error(err) From 945c54adc7d648fd6c7727cb82ffa8d30fbc8c07 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 4 Oct 2023 14:42:05 -0400 Subject: [PATCH 25/65] removed subdomains view and references --- src/registrar/config/urls.py | 5 ---- src/registrar/forms/domain.py | 26 ------------------- src/registrar/models/domain.py | 2 +- src/registrar/templates/domain_dns.html | 3 --- src/registrar/templates/domain_sidebar.html | 9 ------- .../templates/domain_subdomains.html | 16 ------------ src/registrar/views/__init__.py | 1 - src/registrar/views/domain.py | 7 ----- 8 files changed, 1 insertion(+), 68 deletions(-) delete mode 100644 src/registrar/templates/domain_subdomains.html diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 1487d372a..7caa64e5c 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -90,11 +90,6 @@ urlpatterns = [ views.DomainNameserversView.as_view(), name="domain-dns-nameservers", ), - path( - "domain//dns/subdomains", - views.DomainSubdomainsView.as_view(), - name="domain-dns-subdomains", - ), path( "domain//dns/dnssec", views.DomainDNSSECView.as_view(), diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index eb71a00fe..d0cbb9980 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -199,11 +199,6 @@ class DomainDsdataForm(forms.Form): # ) # ], ) - - # delete = forms.BooleanField( - # required=False, - # label="Delete", - # ) # TODO: Conditional DS Key Data fields @@ -217,12 +212,6 @@ DomainDsdataFormset = formset_factory( class DomainKeydataForm(forms.Form): """Form for adding or editing DNSSEC key data.""" - # TODO: ds key data - # has_ds_key_data = forms.TypedChoiceField( - # required=True, - # label="DS Data record type", - # choices=[(False, "DS Data"), (True, "DS Data with Key Data")], - # ) flag = forms.TypedChoiceField( required=True, @@ -235,13 +224,6 @@ class DomainKeydataForm(forms.Form): label="Protocol", choices=PROTOCOL_CHOICES, ) - # protocol = forms.IntegerField( - # max_value=3, - # min_value=3, - # initial=3, - # required=True, - # disabled=True, - # ) algorithm = forms.TypedChoiceField( required=True, @@ -253,15 +235,7 @@ class DomainKeydataForm(forms.Form): required=True, label="Pub key", ) - - # delete = forms.BooleanField( - # required=False, - # label="Delete", - # ) - # TODO: Conditional DS Key Data fields - - DomainKeydataFormset = formset_factory( DomainKeydataForm, diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 34f23261d..56a1a53cb 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -291,7 +291,7 @@ class Domain(TimeStampedModel, DomainHelper): return None @dnssecdata.setter # type: ignore - def dnssecdata(self, _dnssecdata: extensions.DNSSECExtension): + def dnssecdata(self, _dnssecdata: dict): updateParams = { "maxSigLife": _dnssecdata.get("maxSigLife", None), "dsData": _dnssecdata.get("dsData", None), diff --git a/src/registrar/templates/domain_dns.html b/src/registrar/templates/domain_dns.html index c793df5e9..0133a8b07 100644 --- a/src/registrar/templates/domain_dns.html +++ b/src/registrar/templates/domain_dns.html @@ -14,9 +14,6 @@ {% url 'domain-dns-nameservers' pk=domain.id as url %}

                DNS name servers

                - {% url 'domain-subdomains' pk=domain.id as url %} -

                Subdomain records

                - {% url 'domain-dnssec' pk=domain.id as url %}

                DNSSEC

                diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index e81186c43..d31ce613a 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -27,15 +27,6 @@ -
              • - {% url 'domain-dns-subdomains' pk=domain.id as url %} - - Subdomain records - -
              • -
              • {% url 'domain-dns-dnssec' pk=domain.id as url %} Subdomain records - -

                Subdomains allow you to host multiple services under the same domain and create subsets of your website. For example, blogs are often set up as a subdomain (blog.example.gov).

                - -

                Subdomains cannot be requested through the .gov registrar. Those must be set up ... NEED ADDITIONAL COPY HERE

                - -

                If you use subdomains, enter those records (also known as glue records) below.

                - -{% endblock %} {# domain_content #} diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index 8a04fa186..5bca4e1d5 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -5,7 +5,6 @@ from .domain import ( DomainOrgNameAddressView, DomainDNSView, DomainNameserversView, - DomainSubdomainsView, DomainDNSSECView, DomainDsdataView, DomainKeydataView, diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 2c61248b6..53dd0714c 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -227,13 +227,6 @@ class DomainNameserversView(DomainPermissionView, FormMixin): return super().form_valid(formset) -class DomainSubdomainsView(DomainPermissionView): - - """Domain subdomains editing view.""" - - template_name = "domain_subdomains.html" - - class DomainDNSSECView(DomainPermissionView, FormMixin): """Domain DNSSEC editing view.""" From 41ce73ff4b028221e5eda53d7e6ca4abd5a67283 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 4 Oct 2023 15:33:04 -0400 Subject: [PATCH 26/65] cleaned up dsdata form --- src/registrar/forms/domain.py | 38 ++++++----------------------------- 1 file changed, 6 insertions(+), 32 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index d0cbb9980..7a3c84639 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -148,13 +148,7 @@ class DomainDnssecForm(forms.Form): class DomainDsdataForm(forms.Form): - """Form for adding or editing a security email to a domain.""" - # TODO: ds key data - # has_ds_key_data = forms.TypedChoiceField( - # required=True, - # label="DS Data record type", - # choices=[(False, "DS Data"), (True, "DS Data with Key Data")], - # ) + """Form for adding or editing DNSSEC DS Data to a domain.""" key_tag = forms.IntegerField( required=True, @@ -164,45 +158,25 @@ class DomainDsdataForm(forms.Form): MaxValueValidator(65535, "Value must be between 0 and 65535"), ], ) - + algorithm = forms.TypedChoiceField( required=True, label="Algorithm", choices=[(None, "--Select--")] + ALGORITHM_CHOICES, - # Q: Is this even needed or is a required=True sufficient? - # error_messages={ - # "required": ( - # "You must select an Algorithm" - # ) - # }, ) - # Q: Is ChoiceFiled right? Or do we need to data types other than strings - # (TypedChoiceField) + digest_type = forms.TypedChoiceField( required=True, label="Digest Type", choices=[(None, "--Select--")] + DIGEST_TYPE_CHOICES, - # Q: Is this even needed or is a required=True sufficient? - # error_messages={ - # "required": ( - # "You must select a Digest Type" - # ) - # }, ) + digest = forms.CharField( required=True, label="Digest", - # validators=[ - # RegexValidator( - # "^[0-9]{5}(?:-[0-9]{4})?$|^$", - # message="Accepted range 0-65535.", - # ) - # ], + # TODO: Validation of digests in registrar? ) - # TODO: Conditional DS Key Data fields - - DomainDsdataFormset = formset_factory( DomainDsdataForm, @@ -211,7 +185,7 @@ DomainDsdataFormset = formset_factory( class DomainKeydataForm(forms.Form): - """Form for adding or editing DNSSEC key data.""" + """Form for adding or editing DNSSEC Key Data to a domain.""" flag = forms.TypedChoiceField( required=True, From 99ce0c3e86c9f58d30fff83dc1391a13badf0268 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 4 Oct 2023 17:18:45 -0400 Subject: [PATCH 27/65] fix bug with add/delete js related to number of forms update --- src/registrar/assets/js/get-gov.js | 102 +++++++++++++++++------------ 1 file changed, 61 insertions(+), 41 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 4251ed6b4..07a042693 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -237,48 +237,56 @@ function handleValidationClick(e) { * it everywhere. */ (function prepareNameserverForms() { - let serverForm = document.querySelectorAll(".server-form") - let container = document.querySelector("#form-container") - let addButton = document.querySelector("#add-nameserver-form") - let totalForms = document.querySelector("#id_form-TOTAL_FORMS") + let serverForm = document.querySelectorAll(".server-form"); + let container = document.querySelector("#form-container"); + let addButton = document.querySelector("#add-nameserver-form"); + let totalForms = document.querySelector("#id_form-TOTAL_FORMS"); - let formNum = serverForm.length-1 + let formNum = serverForm.length-1; if (addButton) - addButton.addEventListener('click', addForm) + addButton.addEventListener('click', addForm); function addForm(e){ - let newForm = serverForm[2].cloneNode(true) - let formNumberRegex = RegExp(`form-(\\d){1}-`,'g') - let formLabelRegex = RegExp(`Name server (\\d){1}`, 'g') - let formExampleRegex = RegExp(`ns(\\d){1}`, 'g') + let newForm = serverForm[2].cloneNode(true); + let formNumberRegex = RegExp(`form-(\\d){1}-`,'g'); + let formLabelRegex = RegExp(`Name server (\\d){1}`, 'g'); + let formExampleRegex = RegExp(`ns(\\d){1}`, 'g'); - formNum++ - newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum}-`) - newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `Name server ${formNum+1}`) - newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum+1}`) - container.insertBefore(newForm, addButton) - newForm.querySelector("input").value = "" + formNum++; + newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum}-`); + newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `Name server ${formNum+1}`); + newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum+1}`); + container.insertBefore(newForm, addButton); + newForm.querySelector("input").value = ""; - totalForms.setAttribute('value', `${formNum+1}`) + totalForms.setAttribute('value', `${formNum+1}`); } })(); function prepareDeleteButtons() { - let serverForm2 = document.querySelectorAll(".ds-record") - let deleteButtons = document.querySelectorAll(".delete-record") - let totalForms = document.querySelector("#id_form-TOTAL_FORMS") + let deleteButtons = document.querySelectorAll(".delete-record"); + let totalForms = document.querySelector("#id_form-TOTAL_FORMS"); - let formNum = serverForm2.length-1 // Loop through each delete button and attach the click event listener deleteButtons.forEach((deleteButton) => { deleteButton.addEventListener('click', removeForm); }); function removeForm(e){ - let formToRemove = e.target.closest(".ds-record") - formToRemove.remove() - formNum-- - totalForms.setAttribute('value', `${formNum+1}`) + let formToRemove = e.target.closest(".ds-record"); + formToRemove.remove(); + let forms = document.querySelectorAll(".ds-record"); + let formNum2 = forms.length; + totalForms.setAttribute('value', `${formNum2}`); + + // We need to fix the indicies of every existing form otherwise + // the frontend and backend will not match and will error on submit + // let formNumberRegex = RegExp(`form-(\\d){1}-`,'g'); + // let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g'); + // forms.forEach((form, index) => { + // form.innerHTML = form.innerHTML.replace(formNumberRegex, `form-${index}-`); + // form.innerHTML = form.innerHTML.replace(formLabelRegex, `DS Data Record ${index+1}`); + // }); } } @@ -287,27 +295,29 @@ function prepareDeleteButtons() { * */ (function prepareDNSSECForms() { - let serverForm = document.querySelectorAll(".ds-record") - let container = document.querySelector("#form-container") - let addButton = document.querySelector("#add-ds-form") - let totalForms = document.querySelector("#id_form-TOTAL_FORMS") + let serverForm = document.querySelectorAll(".ds-record"); + let container = document.querySelector("#form-container"); + let addButton = document.querySelector("#add-ds-form"); + let totalForms = document.querySelector("#id_form-TOTAL_FORMS"); - prepareDeleteButtons() + // Attach click event listener on the delete buttons of the existing forms + prepareDeleteButtons(); - let formNum = serverForm.length-1 if (addButton) { - addButton.addEventListener('click', addForm) + addButton.addEventListener('click', addForm); } function addForm(e){ - let newForm = serverForm[0].cloneNode(true) - let formNumberRegex = RegExp(`form-(\\d){1}-`,'g') - let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g') + let forms = document.querySelectorAll(".ds-record"); + let formNum = forms.length; + let newForm = serverForm[0].cloneNode(true); + let formNumberRegex = RegExp(`form-(\\d){1}-`,'g'); + let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g'); - formNum++ - newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum}-`) - newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `DS Data Record ${formNum+1}`) - container.insertBefore(newForm, addButton) + formNum++; + newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum-1}-`); + newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `DS Data Record ${formNum}`); + container.insertBefore(newForm, addButton); let inputs = newForm.querySelectorAll("input"); // Reset the values of each input to blank @@ -321,6 +331,7 @@ function prepareDeleteButtons() { } }); + // Reset any existing validation classes let selects = newForm.querySelectorAll("select"); selects.forEach((select) => { select.classList.remove("usa-input--error"); @@ -337,6 +348,7 @@ function prepareDeleteButtons() { usaFormGroup.classList.remove("usa-form-group--error"); }); + // Remove any existing error messages let usaErrorMessages = newForm.querySelectorAll(".usa-error-message"); usaErrorMessages.forEach((usaErrorMessage) => { let parentDiv = usaErrorMessage.closest('div'); @@ -345,9 +357,17 @@ function prepareDeleteButtons() { } }); - totalForms.setAttribute('value', `${formNum+1}`) + totalForms.setAttribute('value', `${formNum}`); - prepareDeleteButtons() + // Attach click event listener on the delete buttons of the new form + prepareDeleteButtons(); + + // We need to fix the indicies of every existing form otherwise + // the frontend and backend will not match and will error on submit + // forms.forEach((form, index) => { + // form.innerHTML = form.innerHTML.replace(formNumberRegex, `form-${index}-`); + // form.innerHTML = form.innerHTML.replace(formLabelRegex, `DS Data Record ${index+1}`); + // }); } })(); From a9488cff4f5f7202b6ad7e2d9247100cf7fb9174 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 4 Oct 2023 18:43:27 -0400 Subject: [PATCH 28/65] refactor JS to rvise the indicies used in id, for and names on elements in DS records after deleting, revise post logic when extension to be passed is empty to pass {} to the setter --- src/registrar/assets/js/get-gov.js | 31 ++++++++++++++++++++- src/registrar/forms/domain.py | 4 +++ src/registrar/templates/domain_keydata.html | 16 +++++++---- src/registrar/views/domain.py | 12 ++++++++ 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 07a042693..17d4c4d6a 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -287,6 +287,34 @@ function prepareDeleteButtons() { // form.innerHTML = form.innerHTML.replace(formNumberRegex, `form-${index}-`); // form.innerHTML = form.innerHTML.replace(formLabelRegex, `DS Data Record ${index+1}`); // }); + + + + let formNumberRegex = RegExp(`form-(\\d){1}-`, 'g'); + let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g'); + + forms.forEach((form, index) => { + // Iterate over child nodes of the current element + Array.from(form.querySelectorAll('label, input, select')).forEach((node) => { + // Iterate through the attributes of the current node + Array.from(node.attributes).forEach((attr) => { + // Check if the attribute value matches the regex + if (formNumberRegex.test(attr.value)) { + // Replace the attribute value with the updated value + attr.value = attr.value.replace(formNumberRegex, `form-${index}-`); + } + }); + }); + + Array.from(form.querySelectorAll('h2, legend')).forEach((node) => { + node.textContent = node.textContent.replace(formLabelRegex, `DS Data record ${index + 1}`); + }); + + }); + + + + } } @@ -316,7 +344,7 @@ function prepareDeleteButtons() { formNum++; newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum-1}-`); - newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `DS Data Record ${formNum}`); + newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `DS Data record ${formNum}`); container.insertBefore(newForm, addButton); let inputs = newForm.querySelectorAll("input"); @@ -369,6 +397,7 @@ function prepareDeleteButtons() { // form.innerHTML = form.innerHTML.replace(formLabelRegex, `DS Data Record ${index+1}`); // }); } + })(); diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 7a3c84639..6ebaa3be9 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -180,6 +180,8 @@ class DomainDsdataForm(forms.Form): DomainDsdataFormset = formset_factory( DomainDsdataForm, + extra=0, + can_delete=True, ) @@ -213,4 +215,6 @@ class DomainKeydataForm(forms.Form): DomainKeydataFormset = formset_factory( DomainKeydataForm, + extra=0, + can_delete=True, ) \ No newline at end of file diff --git a/src/registrar/templates/domain_keydata.html b/src/registrar/templates/domain_keydata.html index a96c19207..1602c5d87 100644 --- a/src/registrar/templates/domain_keydata.html +++ b/src/registrar/templates/domain_keydata.html @@ -90,17 +90,21 @@ Add new record - - + +
                + +
                {% endif %} {% endblock %} {# domain_content #} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 53dd0714c..f7483c6e0 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -293,6 +293,10 @@ class DomainDsdataView(DomainPermissionView, FormMixin): # Add existing nameservers as initial data initial_data.extend({"key_tag": record.keyTag, "algorithm": record.alg, "digest_type": record.digestType, "digest": record.digest} for record in dnssecdata.dsData) + # Ensure at least 3 fields, filled or empty + while len(initial_data) == 0: + initial_data.append({}) + return initial_data def get_success_url(self): @@ -345,6 +349,8 @@ class DomainDsdataView(DomainPermissionView, FormMixin): # no server information in this field, skip it pass domain = self.get_object() + if len(dnssecdata["dsData"]) == 0: + dnssecdata = {} try: domain.dnssecdata = dnssecdata except RegistryError as err: @@ -389,6 +395,10 @@ class DomainKeydataView(DomainPermissionView, FormMixin): # Add existing keydata as initial data initial_data.extend({"flag": record.flags, "protocol": record.protocol, "algorithm": record.alg, "pub_key": record.pubKey} for record in dnssecdata.keyData) + # Ensure at least 3 fields, filled or empty + while len(initial_data) == 0: + initial_data.append({}) + return initial_data def get_success_url(self): @@ -438,6 +448,8 @@ class DomainKeydataView(DomainPermissionView, FormMixin): # no server information in this field, skip it pass domain = self.get_object() + if len(dnssecdata["keyData"]) == 0: + dnssecdata = {} try: domain.dnssecdata = dnssecdata except RegistryError as err: From 7ace7fa27c2a1803987bb51fd14101e66ab11806 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 5 Oct 2023 08:42:39 -0400 Subject: [PATCH 29/65] app-wide change to input display in forms if field is required, had an initial value, but user removed the value before submitting form' --- src/registrar/templatetags/field_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templatetags/field_helpers.py b/src/registrar/templatetags/field_helpers.py index c62cb10aa..bc296753e 100644 --- a/src/registrar/templatetags/field_helpers.py +++ b/src/registrar/templatetags/field_helpers.py @@ -149,7 +149,7 @@ def input_with_errors(context, field=None): # noqa: C901 # see Widget.get_context() on # https://docs.djangoproject.com/en/4.1/ref/forms/widgets widget = field.field.widget.get_context( - field.html_name, field.value() or field.initial, field.build_widget_attrs(attrs) + field.html_name, field.value(), field.build_widget_attrs(attrs) ) # -> {"widget": {"name": ...}} context["widget"] = widget["widget"] From 7bd6e7f5939308324d07278c7c6552a73efea009 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 5 Oct 2023 16:11:56 -0400 Subject: [PATCH 30/65] logic in domain_sidebar which determines display of DS Data and Key Data --- src/registrar/templates/domain_sidebar.html | 2 +- src/registrar/templatetags/url_helpers.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index d31ce613a..aff9f5dab 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -34,7 +34,7 @@ > DNSSEC
                - {% if domain.dnssec_enabled %} + {% if domain.dnssecdata is not None or request.path|startswith:url and request.path|endswith:'data' %}
                • {% url 'domain-dns-dnssec-dsdata' pk=domain.id as url %} diff --git a/src/registrar/templatetags/url_helpers.py b/src/registrar/templatetags/url_helpers.py index 5b76c116f..2b983930f 100644 --- a/src/registrar/templatetags/url_helpers.py +++ b/src/registrar/templatetags/url_helpers.py @@ -18,6 +18,11 @@ def startswith(text, starts): return text.startswith(starts) return False +@register.filter("endswith") +def endswith(text, ends): + if isinstance(text, str): + return text.endswith(ends) + return False @register.simple_tag def public_site_url(url_path): From 170708ecf3bdd8144b61a637dd3ece29e6c9dee3 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 5 Oct 2023 16:29:19 -0400 Subject: [PATCH 31/65] modal and js toggler wip --- src/registrar/assets/js/get-gov.js | 23 +++- src/registrar/templates/domain_dnssec.html | 139 ++++++++++++++++---- src/registrar/templates/includes/modal.html | 42 ++++++ src/registrar/views/domain.py | 39 ++++-- 4 files changed, 202 insertions(+), 41 deletions(-) create mode 100644 src/registrar/templates/includes/modal.html diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 17d4c4d6a..cfe6066e8 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -469,4 +469,25 @@ function prepareDeleteButtons() { // } // }); // } -// })(); \ No newline at end of file +// })(); + + +/** + * + * + */ +(function toggleDNSSECWarning() { + document.getElementById("toggler1").addEventListener("click", function () { + var element = document.getElementById("step-1"); + var element2 = document.getElementById("step-2"); + element.classList.toggle("display-none"); + element2.classList.toggle("display-none"); + }); + + document.getElementById("toggler2").addEventListener("click", function () { + var element = document.getElementById("step-1"); + var element2 = document.getElementById("step-2"); + element.classList.toggle("display-none"); + element2.classList.toggle("display-none"); + }); +})(); \ No newline at end of file diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index e21bb9458..80bcd7b50 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -11,40 +11,121 @@
                  {% csrf_token %} - {% if not domain.dnssec_enabled %} -
                  -
                  - It is strongly recommended that you do not enable this unless you fully understand DNSSEC and know how to set it up properly. If you make a mistake, it could cause your domain name to stop working. -
                  -
                  - - {% elif domain.dnssecdata is None %} -

                  Add DS Records

                  -

                  In order to enable DNSSEC and add Delegation Signer (DS) records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.

                  -

                  - Add DS Data - Add Key Data - -

                  - {% else %} + {% if domain.has_dnssec_records %}
                  In order to fully disable DNSSEC on your domain, you will need to work with your DNS provider to remove your DNSSEC-related records from your zone.
                  - + Disable DNSSEC + {% else %} +
                  +
                  +
                  + It is strongly recommended that you do not enable this unless you fully understand DNSSEC and know how to set it up properly. If you make a mistake, it could cause your domain name to stop working. +
                  +
                  + +
                  + + + + + + + + {% endif %}
                  + +
                  + {% include 'includes/modal.html' with modal_heading="Are you sure you want to continue?" modal_description="Your DNSSEC records will be deleted from the registry." modal_button=modal_button|safe %} +
                  + + + {% comment %}
                  +
                  +
                  + +
                  + +
                  + + + +
                  + +
                  +
                  {% endcomment %} + + {% endblock %} {# domain_content #} diff --git a/src/registrar/templates/includes/modal.html b/src/registrar/templates/includes/modal.html new file mode 100644 index 000000000..b456a59e1 --- /dev/null +++ b/src/registrar/templates/includes/modal.html @@ -0,0 +1,42 @@ +
                  +
                  + +
                  + +
                  + + +
                  + +
                  \ No newline at end of file diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index f7483c6e0..6358bf39a 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -233,6 +233,32 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): template_name = "domain_dnssec.html" form_class = DomainDnssecForm + clicked_enable_dns = False + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + """The initial value for the form (which is a formset here).""" + self.domain = self.get_object() + + has_dnssec_records = self.domain.dnssecdata is not None + + logger.debug(f"clicked_enable_dns {self.clicked_enable_dns}") + + # if does_not_have_dnssec_records and self.clicked_enable_dns == False: + # logger.debug(f"clicked_enable_dns {self.clicked_enable_dns}") + # self.domain.dnssec_enabled = False + # self.domain.dnssec_ds_confirmed = False + # self.domain.dnssec_key_confirmed = False + # self.domain.save() + + # Create HTML for the buttons + modal_button = '' + + context['modal_button'] = modal_button + context['has_dnssec_records'] = has_dnssec_records + context['domain'] = self.domain + + return context def get_success_url(self): """Redirect to the DNSSEC page for the domain.""" @@ -243,16 +269,8 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): """ self.domain = self.get_object() form = self.get_form() - if form.is_valid(): - if 'enable_dnssec' in request.POST: - self.domain.dnssec_enabled = True - self.domain.save() - elif 'cancel' in request.POST: - self.domain.dnssec_enabled = False - self.domain.dnssec_ds_confirmed = False - self.domain.dnssec_key_confirmed = False - self.domain.save() - elif 'disable_dnssec' in request.POST: + if form.is_valid(): + if 'disable_dnssec' in request.POST: try: self.domain.dnssecdata = {} except RegistryError as err: @@ -261,7 +279,6 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): messages.error( self.request, errmsg ) - self.domain.dnssec_enabled = False self.domain.dnssec_ds_confirmed = False self.domain.dnssec_key_confirmed = False self.domain.save() From 13e294d0d2d5bb1eb7b648e0f1798db8c4564f71 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 5 Oct 2023 16:54:01 -0400 Subject: [PATCH 32/65] clean up JS and dnssec template --- src/registrar/assets/js/get-gov.js | 22 +++--- src/registrar/templates/domain_dnssec.html | 81 +++------------------- src/registrar/views/domain.py | 6 +- 3 files changed, 20 insertions(+), 89 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index cfe6066e8..36d5f257a 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -472,22 +472,18 @@ function prepareDeleteButtons() { // })(); +function toggleElements() { + let element1 = document.getElementById("enable-dnssec"); + let element2 = document.getElementById("add-records"); + element1.classList.toggle("display-none"); + element2.classList.toggle("display-none"); +} + /** * * */ (function toggleDNSSECWarning() { - document.getElementById("toggler1").addEventListener("click", function () { - var element = document.getElementById("step-1"); - var element2 = document.getElementById("step-2"); - element.classList.toggle("display-none"); - element2.classList.toggle("display-none"); - }); - - document.getElementById("toggler2").addEventListener("click", function () { - var element = document.getElementById("step-1"); - var element2 = document.getElementById("step-2"); - element.classList.toggle("display-none"); - element2.classList.toggle("display-none"); - }); + document.getElementById("enable_dnssec").addEventListener("click", toggleElements); + document.getElementById("cancel_dnssec").addEventListener("click", toggleElements); })(); \ No newline at end of file diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index 80bcd7b50..903020c93 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -11,7 +11,7 @@
                  {% csrf_token %} - {% if domain.has_dnssec_records %} + {% if has_dnssec_records %}
                  In order to fully disable DNSSEC on your domain, you will need to work with your DNS provider to remove your DNSSEC-related records from your zone. @@ -25,7 +25,7 @@ >Disable DNSSEC {% else %} -
                  +
                  It is strongly recommended that you do not enable this unless you fully understand DNSSEC and know how to set it up properly. If you make a mistake, it could cause your domain name to stop working. @@ -35,11 +35,11 @@ type="button" class="usa-button" name="enable_dnssec" - id="toggler1" + id="enable_dnssec" >Enable DNSSEC
                  - - - {% comment %} display: none is sufficient on hidden elements for accessibility (removes accessibility tree) {% endcomment %} - {% endif %} diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 585b4a981..2ad162360 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -19,7 +19,7 @@

                  - {% elif not domain.dnssec_ds_confirmed %} + {% elif not dnssec_ds_confirmed %}

                  In order to enable DNSSEC and add DS records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.

                  {% csrf_token %} diff --git a/src/registrar/templates/domain_keydata.html b/src/registrar/templates/domain_keydata.html index cea62e71a..a5d2f610c 100644 --- a/src/registrar/templates/domain_keydata.html +++ b/src/registrar/templates/domain_keydata.html @@ -19,7 +19,7 @@

                  - {% elif not domain.dnssec_key_confirmed %} + {% elif not dnssec_key_confirmed %}

                  In order to enable DNSSEC and add DS records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.

                  {% csrf_token %} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 105691c37..fd5d22651 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -233,7 +233,6 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): template_name = "domain_dnssec.html" form_class = DomainDnssecForm - clicked_enable_dns = False def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -242,20 +241,13 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): has_dnssec_records = self.domain.dnssecdata is not None - # if does_not_have_dnssec_records and self.clicked_enable_dns == False: - # logger.debug(f"clicked_enable_dns {self.clicked_enable_dns}") - # self.domain.dnssec_enabled = False - # self.domain.dnssec_ds_confirmed = False - # self.domain.dnssec_key_confirmed = False - # self.domain.save() - # Create HTML for the buttons modal_button = '' context['modal_button'] = modal_button context['has_dnssec_records'] = has_dnssec_records - # context['domain'] = self.domain - + context['dnssec_enabled'] = self.request.session.pop('dnssec_enabled', False) + return context def get_success_url(self): @@ -277,9 +269,12 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): messages.error( self.request, errmsg ) - self.domain.dnssec_ds_confirmed = False - self.domain.dnssec_key_confirmed = False - self.domain.save() + request.session['dnssec_ds_confirmed'] = False + request.session['dnssec_key_confirmed'] = False + elif 'enable_dnssec' in request.POST: + request.session['dnssec_enabled'] = True + request.session['dnssec_ds_confirmed'] = False + request.session['dnssec_key_confirmed'] = False return self.form_valid(form) @@ -323,6 +318,17 @@ class DomainDsdataView(DomainPermissionView, FormMixin): context = super().get_context_data(**kwargs) # use "formset" instead of "form" for the key context["formset"] = context.pop("form") + + # set the dnssec_ds_confirmed flag in the context for this view + # based either on the existence of DS Data in the domain, + # or on the flag stored in the session + domain = self.get_object() + dnssecdata: extensions.DNSSECExtension = domain.dnssecdata + + if dnssecdata is not None and dnssecdata.dsData is not None: + self.request.session['dnssec_ds_confirmed'] = True + + context['dnssec_ds_confirmed'] = self.request.session.get('dnssec_ds_confirmed', False) return context def post(self, request, *args, **kwargs): @@ -331,9 +337,8 @@ class DomainDsdataView(DomainPermissionView, FormMixin): formset = self.get_form() if 'confirm-ds' in request.POST: - self.object.dnssec_ds_confirmed = True - self.object.dnssec_key_confirmed = False - self.object.save() + request.session['dnssec_ds_confirmed'] = True + request.session['dnssec_key_confirmed'] = False return super().form_valid(formset) if 'btn-cancel-click' in request.POST: @@ -425,6 +430,17 @@ class DomainKeydataView(DomainPermissionView, FormMixin): context = super().get_context_data(**kwargs) # use "formset" instead of "form" for the key context["formset"] = context.pop("form") + + # set the dnssec_key_confirmed flag in the context for this view + # based either on the existence of Key Data in the domain, + # or on the flag stored in the session + domain = self.get_object() + dnssecdata: extensions.DNSSECExtension = domain.dnssecdata + + if dnssecdata is not None and dnssecdata.keyData is not None: + self.request.session['dnssec_key_confirmed'] = True + + context['dnssec_key_confirmed'] = self.request.session.get('dnssec_key_confirmed', False) return context def post(self, request, *args, **kwargs): @@ -433,8 +449,8 @@ class DomainKeydataView(DomainPermissionView, FormMixin): formset = self.get_form() if 'confirm-key' in request.POST: - self.object.dnssec_key_confirmed = True - self.object.dnssec_ds_confirmed = False + request.session['dnssec_key_confirmed'] = True + request.session['dnssec_ds_confirmed'] = False self.object.save() return super().form_valid(formset) From ba8901bf272822427aa622f9d9d56542da4c552c Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 6 Oct 2023 07:56:44 -0400 Subject: [PATCH 38/65] removing unnecessary domain attributes --- ...ove_domain_dnssec_ds_confirmed_and_more.py | 24 +++++++++++++++++++ src/registrar/models/domain.py | 15 ------------ 2 files changed, 24 insertions(+), 15 deletions(-) create mode 100644 src/registrar/migrations/0035_remove_domain_dnssec_ds_confirmed_and_more.py diff --git a/src/registrar/migrations/0035_remove_domain_dnssec_ds_confirmed_and_more.py b/src/registrar/migrations/0035_remove_domain_dnssec_ds_confirmed_and_more.py new file mode 100644 index 000000000..8b7b7566e --- /dev/null +++ b/src/registrar/migrations/0035_remove_domain_dnssec_ds_confirmed_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.1 on 2023-10-06 11:50 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0034_domain_dnssec_ds_confirmed_and_more"), + ] + + operations = [ + migrations.RemoveField( + model_name="domain", + name="dnssec_ds_confirmed", + ), + migrations.RemoveField( + model_name="domain", + name="dnssec_enabled", + ), + migrations.RemoveField( + model_name="domain", + name="dnssec_key_confirmed", + ), + ] diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 56a1a53cb..7a890632c 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -702,21 +702,6 @@ class Domain(TimeStampedModel, DomainHelper): help_text="Very basic info about the lifecycle of this domain object", ) - dnssec_enabled = models.BooleanField( - default=False, - help_text="Boolean indicating if dnssec is enabled", - ) - - dnssec_ds_confirmed = models.BooleanField( - default=False, - help_text="Boolean indicating if DS record adding is confirmed", - ) - - dnssec_key_confirmed = models.BooleanField( - default=False, - help_text="Boolean indicating if Key record adding is confirmed", - ) - # ForeignKey on UserDomainRole creates a "permissions" member for # all of the user-roles that are in place for this domain From 88bcb1eedcccae3b8f997324890923111da90a92 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 6 Oct 2023 10:12:10 -0400 Subject: [PATCH 39/65] fix modal ui --- src/registrar/assets/sass/_theme/_buttons.scss | 8 ++++++++ src/registrar/templates/domain_dnssec.html | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 45ea6620c..f4c72f4fa 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -24,6 +24,14 @@ a.breadcrumb__back { a.usa-button { text-decoration: none; + color: color('white'); +} + +a.usa-button:visited, +a.usa-button:hover, +a.usa-button:focus, +a.usa-button:active { + color: color('white'); } a.usa-button--outline, diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index 4763ff3c1..6a0a15389 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -18,9 +18,9 @@
                  Disable DNSSEC From a0f7cd5e1dda8d8320d99af921a6bff16c82b88f Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 6 Oct 2023 12:04:56 -0400 Subject: [PATCH 40/65] shared mock data for dnssec unit tests --- src/registrar/tests/common.py | 63 +++++++- src/registrar/tests/test_models_domain.py | 168 +++++----------------- 2 files changed, 94 insertions(+), 137 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 10c387099..9291aa271 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -7,7 +7,7 @@ import random from string import ascii_uppercase from django.test import TestCase from unittest.mock import MagicMock, Mock, patch -from typing import List, Dict +from typing import List, Dict, Mapping, Any from django.conf import settings from django.contrib.auth import get_user_model, login @@ -26,6 +26,7 @@ from registrar.models import ( from epplibwrapper import ( commands, common, + extensions, RegistryError, ErrorCode, ) @@ -584,6 +585,37 @@ class MockEppLib(TestCase): mockDataInfoHosts = fakedEppObject( "lastPw", cr_date=datetime.datetime(2023, 8, 25, 19, 45, 35) ) + addDsData1 = { + "keyTag": 1234, + "alg": 3, + "digestType": 1, + "digest": "ec0bdd990b39feead889f0ba613db4adec0bdd99", + } + addDsData2 = { + "keyTag": 2345, + "alg": 3, + "digestType": 1, + "digest": "ec0bdd990b39feead889f0ba613db4adecb4adec", + } + keyDataDict = { + "flags": 257, + "protocol": 3, + "alg": 1, + "pubKey": "AQPJ////4Q==", + } + dnssecExtensionWithDsData: Mapping[str, Any] = { + "dsData": [common.DSData(**addDsData1)] + } + dnssecExtensionWithMultDsData: Mapping[str, Any] = { + "dsData": [ + common.DSData(**addDsData1), + common.DSData(**addDsData2), + ], + } + dnssecExtensionWithKeyData: Mapping[str, Any] = { + "maxSigLife": 3215, + "keyData": [common.DNSSECKeyData(**keyDataDict)], + } def mockSend(self, _request, cleaned): """Mocks the registry.send function used inside of domain.py @@ -593,6 +625,30 @@ class MockEppLib(TestCase): if isinstance(_request, commands.InfoDomain): if getattr(_request, "name", None) == "security.gov": return MagicMock(res_data=[self.infoDomainNoContact]) + elif getattr(_request, "name", None) == "dnssec-dsdata.gov": + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[ + extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) + ], + ) + elif getattr(_request, "name", None) == "dnssec-multdsdata.gov": + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[ + extensions.DNSSECExtension(**self.dnssecExtensionWithMultDsData) + ], + ) + elif getattr(_request, "name", None) == "dnssec-keydata.gov": + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[ + extensions.DNSSECExtension(**self.dnssecExtensionWithKeyData) + ], + ) + elif getattr(_request, "name", None) == "dnssec-none.gov": + # this case is not necessary, but helps improve readability + return MagicMock(res_data=[self.mockDataInfoDomain]) return MagicMock(res_data=[self.mockDataInfoDomain]) elif isinstance(_request, commands.InfoContact): return MagicMock(res_data=[self.mockDataInfoContact]) @@ -614,6 +670,11 @@ class MockEppLib(TestCase): raise RegistryError( code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION ) + elif ( + isinstance(_request, commands.UpdateDomain) + and getattr(_request, "name", None) == "dnssec-invalid.gov" + ): + raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR) return MagicMock(res_data=[self.mockDataInfoHosts]) def setUp(self): diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 16dd30017..6f164d1f4 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -3,7 +3,6 @@ Feature being tested: Registry Integration This file tests the various ways in which the registrar interacts with the registry. """ -from typing import Mapping, Any from django.test import TestCase from django.db.utils import IntegrityError from unittest.mock import MagicMock, patch, call @@ -803,37 +802,7 @@ class TestRegistrantDNSSEC(MockEppLib): super().setUp() # for the tests, need a domain in the unknown state self.domain, _ = Domain.objects.get_or_create(name="fake.gov") - self.addDsData1 = { - "keyTag": 1234, - "alg": 3, - "digestType": 1, - "digest": "ec0bdd990b39feead889f0ba613db4adec0bdd99", - } - self.addDsData2 = { - "keyTag": 2345, - "alg": 3, - "digestType": 1, - "digest": "ec0bdd990b39feead889f0ba613db4adecb4adec", - } - self.keyDataDict = { - "flags": 257, - "protocol": 3, - "alg": 1, - "pubKey": "AQPJ////4Q==", - } - self.dnssecExtensionWithDsData: Mapping[str, Any] = { - "dsData": [common.DSData(**self.addDsData1)] - } - self.dnssecExtensionWithMultDsData: Mapping[str, Any] = { - "dsData": [ - common.DSData(**self.addDsData1), - common.DSData(**self.addDsData2), - ], - } - self.dnssecExtensionWithKeyData: Mapping[str, Any] = { - "maxSigLife": 3215, - "keyData": [common.DNSSECKeyData(**self.keyDataDict)], - } + def tearDown(self): Domain.objects.all().delete() @@ -852,26 +821,13 @@ class TestRegistrantDNSSEC(MockEppLib): """ - # make sure to stop any other patcher so there are no conflicts - self.mockSendPatch.stop() + domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") - def side_effect(_request, cleaned): - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[ - extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) - ], - ) - - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - self.domain.dnssecdata = self.dnssecExtensionWithDsData + domain.dnssecdata = self.dnssecExtensionWithDsData # get the DNS SEC extension added to the UpdateDomain command and # verify that it is properly sent # args[0] is the _request sent to registry - args, _ = mocked_send.call_args + args, _ = self.mockedSendFunction.call_args # assert that the extension matches self.assertEquals( args[0].extensions[0], @@ -880,12 +836,12 @@ class TestRegistrantDNSSEC(MockEppLib): ), ) # test that the dnssecdata getter is functioning properly - dnssecdata_get = self.domain.dnssecdata - mocked_send.assert_has_calls( + dnssecdata_get = domain.dnssecdata + self.mockedSendFunction.assert_has_calls( [ call( commands.UpdateDomain( - name="fake.gov", + name="dnssec-dsdata.gov", nsset=None, keyset=None, registrant=None, @@ -895,7 +851,7 @@ class TestRegistrantDNSSEC(MockEppLib): ), call( commands.InfoDomain( - name="fake.gov", + name="dnssec-dsdata.gov", ), cleaned=True, ), @@ -906,8 +862,6 @@ class TestRegistrantDNSSEC(MockEppLib): dnssecdata_get.dsData, self.dnssecExtensionWithDsData["dsData"] ) - patcher.stop() - def test_dnssec_is_idempotent(self): """ Scenario: Registrant adds DNS data twice, due to a UI glitch @@ -923,32 +877,19 @@ class TestRegistrantDNSSEC(MockEppLib): """ - # make sure to stop any other patcher so there are no conflicts - self.mockSendPatch.stop() - - def side_effect(_request, cleaned): - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[ - extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) - ], - ) - - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect + domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") # set the dnssecdata once - self.domain.dnssecdata = self.dnssecExtensionWithDsData + domain.dnssecdata = self.dnssecExtensionWithDsData # set the dnssecdata again - self.domain.dnssecdata = self.dnssecExtensionWithDsData + domain.dnssecdata = self.dnssecExtensionWithDsData # test that the dnssecdata getter is functioning properly - dnssecdata_get = self.domain.dnssecdata - mocked_send.assert_has_calls( + dnssecdata_get = domain.dnssecdata + self.mockedSendFunction.assert_has_calls( [ call( commands.UpdateDomain( - name="fake.gov", + name="dnssec-dsdata.gov", nsset=None, keyset=None, registrant=None, @@ -958,7 +899,7 @@ class TestRegistrantDNSSEC(MockEppLib): ), call( commands.UpdateDomain( - name="fake.gov", + name="dnssec-dsdata.gov", nsset=None, keyset=None, registrant=None, @@ -968,7 +909,7 @@ class TestRegistrantDNSSEC(MockEppLib): ), call( commands.InfoDomain( - name="fake.gov", + name="dnssec-dsdata.gov", ), cleaned=True, ), @@ -979,8 +920,6 @@ class TestRegistrantDNSSEC(MockEppLib): dnssecdata_get.dsData, self.dnssecExtensionWithDsData["dsData"] ) - patcher.stop() - def test_user_adds_dnssec_data_multiple_dsdata(self): """ Scenario: Registrant adds DNSSEC data with multiple DSData. @@ -994,26 +933,13 @@ class TestRegistrantDNSSEC(MockEppLib): """ - # make sure to stop any other patcher so there are no conflicts - self.mockSendPatch.stop() + domain, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") - def side_effect(_request, cleaned): - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[ - extensions.DNSSECExtension(**self.dnssecExtensionWithMultDsData) - ], - ) - - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - self.domain.dnssecdata = self.dnssecExtensionWithMultDsData + domain.dnssecdata = self.dnssecExtensionWithMultDsData # get the DNS SEC extension added to the UpdateDomain command # and verify that it is properly sent # args[0] is the _request sent to registry - args, _ = mocked_send.call_args + args, _ = self.mockedSendFunction.call_args # assert that the extension matches self.assertEquals( args[0].extensions[0], @@ -1022,12 +948,12 @@ class TestRegistrantDNSSEC(MockEppLib): ), ) # test that the dnssecdata getter is functioning properly - dnssecdata_get = self.domain.dnssecdata - mocked_send.assert_has_calls( + dnssecdata_get = domain.dnssecdata + self.mockedSendFunction.assert_has_calls( [ call( commands.UpdateDomain( - name="fake.gov", + name="dnssec-multdsdata.gov", nsset=None, keyset=None, registrant=None, @@ -1037,7 +963,7 @@ class TestRegistrantDNSSEC(MockEppLib): ), call( commands.InfoDomain( - name="fake.gov", + name="dnssec-multdsdata.gov", ), cleaned=True, ), @@ -1048,8 +974,6 @@ class TestRegistrantDNSSEC(MockEppLib): dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData["dsData"] ) - patcher.stop() - def test_user_adds_dnssec_keydata(self): """ Scenario: Registrant adds DNSSEC data. @@ -1063,26 +987,13 @@ class TestRegistrantDNSSEC(MockEppLib): """ - # make sure to stop any other patcher so there are no conflicts - self.mockSendPatch.stop() + domain, _ = Domain.objects.get_or_create(name="dnssec-keydata.gov") - def side_effect(_request, cleaned): - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[ - extensions.DNSSECExtension(**self.dnssecExtensionWithKeyData) - ], - ) - - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - self.domain.dnssecdata = self.dnssecExtensionWithKeyData + domain.dnssecdata = self.dnssecExtensionWithKeyData # get the DNS SEC extension added to the UpdateDomain command # and verify that it is properly sent # args[0] is the _request sent to registry - args, _ = mocked_send.call_args + args, _ = self.mockedSendFunction.call_args # assert that the extension matches self.assertEquals( args[0].extensions[0], @@ -1091,12 +1002,12 @@ class TestRegistrantDNSSEC(MockEppLib): ), ) # test that the dnssecdata getter is functioning properly - dnssecdata_get = self.domain.dnssecdata - mocked_send.assert_has_calls( + dnssecdata_get = domain.dnssecdata + self.mockedSendFunction.assert_has_calls( [ call( commands.UpdateDomain( - name="fake.gov", + name="dnssec-keydata.gov", nsset=None, keyset=None, registrant=None, @@ -1106,7 +1017,7 @@ class TestRegistrantDNSSEC(MockEppLib): ), call( commands.InfoDomain( - name="fake.gov", + name="dnssec-keydata.gov", ), cleaned=True, ), @@ -1117,8 +1028,6 @@ class TestRegistrantDNSSEC(MockEppLib): dnssecdata_get.keyData, self.dnssecExtensionWithKeyData["keyData"] ) - patcher.stop() - def test_update_is_unsuccessful(self): """ Scenario: An update to the dns data is unsuccessful @@ -1126,27 +1035,14 @@ class TestRegistrantDNSSEC(MockEppLib): Then a user-friendly error message is returned for displaying on the web """ - # make sure to stop any other patcher so there are no conflicts - self.mockSendPatch.stop() + domain, _ = Domain.objects.get_or_create(name="dnssec-invalid.gov") - def side_effect(_request, cleaned): - raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR) - - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - # if RegistryError is raised, view formats user-friendly - # error message if error is_client_error, is_session_error, or - # is_server_error; so test for those conditions with self.assertRaises(RegistryError) as err: - self.domain.dnssecdata = self.dnssecExtensionWithDsData + domain.dnssecdata = self.dnssecExtensionWithDsData self.assertTrue( err.is_client_error() or err.is_session_error() or err.is_server_error() ) - patcher.stop() - class TestAnalystClientHold(MockEppLib): """Rule: Analysts may suspend or restore a domain by using client hold""" From 136db5bab2022c34482f35fb9b8f5f31fa5bd948 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 6 Oct 2023 12:12:43 -0400 Subject: [PATCH 41/65] initial breakdown of test_views --- src/registrar/tests/test_views.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index c49ec8f3f..83e9a8749 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1133,7 +1133,7 @@ class TestDomainPermissions(TestWithDomainPermissions): self.assertEqual(response.status_code, 403) -class TestDomainDetail(TestWithDomainPermissions, WebTest): +class TestDomainOverview(TestWithDomainPermissions, WebTest): def setUp(self): super().setUp() self.app.set_user(self.user.username) @@ -1147,6 +1147,13 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): self.assertContains(detail_page, "igorville.gov") self.assertContains(detail_page, "Status") + +class TestDomainUserManagement(TestWithDomainPermissions, WebTest): + def setUp(self): + super().setUp() + self.app.set_user(self.user.username) + self.client.force_login(self.user) + def test_domain_user_management(self): response = self.client.get( reverse("domain-users", kwargs={"pk": self.domain.id}) From bb61ce9f642eb853a8bb0d0b599f9494bff2b09d Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 6 Oct 2023 13:26:07 -0400 Subject: [PATCH 42/65] 50% coverage on DNSSEC code (testing for statuses and flows) --- src/registrar/tests/test_views.py | 181 ++++++++++++++++++++++++++---- 1 file changed, 160 insertions(+), 21 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 83e9a8749..8e984be85 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1071,21 +1071,51 @@ class TestWithDomainPermissions(TestWithUser): def setUp(self): super().setUp() self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") + self.domain_dsdata, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") + self.domain_multdsdata, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") + self.domain_keydata, _ = Domain.objects.get_or_create(name="dnssec-keydata.gov") + # We could simply use domain (igoreville) but this will be more readable in tests + # that inherit this setUp + self.domain_dnssec_none, _ = Domain.objects.get_or_create(name="dnssec-none.gov") self.domain_information, _ = DomainInformation.objects.get_or_create( creator=self.user, domain=self.domain ) + DomainInformation.objects.get_or_create( + creator=self.user, domain=self.domain_dsdata + ) + DomainInformation.objects.get_or_create( + creator=self.user, domain=self.domain_multdsdata + ) + DomainInformation.objects.get_or_create( + creator=self.user, domain=self.domain_keydata + ) + DomainInformation.objects.get_or_create( + creator=self.user, domain=self.domain_dnssec_none + ) self.role, _ = UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN ) + UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.ADMIN + ) + UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain_multdsdata, role=UserDomainRole.Roles.ADMIN + ) + UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain_keydata, role=UserDomainRole.Roles.ADMIN + ) + UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain_dnssec_none, role=UserDomainRole.Roles.ADMIN + ) def tearDown(self): try: - self.domain_information.delete() + UserDomainRole.objects.all().delete() if hasattr(self.domain, "contacts"): self.domain.contacts.all().delete() DomainApplication.objects.all().delete() - self.domain.delete() - self.role.delete() + Domain.objects.all().delete() + UserDomainRole.objects.all().delete() except ValueError: # pass if already deleted pass super().tearDown() @@ -1143,17 +1173,25 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest): home_page = self.app.get("/") self.assertContains(home_page, "igorville.gov") # click the "Edit" link - detail_page = home_page.click("Manage") + detail_page = home_page.click("Manage", index=0) self.assertContains(detail_page, "igorville.gov") self.assertContains(detail_page, "Status") + + def test_domain_overview_blocked_for_ineligible_user(self): + """We could easily duplicate this test for all domain management + views, but a single url test should be solid enough since all domain + management pages share the same permissions class""" + self.user.status = User.RESTRICTED + self.user.save() + home_page = self.app.get("/") + self.assertContains(home_page, "igorville.gov") + with less_console_noise(): + response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) + self.assertEqual(response.status_code, 403) -class TestDomainUserManagement(TestWithDomainPermissions, WebTest): - def setUp(self): - super().setUp() - self.app.set_user(self.user.username) - self.client.force_login(self.user) - +class TestDomainUserManagement(TestDomainOverview): + def test_domain_user_management(self): response = self.client.get( reverse("domain-users", kwargs={"pk": self.domain.id}) @@ -1311,6 +1349,8 @@ class TestDomainUserManagement(TestWithDomainPermissions, WebTest): # Now load the home page and make sure our domain appears there home_page = self.app.get(reverse("home")) self.assertContains(home_page, self.domain.name) + +class TestDomainNameservers(TestDomainOverview): def test_domain_nameservers(self): """Can load domain's nameservers page.""" @@ -1362,6 +1402,8 @@ class TestDomainUserManagement(TestWithDomainPermissions, WebTest): # error text appears twice, once at the top of the page, once around # the field. self.assertContains(result, "This field is required", count=2, status_code=200) + +class TestDomainAuthorizingOfficial(TestDomainOverview): def test_domain_authorizing_official(self): """Can load domain's authorizing official page.""" @@ -1380,6 +1422,8 @@ class TestDomainUserManagement(TestWithDomainPermissions, WebTest): reverse("domain-authorizing-official", kwargs={"pk": self.domain.id}) ) self.assertContains(page, "Testy") + +class TestDomainOrganization(TestDomainOverview): def test_domain_org_name_address(self): """Can load domain's org name and mailing address page.""" @@ -1416,6 +1460,8 @@ class TestDomainUserManagement(TestWithDomainPermissions, WebTest): self.assertContains(success_result_page, "Not igorville") self.assertContains(success_result_page, "Faketown") + +class TestDomainContactInformation(TestDomainOverview): def test_domain_your_contact_information(self): """Can load domain's your contact information page.""" @@ -1432,6 +1478,8 @@ class TestDomainUserManagement(TestWithDomainPermissions, WebTest): reverse("domain-your-contact-information", kwargs={"pk": self.domain.id}) ) self.assertContains(page, "Testy") + +class TestDomainSecurityEmail(TestDomainOverview): def test_domain_security_email(self): """Can load domain's security email page.""" @@ -1465,18 +1513,109 @@ class TestDomainUserManagement(TestWithDomainPermissions, WebTest): self.assertContains( success_page, "The security email for this domain have been updated" ) + + +class TestDomainDNSSEC(TestDomainOverview): + + """MockEPPLib is already inherited.""" + + def test_dnssec_page_refreshes_enable_button(self): + """DNSSEC overview page loads when domain has no DNSSEC data + and shows a 'Enable DNSSEC' button. When button is clicked the template + updates. When user navigates away then comes back to the page, the + 'Enable DNSSEC' button is shown again.""" + # home_page = self.app.get("/") + + page = self.client.get( + reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}) + ) + self.assertContains(page, "Enable DNSSEC") + + # Prepare the data for the POST request + post_data = { + 'enable_dnssec': 'Enable DNSSEC', # Replace with the actual form field and value + # Add other form fields as needed + } + updated_page = self.client.post(reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}), post_data, follow=True) + + self.assertEqual(updated_page.status_code, 200) + + self.assertContains(updated_page, "Add DS Data") + self.assertContains(updated_page, "Add Key Data") + + self.app.get("/") + + back_to_page = self.client.get( + reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}) + ) + self.assertContains(back_to_page, "Enable DNSSEC") + + + def test_dnssec_page_loads_with_data_in_domain(self): + """DNSSEC overview page loads when domain has DNSSEC data + and the template contains a button to disable DNSSEC.""" + + page = self.client.get( + reverse("domain-dns-dnssec", kwargs={"pk": self.domain_multdsdata.id}) + ) + self.assertContains(page, "Disable DNSSEC") + + def test_ds_form_loads_with_no_domain_data(self): + """DNSSEC Add DS Data page loads when there is no + domain DNSSEC data and shows a button to Add DS Data record""" + + page = self.client.get( + reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dnssec_none.id}) + ) + self.assertContains(page, "Add DS Data record") + + def test_ds_form_loads_with_ds_data(self): + """DNSSEC Add DS Data page loads when there is + domain DNSSEC DS data and shows the data""" + + page = self.client.get( + reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}) + ) + self.assertContains(page, "DS Data record 1") + + def test_ds_form_loads_with_key_data(self): + """DNSSEC Add DS Data page loads when there is + domain DNSSEC KEY data and shows an alert""" + + page = self.client.get( + reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_keydata.id}) + ) + self.assertContains(page, "Warning, you cannot add DS Data") + + def test_key_form_loads_with_no_domain_data(self): + """DNSSEC Add Key Data page loads when there is no + domain DNSSEC data and shows a button to Add DS Key record""" + + page = self.client.get( + reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_dnssec_none.id}) + ) + self.assertContains(page, "Add DS Key record") + + def test_key_form_loads_with_key_data(self): + """DNSSEC Add Key Data page loads when there is + domain DNSSEC Key data and shows the data""" + + page = self.client.get( + reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id}) + ) + self.assertContains(page, "DS Data record 1") + + def test_key_form_loads_with_ds_data(self): + """DNSSEC Add Key Data page loads when there is + domain DNSSEC DS data and shows an alert""" + + page = self.client.get( + reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_dsdata.id}) + ) + self.assertContains(page, "Warning, you cannot add Key Data") - def test_domain_overview_blocked_for_ineligible_user(self): - """We could easily duplicate this test for all domain management - views, but a single url test should be solid enough since all domain - management pages share the same permissions class""" - self.user.status = User.RESTRICTED - self.user.save() - home_page = self.app.get("/") - self.assertContains(home_page, "igorville.gov") - with less_console_noise(): - response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) - self.assertEqual(response.status_code, 403) + + class TestApplicationStatus(TestWithUser, WebTest): From a6f4c2f000c18ba6b04fc2cb6c018c2f160976d4 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 6 Oct 2023 13:59:20 -0400 Subject: [PATCH 43/65] unit tests on ds data form and disable dnssec --- src/registrar/tests/test_views.py | 52 ++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 8e984be85..5c24b33f4 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1550,7 +1550,6 @@ class TestDomainDNSSEC(TestDomainOverview): ) self.assertContains(back_to_page, "Enable DNSSEC") - def test_dnssec_page_loads_with_data_in_domain(self): """DNSSEC overview page loads when domain has DNSSEC data and the template contains a button to disable DNSSEC.""" @@ -1559,6 +1558,16 @@ class TestDomainDNSSEC(TestDomainOverview): reverse("domain-dns-dnssec", kwargs={"pk": self.domain_multdsdata.id}) ) self.assertContains(page, "Disable DNSSEC") + + # Prepare the data for the POST request + post_data = { + 'disable_dnssec': 'Disable DNSSEC', # Replace with the actual form field and value + } + updated_page = self.client.post(reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}), post_data, follow=True) + + self.assertEqual(updated_page.status_code, 200) + + self.assertContains(updated_page, "Enable DNSSEC") def test_ds_form_loads_with_no_domain_data(self): """DNSSEC Add DS Data page loads when there is no @@ -1614,6 +1623,47 @@ class TestDomainDNSSEC(TestDomainOverview): ) self.assertContains(page, "Warning, you cannot add Key Data") + def test_ds_data_form_submits(self): + """DS Data form submits successfully + + Uses self.app WebTest because we need to interact with forms. + """ + add_data_page = self.app.get( + reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}) + ) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + with less_console_noise(): # swallow log warning message + result = add_data_page.forms[0].submit() + # form submission was a post, response should be a redirect + self.assertEqual(result.status_code, 302) + self.assertEqual( + result["Location"], + reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}), + ) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + page = result.follow() + self.assertContains(page, "The DS Data records for this domain have been updated.") + + def test_domain_nameservers_form_invalid(self): + """DS Data form errors with invalid data + + Uses self.app WebTest because we need to interact with forms. + """ + add_data_page = self.app.get( + reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}) + ) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # first two nameservers are required, so if we empty one out we should + # get a form error + add_data_page.forms[0]["form-0-key_tag"] = "" + with less_console_noise(): # swallow logged warning message + result = add_data_page.forms[0].submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the field. + self.assertContains(result, "Key tag is required", count=2, status_code=200) From c39384df8c8842076d818659fd977e1b2945d111 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 6 Oct 2023 14:04:37 -0400 Subject: [PATCH 44/65] unit tests completed --- src/registrar/tests/test_views.py | 46 +++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 5c24b33f4..cff7d1614 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1645,7 +1645,7 @@ class TestDomainDNSSEC(TestDomainOverview): page = result.follow() self.assertContains(page, "The DS Data records for this domain have been updated.") - def test_domain_nameservers_form_invalid(self): + def test_ds_data_form_invalid(self): """DS Data form errors with invalid data Uses self.app WebTest because we need to interact with forms. @@ -1665,9 +1665,49 @@ class TestDomainDNSSEC(TestDomainOverview): # the field. self.assertContains(result, "Key tag is required", count=2, status_code=200) + def test_key_data_form_submits(self): + """Key Data form submits successfully + + Uses self.app WebTest because we need to interact with forms. + """ + add_data_page = self.app.get( + reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id}) + ) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + with less_console_noise(): # swallow log warning message + result = add_data_page.forms[0].submit() + # form submission was a post, response should be a redirect + self.assertEqual(result.status_code, 302) + self.assertEqual( + result["Location"], + reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id}), + ) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + page = result.follow() + self.assertContains(page, "The Key Data records for this domain have been updated.") + + def test_key_data_form_invalid(self): + """Key Data form errors with invalid data + + Uses self.app WebTest because we need to interact with forms. + """ + add_data_page = self.app.get( + reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id}) + ) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # first two nameservers are required, so if we empty one out we should + # get a form error + add_data_page.forms[0]["form-0-pub_key"] = "" + with less_console_noise(): # swallow logged warning message + result = add_data_page.forms[0].submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the field. + self.assertContains(result, "Pub key is required", count=2, status_code=200) + - - class TestApplicationStatus(TestWithUser, WebTest): def setUp(self): super().setUp() From 6ece85796c6ae4ccf834ae24799546cc820b7ea9 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 6 Oct 2023 15:22:03 -0400 Subject: [PATCH 45/65] removing old migrations --- .../migrations/0033_domain_dnssec_enabled.py | 19 ------------- ...034_domain_dnssec_ds_confirmed_and_more.py | 28 ------------------- ...ove_domain_dnssec_ds_confirmed_and_more.py | 24 ---------------- 3 files changed, 71 deletions(-) delete mode 100644 src/registrar/migrations/0033_domain_dnssec_enabled.py delete mode 100644 src/registrar/migrations/0034_domain_dnssec_ds_confirmed_and_more.py delete mode 100644 src/registrar/migrations/0035_remove_domain_dnssec_ds_confirmed_and_more.py diff --git a/src/registrar/migrations/0033_domain_dnssec_enabled.py b/src/registrar/migrations/0033_domain_dnssec_enabled.py deleted file mode 100644 index a4695a02b..000000000 --- a/src/registrar/migrations/0033_domain_dnssec_enabled.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-03 06:36 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("registrar", "0032_alter_transitiondomain_status"), - ] - - operations = [ - migrations.AddField( - model_name="domain", - name="dnssec_enabled", - field=models.BooleanField( - default=False, help_text="Boolean indicating if dnssec is enabled" - ), - ), - ] diff --git a/src/registrar/migrations/0034_domain_dnssec_ds_confirmed_and_more.py b/src/registrar/migrations/0034_domain_dnssec_ds_confirmed_and_more.py deleted file mode 100644 index 72513a401..000000000 --- a/src/registrar/migrations/0034_domain_dnssec_ds_confirmed_and_more.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-03 17:34 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("registrar", "0033_domain_dnssec_enabled"), - ] - - operations = [ - migrations.AddField( - model_name="domain", - name="dnssec_ds_confirmed", - field=models.BooleanField( - default=False, - help_text="Boolean indicating if DS record adding is confirmed", - ), - ), - migrations.AddField( - model_name="domain", - name="dnssec_key_confirmed", - field=models.BooleanField( - default=False, - help_text="Boolean indicating if Key record adding is confirmed", - ), - ), - ] diff --git a/src/registrar/migrations/0035_remove_domain_dnssec_ds_confirmed_and_more.py b/src/registrar/migrations/0035_remove_domain_dnssec_ds_confirmed_and_more.py deleted file mode 100644 index 8b7b7566e..000000000 --- a/src/registrar/migrations/0035_remove_domain_dnssec_ds_confirmed_and_more.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-06 11:50 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("registrar", "0034_domain_dnssec_ds_confirmed_and_more"), - ] - - operations = [ - migrations.RemoveField( - model_name="domain", - name="dnssec_ds_confirmed", - ), - migrations.RemoveField( - model_name="domain", - name="dnssec_enabled", - ), - migrations.RemoveField( - model_name="domain", - name="dnssec_key_confirmed", - ), - ] From a9d57cda079bf91a532f202ed4b5bad131a16334 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 6 Oct 2023 15:38:50 -0400 Subject: [PATCH 46/65] fixed unit tests --- src/registrar/tests/test_views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 3f583ea77..da4d2dce8 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -18,6 +18,7 @@ from registrar.models import ( DraftDomain, DomainInvitation, Contact, + PublicContact, Website, UserDomainRole, User, @@ -1113,6 +1114,7 @@ class TestWithDomainPermissions(TestWithUser): if hasattr(self.domain, "contacts"): self.domain.contacts.all().delete() DomainApplication.objects.all().delete() + PublicContact.objects.all().delete() Domain.objects.all().delete() UserDomainRole.objects.all().delete() except ValueError: # pass if already deleted From 93d8b8227e046f4dbdd2eb043f03bb59b973c86d Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 6 Oct 2023 16:18:59 -0400 Subject: [PATCH 47/65] refactored complicated test code in common, satisfied the linter --- src/registrar/forms/common.py | 5 +- src/registrar/forms/domain.py | 52 ++++---- src/registrar/templatetags/url_helpers.py | 2 + src/registrar/tests/common.py | 110 ++++++++-------- src/registrar/tests/test_models_domain.py | 1 - src/registrar/tests/test_views.py | 130 +++++++++++-------- src/registrar/views/domain.py | 146 ++++++++++++---------- 7 files changed, 241 insertions(+), 205 deletions(-) diff --git a/src/registrar/forms/common.py b/src/registrar/forms/common.py index 377f59797..159113488 100644 --- a/src/registrar/forms/common.py +++ b/src/registrar/forms/common.py @@ -1,7 +1,8 @@ # common.py -# +# # ALGORITHM_CHOICES are options for alg attribute in DS Data and Key Data -# reference: https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml +# reference: +# https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml ALGORITHM_CHOICES = [ (1, "(1) ERSA/MD5 [RSAMD5]"), (2, "(2) Diffie-Hellman [DH]"), diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index a9c736175..4ee17a72d 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -7,7 +7,13 @@ from django.forms import formset_factory from phonenumber_field.widgets import RegionalPhoneNumberWidget from ..models import Contact, DomainInformation -from .common import ALGORITHM_CHOICES, DIGEST_TYPE_CHOICES, FLAG_CHOICES, PROTOCOL_CHOICES +from .common import ( + ALGORITHM_CHOICES, + DIGEST_TYPE_CHOICES, + FLAG_CHOICES, + PROTOCOL_CHOICES, +) + class DomainAddUserForm(forms.Form): @@ -157,35 +163,27 @@ class DomainDsdataForm(forms.Form): MinValueValidator(0, message="Value must be between 0 and 65535"), MaxValueValidator(65535, message="Value must be between 0 and 65535"), ], - error_messages={ - "required": ("Key tag is required.") - }, + error_messages={"required": ("Key tag is required.")}, ) - + algorithm = forms.TypedChoiceField( required=True, label="Algorithm", - choices=[(None, "--Select--")] + ALGORITHM_CHOICES, - error_messages={ - "required": ("Algorithm is required.") - }, + choices=[(None, "--Select--")] + ALGORITHM_CHOICES, # type: ignore + error_messages={"required": ("Algorithm is required.")}, ) digest_type = forms.TypedChoiceField( required=True, label="Digest Type", - choices=[(None, "--Select--")] + DIGEST_TYPE_CHOICES, - error_messages={ - "required": ("Digest Type is required.") - }, + choices=[(None, "--Select--")] + DIGEST_TYPE_CHOICES, # type: ignore + error_messages={"required": ("Digest Type is required.")}, ) digest = forms.CharField( required=True, label="Digest", - error_messages={ - "required": ("Digest is required.") - }, + error_messages={"required": ("Digest is required.")}, ) @@ -204,35 +202,27 @@ class DomainKeydataForm(forms.Form): required=True, label="Flag", choices=FLAG_CHOICES, - error_messages={ - "required": ("Flag is required.") - }, + error_messages={"required": ("Flag is required.")}, ) protocol = forms.TypedChoiceField( required=True, label="Protocol", choices=PROTOCOL_CHOICES, - error_messages={ - "required": ("Protocol is required.") - }, + error_messages={"required": ("Protocol is required.")}, ) algorithm = forms.TypedChoiceField( required=True, label="Algorithm", - choices=[(None, "--Select--")] + ALGORITHM_CHOICES, - error_messages={ - "required": ("Algorithm is required.") - }, + choices=[(None, "--Select--")] + ALGORITHM_CHOICES, # type: ignore + error_messages={"required": ("Algorithm is required.")}, ) - + pub_key = forms.CharField( required=True, label="Pub key", - error_messages={ - "required": ("Pub key is required.") - }, + error_messages={"required": ("Pub key is required.")}, ) @@ -240,4 +230,4 @@ DomainKeydataFormset = formset_factory( DomainKeydataForm, extra=0, can_delete=True, -) \ No newline at end of file +) diff --git a/src/registrar/templatetags/url_helpers.py b/src/registrar/templatetags/url_helpers.py index 2b983930f..931eedc92 100644 --- a/src/registrar/templatetags/url_helpers.py +++ b/src/registrar/templatetags/url_helpers.py @@ -18,12 +18,14 @@ def startswith(text, starts): return text.startswith(starts) return False + @register.filter("endswith") def endswith(text, ends): if isinstance(text, str): return text.endswith(ends) return False + @register.simple_tag def public_site_url(url_path): """Make a full URL for this path at our public site. diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index b44e21d01..70d0cd1c9 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -689,18 +689,18 @@ class MockEppLib(TestCase): "alg": 1, "pubKey": "AQPJ////4Q==", } - dnssecExtensionWithDsData: Mapping[str, Any] = { - "dsData": [common.DSData(**addDsData1)] + dnssecExtensionWithDsData: Mapping[Any, Any] = { + "dsData": [common.DSData(**addDsData1)] # type: ignore } dnssecExtensionWithMultDsData: Mapping[str, Any] = { "dsData": [ - common.DSData(**addDsData1), - common.DSData(**addDsData2), + common.DSData(**addDsData1), # type: ignore + common.DSData(**addDsData2), # type: ignore ], } dnssecExtensionWithKeyData: Mapping[str, Any] = { "maxSigLife": 3215, - "keyData": [common.DNSSECKeyData(**keyDataDict)], + "keyData": [common.DNSSECKeyData(**keyDataDict)], # type: ignore } def mockSend(self, _request, cleaned): @@ -709,54 +709,9 @@ class MockEppLib(TestCase): returns objects that simulate what would be in a epp response but only relevant pieces for tests""" if isinstance(_request, commands.InfoDomain): - if getattr(_request, "name", None) == "security.gov": - return MagicMock(res_data=[self.infoDomainNoContact]) - elif getattr(_request, "name", None) == "dnssec-dsdata.gov": - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[ - extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) - ], - ) - elif getattr(_request, "name", None) == "dnssec-multdsdata.gov": - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[ - extensions.DNSSECExtension(**self.dnssecExtensionWithMultDsData) - ], - ) - elif getattr(_request, "name", None) == "dnssec-keydata.gov": - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[ - extensions.DNSSECExtension(**self.dnssecExtensionWithKeyData) - ], - ) - elif getattr(_request, "name", None) == "dnssec-none.gov": - # this case is not necessary, but helps improve readability - return MagicMock(res_data=[self.mockDataInfoDomain]) - elif getattr(_request, "name", None) == "freeman.gov": - return MagicMock(res_data=[self.InfoDomainWithContacts]) - else: - return MagicMock(res_data=[self.mockDataInfoDomain]) + return self.mockInfoDomainCommands(_request, cleaned) elif isinstance(_request, commands.InfoContact): - mocked_result: info.InfoContactResultData - - # For testing contact types - match getattr(_request, "id", None): - case "securityContact": - mocked_result = self.mockSecurityContact - case "technicalContact": - mocked_result = self.mockTechnicalContact - case "adminContact": - mocked_result = self.mockAdministrativeContact - case "regContact": - mocked_result = self.mockRegistrantContact - case _: - # Default contact return - mocked_result = self.mockDataInfoContact - - return MagicMock(res_data=[mocked_result]) + return self.mockInfoContactCommands(_request, cleaned) elif ( isinstance(_request, commands.CreateContact) and getattr(_request, "id", None) == "fail" @@ -782,6 +737,57 @@ class MockEppLib(TestCase): raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR) return MagicMock(res_data=[self.mockDataInfoHosts]) + def mockInfoDomainCommands(self, _request, cleaned): + if getattr(_request, "name", None) == "security.gov": + return MagicMock(res_data=[self.infoDomainNoContact]) + elif getattr(_request, "name", None) == "dnssec-dsdata.gov": + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[ + extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) + ], + ) + elif getattr(_request, "name", None) == "dnssec-multdsdata.gov": + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[ + extensions.DNSSECExtension(**self.dnssecExtensionWithMultDsData) + ], + ) + elif getattr(_request, "name", None) == "dnssec-keydata.gov": + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[ + extensions.DNSSECExtension(**self.dnssecExtensionWithKeyData) + ], + ) + elif getattr(_request, "name", None) == "dnssec-none.gov": + # this case is not necessary, but helps improve readability + return MagicMock(res_data=[self.mockDataInfoDomain]) + elif getattr(_request, "name", None) == "freeman.gov": + return MagicMock(res_data=[self.InfoDomainWithContacts]) + else: + return MagicMock(res_data=[self.mockDataInfoDomain]) + + def mockInfoContactCommands(self, _request, cleaned): + mocked_result: info.InfoContactResultData + + # For testing contact types + match getattr(_request, "id", None): + case "securityContact": + mocked_result = self.mockSecurityContact + case "technicalContact": + mocked_result = self.mockTechnicalContact + case "adminContact": + mocked_result = self.mockAdministrativeContact + case "regContact": + mocked_result = self.mockRegistrantContact + case _: + # Default contact return + mocked_result = self.mockDataInfoContact + + return MagicMock(res_data=[mocked_result]) + def setUp(self): """mock epp send function as this will fail locally""" self.mockSendPatch = patch("registrar.models.domain.registry.send") diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 5b299c112..d0718b4ba 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1003,7 +1003,6 @@ class TestRegistrantDNSSEC(MockEppLib): super().setUp() # for the tests, need a domain in the unknown state self.domain, _ = Domain.objects.get_or_create(name="fake.gov") - def tearDown(self): Domain.objects.all().delete() diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index da4d2dce8..553b45135 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1072,11 +1072,15 @@ class TestWithDomainPermissions(TestWithUser): super().setUp() self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") self.domain_dsdata, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") - self.domain_multdsdata, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") + self.domain_multdsdata, _ = Domain.objects.get_or_create( + name="dnssec-multdsdata.gov" + ) self.domain_keydata, _ = Domain.objects.get_or_create(name="dnssec-keydata.gov") - # We could simply use domain (igoreville) but this will be more readable in tests + # We could simply use domain (igorville) but this will be more readable in tests # that inherit this setUp - self.domain_dnssec_none, _ = Domain.objects.get_or_create(name="dnssec-none.gov") + self.domain_dnssec_none, _ = Domain.objects.get_or_create( + name="dnssec-none.gov" + ) self.domain_information, _ = DomainInformation.objects.get_or_create( creator=self.user, domain=self.domain ) @@ -1099,13 +1103,17 @@ class TestWithDomainPermissions(TestWithUser): user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.ADMIN ) UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_multdsdata, role=UserDomainRole.Roles.ADMIN + user=self.user, + domain=self.domain_multdsdata, + role=UserDomainRole.Roles.ADMIN, ) UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain_keydata, role=UserDomainRole.Roles.ADMIN ) UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_dnssec_none, role=UserDomainRole.Roles.ADMIN + user=self.user, + domain=self.domain_dnssec_none, + role=UserDomainRole.Roles.ADMIN, ) def tearDown(self): @@ -1177,7 +1185,7 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest): detail_page = home_page.click("Manage", index=0) self.assertContains(detail_page, "igorville.gov") self.assertContains(detail_page, "Status") - + def test_domain_overview_blocked_for_ineligible_user(self): """We could easily duplicate this test for all domain management views, but a single url test should be solid enough since all domain @@ -1192,7 +1200,6 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest): class TestDomainUserManagement(TestDomainOverview): - def test_domain_user_management(self): response = self.client.get( reverse("domain-users", kwargs={"pk": self.domain.id}) @@ -1350,9 +1357,9 @@ class TestDomainUserManagement(TestDomainOverview): # Now load the home page and make sure our domain appears there home_page = self.app.get(reverse("home")) self.assertContains(home_page, self.domain.name) - -class TestDomainNameservers(TestDomainOverview): + +class TestDomainNameservers(TestDomainOverview): def test_domain_nameservers(self): """Can load domain's nameservers page.""" page = self.client.get( @@ -1403,9 +1410,9 @@ class TestDomainNameservers(TestDomainOverview): # error text appears twice, once at the top of the page, once around # the field. self.assertContains(result, "This field is required", count=2, status_code=200) - -class TestDomainAuthorizingOfficial(TestDomainOverview): + +class TestDomainAuthorizingOfficial(TestDomainOverview): def test_domain_authorizing_official(self): """Can load domain's authorizing official page.""" page = self.client.get( @@ -1423,9 +1430,9 @@ class TestDomainAuthorizingOfficial(TestDomainOverview): reverse("domain-authorizing-official", kwargs={"pk": self.domain.id}) ) self.assertContains(page, "Testy") - -class TestDomainOrganization(TestDomainOverview): + +class TestDomainOrganization(TestDomainOverview): def test_domain_org_name_address(self): """Can load domain's org name and mailing address page.""" page = self.client.get( @@ -1461,9 +1468,9 @@ class TestDomainOrganization(TestDomainOverview): self.assertContains(success_result_page, "Not igorville") self.assertContains(success_result_page, "Faketown") - -class TestDomainContactInformation(TestDomainOverview): + +class TestDomainContactInformation(TestDomainOverview): def test_domain_your_contact_information(self): """Can load domain's your contact information page.""" page = self.client.get( @@ -1479,9 +1486,9 @@ class TestDomainContactInformation(TestDomainOverview): reverse("domain-your-contact-information", kwargs={"pk": self.domain.id}) ) self.assertContains(page, "Testy") - -class TestDomainSecurityEmail(TestDomainOverview): + +class TestDomainSecurityEmail(TestDomainOverview): def test_domain_security_email_existing_security_contact(self): """Can load domain's security email page.""" self.mockSendPatch = patch("registrar.models.domain.registry.send") @@ -1546,47 +1553,50 @@ class TestDomainSecurityEmail(TestDomainOverview): self.assertContains( success_page, "The security email for this domain has been updated" ) - - + + class TestDomainDNSSEC(TestDomainOverview): - + """MockEPPLib is already inherited.""" - + def test_dnssec_page_refreshes_enable_button(self): """DNSSEC overview page loads when domain has no DNSSEC data and shows a 'Enable DNSSEC' button. When button is clicked the template updates. When user navigates away then comes back to the page, the 'Enable DNSSEC' button is shown again.""" # home_page = self.app.get("/") - + page = self.client.get( reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}) ) self.assertContains(page, "Enable DNSSEC") - + # Prepare the data for the POST request post_data = { - 'enable_dnssec': 'Enable DNSSEC', # Replace with the actual form field and value - # Add other form fields as needed + "enable_dnssec": "Enable DNSSEC", } - updated_page = self.client.post(reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}), post_data, follow=True) - + updated_page = self.client.post( + reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}), + post_data, + follow=True, + ) + self.assertEqual(updated_page.status_code, 200) - + self.assertContains(updated_page, "Add DS Data") self.assertContains(updated_page, "Add Key Data") - + self.app.get("/") - + back_to_page = self.client.get( reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}) ) self.assertContains(back_to_page, "Enable DNSSEC") - + def test_dnssec_page_loads_with_data_in_domain(self): """DNSSEC overview page loads when domain has DNSSEC data and the template contains a button to disable DNSSEC.""" - + page = self.client.get( reverse("domain-dns-dnssec", kwargs={"pk": self.domain_multdsdata.id}) ) @@ -1594,63 +1604,71 @@ class TestDomainDNSSEC(TestDomainOverview): # Prepare the data for the POST request post_data = { - 'disable_dnssec': 'Disable DNSSEC', # Replace with the actual form field and value + "disable_dnssec": "Disable DNSSEC", } - updated_page = self.client.post(reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}), post_data, follow=True) - + updated_page = self.client.post( + reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}), + post_data, + follow=True, + ) + self.assertEqual(updated_page.status_code, 200) - + self.assertContains(updated_page, "Enable DNSSEC") - + def test_ds_form_loads_with_no_domain_data(self): """DNSSEC Add DS Data page loads when there is no domain DNSSEC data and shows a button to Add DS Data record""" - + page = self.client.get( - reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dnssec_none.id}) + reverse( + "domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dnssec_none.id} + ) ) self.assertContains(page, "Add DS Data record") - + def test_ds_form_loads_with_ds_data(self): """DNSSEC Add DS Data page loads when there is domain DNSSEC DS data and shows the data""" - + page = self.client.get( reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}) ) self.assertContains(page, "DS Data record 1") - + def test_ds_form_loads_with_key_data(self): """DNSSEC Add DS Data page loads when there is domain DNSSEC KEY data and shows an alert""" - + page = self.client.get( reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_keydata.id}) ) self.assertContains(page, "Warning, you cannot add DS Data") - + def test_key_form_loads_with_no_domain_data(self): """DNSSEC Add Key Data page loads when there is no domain DNSSEC data and shows a button to Add DS Key record""" - + page = self.client.get( - reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_dnssec_none.id}) + reverse( + "domain-dns-dnssec-keydata", kwargs={"pk": self.domain_dnssec_none.id} + ) ) self.assertContains(page, "Add DS Key record") - + def test_key_form_loads_with_key_data(self): """DNSSEC Add Key Data page loads when there is domain DNSSEC Key data and shows the data""" - + page = self.client.get( reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id}) ) self.assertContains(page, "DS Data record 1") - + def test_key_form_loads_with_ds_data(self): """DNSSEC Add Key Data page loads when there is domain DNSSEC DS data and shows an alert""" - + page = self.client.get( reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_dsdata.id}) ) @@ -1676,7 +1694,9 @@ class TestDomainDNSSEC(TestDomainOverview): ) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) page = result.follow() - self.assertContains(page, "The DS Data records for this domain have been updated.") + self.assertContains( + page, "The DS Data records for this domain have been updated." + ) def test_ds_data_form_invalid(self): """DS Data form errors with invalid data @@ -1718,7 +1738,9 @@ class TestDomainDNSSEC(TestDomainOverview): ) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) page = result.follow() - self.assertContains(page, "The Key Data records for this domain have been updated.") + self.assertContains( + page, "The Key Data records for this domain have been updated." + ) def test_key_data_form_invalid(self): """Key Data form errors with invalid data @@ -1739,8 +1761,8 @@ class TestDomainDNSSEC(TestDomainOverview): # error text appears twice, once at the top of the page, once around # the field. self.assertContains(result, "Pub key is required", count=2, status_code=200) - - + + class TestApplicationStatus(TestWithUser, WebTest): def setUp(self): super().setUp() diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 9334700ef..e51395ba3 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -233,20 +233,24 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): template_name = "domain_dnssec.html" form_class = DomainDnssecForm - + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) """The initial value for the form (which is a formset here).""" self.domain = self.get_object() - - has_dnssec_records = self.domain.dnssecdata is not None - - # Create HTML for the buttons - modal_button = '' - context['modal_button'] = modal_button - context['has_dnssec_records'] = has_dnssec_records - context['dnssec_enabled'] = self.request.session.pop('dnssec_enabled', False) + has_dnssec_records = self.domain.dnssecdata is not None + + # Create HTML for the buttons + modal_button = ( + '' + ) + + context["modal_button"] = modal_button + context["has_dnssec_records"] = has_dnssec_records + context["dnssec_enabled"] = self.request.session.pop("dnssec_enabled", False) return context @@ -255,27 +259,24 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): return reverse("domain-dns-dnssec", kwargs={"pk": self.domain.pk}) def post(self, request, *args, **kwargs): - """Form submission posts to this view. - """ + """Form submission posts to this view.""" self.domain = self.get_object() form = self.get_form() - if form.is_valid(): - if 'disable_dnssec' in request.POST: + if form.is_valid(): + if "disable_dnssec" in request.POST: try: self.domain.dnssecdata = {} except RegistryError as err: errmsg = "Error removing existing DNSSEC record(s)." logger.error(errmsg + ": " + err) - messages.error( - self.request, errmsg - ) - request.session['dnssec_ds_confirmed'] = False - request.session['dnssec_key_confirmed'] = False - elif 'enable_dnssec' in request.POST: - request.session['dnssec_enabled'] = True - request.session['dnssec_ds_confirmed'] = False - request.session['dnssec_key_confirmed'] = False - + messages.error(self.request, errmsg) + request.session["dnssec_ds_confirmed"] = False + request.session["dnssec_key_confirmed"] = False + elif "enable_dnssec" in request.POST: + request.session["dnssec_enabled"] = True + request.session["dnssec_ds_confirmed"] = False + request.session["dnssec_key_confirmed"] = False + return self.form_valid(form) @@ -292,21 +293,28 @@ class DomainDsdataView(DomainPermissionView, FormMixin): domain = self.get_object() dnssecdata: extensions.DNSSECExtension = domain.dnssecdata initial_data = [] - - if dnssecdata is not None: + if dnssecdata is not None: if dnssecdata.keyData is not None: # TODO: Throw an error pass if dnssecdata.dsData is not None: # Add existing nameservers as initial data - initial_data.extend({"key_tag": record.keyTag, "algorithm": record.alg, "digest_type": record.digestType, "digest": record.digest} for record in dnssecdata.dsData) - + initial_data.extend( + { + "key_tag": record.keyTag, + "algorithm": record.alg, + "digest_type": record.digestType, + "digest": record.digest, + } + for record in dnssecdata.dsData + ) + # Ensure at least 3 fields, filled or empty while len(initial_data) == 0: initial_data.append({}) - + return initial_data def get_success_url(self): @@ -319,16 +327,18 @@ class DomainDsdataView(DomainPermissionView, FormMixin): # use "formset" instead of "form" for the key context["formset"] = context.pop("form") - # set the dnssec_ds_confirmed flag in the context for this view + # set the dnssec_ds_confirmed flag in the context for this view # based either on the existence of DS Data in the domain, # or on the flag stored in the session domain = self.get_object() dnssecdata: extensions.DNSSECExtension = domain.dnssecdata if dnssecdata is not None and dnssecdata.dsData is not None: - self.request.session['dnssec_ds_confirmed'] = True + self.request.session["dnssec_ds_confirmed"] = True - context['dnssec_ds_confirmed'] = self.request.session.get('dnssec_ds_confirmed', False) + context["dnssec_ds_confirmed"] = self.request.session.get( + "dnssec_ds_confirmed", False + ) return context def post(self, request, *args, **kwargs): @@ -336,14 +346,14 @@ class DomainDsdataView(DomainPermissionView, FormMixin): self.object = self.get_object() formset = self.get_form() - if 'confirm-ds' in request.POST: - request.session['dnssec_ds_confirmed'] = True - request.session['dnssec_key_confirmed'] = False + if "confirm-ds" in request.POST: + request.session["dnssec_ds_confirmed"] = True + request.session["dnssec_key_confirmed"] = False return super().form_valid(formset) - - if 'btn-cancel-click' in request.POST: - return redirect('/', {'formset': formset},RequestContext(request)) - + + if "btn-cancel-click" in request.POST: + return redirect("/", {"formset": formset}, RequestContext(request)) + if formset.is_valid(): return self.form_valid(formset) else: @@ -353,11 +363,12 @@ class DomainDsdataView(DomainPermissionView, FormMixin): """The formset is valid, perform something with it.""" # Set the nameservers from the formset - dnssecdata = {"dsData":[]} + dnssecdata = {"dsData": []} for form in formset: try: - # if 'delete' not in form.cleaned_data or form.cleaned_data['delete'] == False: + # if 'delete' not in form.cleaned_data + # or form.cleaned_data['delete'] == False: dsrecord = { "keyTag": form.cleaned_data["key_tag"], "alg": form.cleaned_data["algorithm"], @@ -378,9 +389,7 @@ class DomainDsdataView(DomainPermissionView, FormMixin): errmsg = "Error updating DNSSEC data in the registry." logger.error(errmsg) logger.error(err) - messages.error( - self.request, errmsg - ) + messages.error(self.request, errmsg) return self.form_invalid(formset) else: messages.success( @@ -388,7 +397,6 @@ class DomainDsdataView(DomainPermissionView, FormMixin): ) # superclass has the redirect return super().form_valid(formset) - class DomainKeydataView(DomainPermissionView, FormMixin): @@ -404,21 +412,28 @@ class DomainKeydataView(DomainPermissionView, FormMixin): domain = self.get_object() dnssecdata: extensions.DNSSECExtension = domain.dnssecdata initial_data = [] - - if dnssecdata is not None: + if dnssecdata is not None: if dnssecdata.dsData is not None: # TODO: Throw an error pass if dnssecdata.keyData is not None: # Add existing keydata as initial data - initial_data.extend({"flag": record.flags, "protocol": record.protocol, "algorithm": record.alg, "pub_key": record.pubKey} for record in dnssecdata.keyData) - + initial_data.extend( + { + "flag": record.flags, + "protocol": record.protocol, + "algorithm": record.alg, + "pub_key": record.pubKey, + } + for record in dnssecdata.keyData + ) + # Ensure at least 3 fields, filled or empty while len(initial_data) == 0: initial_data.append({}) - + return initial_data def get_success_url(self): @@ -431,32 +446,34 @@ class DomainKeydataView(DomainPermissionView, FormMixin): # use "formset" instead of "form" for the key context["formset"] = context.pop("form") - # set the dnssec_key_confirmed flag in the context for this view + # set the dnssec_key_confirmed flag in the context for this view # based either on the existence of Key Data in the domain, # or on the flag stored in the session domain = self.get_object() dnssecdata: extensions.DNSSECExtension = domain.dnssecdata if dnssecdata is not None and dnssecdata.keyData is not None: - self.request.session['dnssec_key_confirmed'] = True + self.request.session["dnssec_key_confirmed"] = True - context['dnssec_key_confirmed'] = self.request.session.get('dnssec_key_confirmed', False) + context["dnssec_key_confirmed"] = self.request.session.get( + "dnssec_key_confirmed", False + ) return context def post(self, request, *args, **kwargs): """Formset submission posts to this view.""" self.object = self.get_object() formset = self.get_form() - - if 'confirm-key' in request.POST: - request.session['dnssec_key_confirmed'] = True - request.session['dnssec_ds_confirmed'] = False + + if "confirm-key" in request.POST: + request.session["dnssec_key_confirmed"] = True + request.session["dnssec_ds_confirmed"] = False self.object.save() return super().form_valid(formset) - - if 'btn-cancel-click' in request.POST: - return redirect('/', {'formset': formset},RequestContext(request)) - + + if "btn-cancel-click" in request.POST: + return redirect("/", {"formset": formset}, RequestContext(request)) + if formset.is_valid(): return self.form_valid(formset) else: @@ -466,11 +483,12 @@ class DomainKeydataView(DomainPermissionView, FormMixin): """The formset is valid, perform something with it.""" # Set the nameservers from the formset - dnssecdata = {"keyData":[]} + dnssecdata = {"keyData": []} for form in formset: try: - # if 'delete' not in form.cleaned_data or form.cleaned_data['delete'] == False: + # if 'delete' not in form.cleaned_data + # or form.cleaned_data['delete'] == False: keyrecord = { "flags": form.cleaned_data["flag"], "protocol": form.cleaned_data["protocol"], @@ -490,9 +508,7 @@ class DomainKeydataView(DomainPermissionView, FormMixin): errmsg = "Error updating DNSSEC data in the registry." logger.error(errmsg) logger.error(err) - messages.error( - self.request, errmsg - ) + messages.error(self.request, errmsg) return self.form_invalid(formset) else: messages.success( From 93696c31c6822fb36d49aa808a52453cc0f5aa38 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 9 Oct 2023 16:30:04 -0400 Subject: [PATCH 48/65] first pass at dnssec rewrite --- src/registrar/models/domain.py | 73 ++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 0268cc4a4..90a0168c4 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -292,21 +292,80 @@ class Domain(TimeStampedModel, DomainHelper): logger.info("Domain does not have dnssec data defined %s" % err) return None + def getDnssecdataChanges( + self, _dnssecdata: dict + ) -> tuple[dict, dict]: + """ + calls self.dnssecdata, it should pull from cache but may result + in an epp call + returns tuple of 2 values as follows: + addExtension: dict + remExtension: dict + + addExtension includes all dsData or keyData to be added + remExtension includes all dsData or keyData to be removed + + method operates on dsData OR keyData, never a mix of the two; + operates based on which is present in _dnssecdata; + if neither is present, addExtension will be empty dict, and + remExtension will be all existing dnssecdata to be deleted + """ + + oldDnssecdata = self.dnssecdata + addDnssecdata = {} + remDnssecdata = {} + + if len(_dnssecdata["dsData"]) > 0: + # initialize addDnssecdata and remDnssecdata for dsData + addDnssecdata["dsData"] = [] + remDnssecdata["dsData"] = [] + + # if existing dsData not in new dsData, mark for removal + remDnssecdata["dsData"] = [dsData for dsData in oldDnssecdata["dsData"] if dsData not in _dnssecdata["dsData"]] + + # if new dsData not in existing dsData, mark for add + addDnssecdata["dsData"] = [dsData for dsData in _dnssecdata["dsData"] if dsData not in oldDnssecdata["dsData"]] + elif len(_dnssecdata["keyData"]) > 0: + # initialize addDnssecdata and remDnssecdata for keyData + addDnssecdata["keyData"] = [] + remDnssecdata["keyData"] = [] + + # if existing keyData not in new keyData, mark for removal + remDnssecdata["keyData"] = [keyData for keyData in oldDnssecdata["keyData"] if keyData not in _dnssecdata["keyData"]] + + # if new keyData not in existing keyData, mark for add + addDnssecdata["keyData"] = [keyData for keyData in _dnssecdata["keyData"] if keyData not in oldDnssecdata["keyData"]] + else: + # there are no new dsData or keyData, remove all + remDnssecdata["dsData"] = oldDnssecdata["dsData"] + remDnssecdata["keyData"] = oldDnssecdata["keyData"] + + return addDnssecdata, remDnssecdata + @dnssecdata.setter # type: ignore def dnssecdata(self, _dnssecdata: dict): - updateParams = { + _addDnssecdata, _remDnssecdata = self.getDnssecdataChanges(_dnssecdata) + addParams = { "maxSigLife": _dnssecdata.get("maxSigLife", None), "dsData": _dnssecdata.get("dsData", None), "keyData": _dnssecdata.get("keyData", None), - "remAllDsKeyData": True, } - request = commands.UpdateDomain(name=self.name) - extension = commands.UpdateDomainDNSSECExtension(**updateParams) - request.add_extension(extension) + remParams = { + "maxSigLife": _dnssecdata.get("maxSigLife", None), + "dsData": _dnssecdata.get("dsData", None), + "keyData": _dnssecdata.get("keyData", None), + } + addRequest = commands.UpdateDomain(name=self.name) + addExtension = commands.UpdateDomainDNSSECExtension(**addParams) + addRequest.add_extension(addExtension) + remRequest = commands.UpdateDomain(name=self.name) + remExtension = commands.UpdateDomainDNSSECExtension(**remParams) + remRequest.add_extension(remExtension) try: - registry.send(request, cleaned=True) + registry.send(addRequest, cleaned=True) + registry.send(remRequest, cleaned=True) except RegistryError as e: - logger.error("Error adding DNSSEC, code was %s error was %s" % (e.code, e)) + logger.error("Error updating DNSSEC, code was %s error was %s" % (e.code, e)) raise e @nameservers.setter # type: ignore From 6bd93d56e21357cf57c80ac9b8dbbbaf3eac34d3 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 9 Oct 2023 19:45:44 -0400 Subject: [PATCH 49/65] updated logic for setter of dnssec --- src/registrar/forms/domain.py | 5 +++ src/registrar/models/domain.py | 56 +++++++++++++++++++--------------- src/registrar/views/domain.py | 10 +++--- 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 4ee17a72d..41b15e688 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -169,6 +169,7 @@ class DomainDsdataForm(forms.Form): algorithm = forms.TypedChoiceField( required=True, label="Algorithm", + coerce=int, # need to coerce into int so dsData objects can be compared choices=[(None, "--Select--")] + ALGORITHM_CHOICES, # type: ignore error_messages={"required": ("Algorithm is required.")}, ) @@ -176,6 +177,7 @@ class DomainDsdataForm(forms.Form): digest_type = forms.TypedChoiceField( required=True, label="Digest Type", + coerce=int, # need to coerce into int so dsData objects can be compared choices=[(None, "--Select--")] + DIGEST_TYPE_CHOICES, # type: ignore error_messages={"required": ("Digest Type is required.")}, ) @@ -201,6 +203,7 @@ class DomainKeydataForm(forms.Form): flag = forms.TypedChoiceField( required=True, label="Flag", + coerce=int, choices=FLAG_CHOICES, error_messages={"required": ("Flag is required.")}, ) @@ -208,6 +211,7 @@ class DomainKeydataForm(forms.Form): protocol = forms.TypedChoiceField( required=True, label="Protocol", + coerce=int, choices=PROTOCOL_CHOICES, error_messages={"required": ("Protocol is required.")}, ) @@ -215,6 +219,7 @@ class DomainKeydataForm(forms.Form): algorithm = forms.TypedChoiceField( required=True, label="Algorithm", + coerce=int, choices=[(None, "--Select--")] + ALGORITHM_CHOICES, # type: ignore error_messages={"required": ("Algorithm is required.")}, ) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 90a0168c4..2188a1764 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -291,7 +291,7 @@ class Domain(TimeStampedModel, DomainHelper): # TODO - 433 error handling ticket should address this logger.info("Domain does not have dnssec data defined %s" % err) return None - + def getDnssecdataChanges( self, _dnssecdata: dict ) -> tuple[dict, dict]: @@ -312,33 +312,37 @@ class Domain(TimeStampedModel, DomainHelper): """ oldDnssecdata = self.dnssecdata - addDnssecdata = {} - remDnssecdata = {} + addDnssecdata = {"dsData": [], "keyData": [],} + remDnssecdata = {"dsData": [], "keyData": [],} + + if _dnssecdata and len(_dnssecdata["dsData"]) > 0: - if len(_dnssecdata["dsData"]) > 0: # initialize addDnssecdata and remDnssecdata for dsData - addDnssecdata["dsData"] = [] + addDnssecdata["dsData"] = _dnssecdata["dsData"] remDnssecdata["dsData"] = [] - # if existing dsData not in new dsData, mark for removal - remDnssecdata["dsData"] = [dsData for dsData in oldDnssecdata["dsData"] if dsData not in _dnssecdata["dsData"]] + if oldDnssecdata and len(oldDnssecdata.dsData) > 0: + # if existing dsData not in new dsData, mark for removal + remDnssecdata["dsData"] = [dsData for dsData in oldDnssecdata.dsData if dsData not in _dnssecdata["dsData"]] - # if new dsData not in existing dsData, mark for add - addDnssecdata["dsData"] = [dsData for dsData in _dnssecdata["dsData"] if dsData not in oldDnssecdata["dsData"]] - elif len(_dnssecdata["keyData"]) > 0: + # if new dsData not in existing dsData, mark for add + addDnssecdata["dsData"] = [dsData for dsData in _dnssecdata["dsData"] if dsData not in oldDnssecdata.dsData] + + elif _dnssecdata and len(_dnssecdata["keyData"]) > 0: # initialize addDnssecdata and remDnssecdata for keyData - addDnssecdata["keyData"] = [] + addDnssecdata["keyData"] = _dnssecdata["keyData"] remDnssecdata["keyData"] = [] - # if existing keyData not in new keyData, mark for removal - remDnssecdata["keyData"] = [keyData for keyData in oldDnssecdata["keyData"] if keyData not in _dnssecdata["keyData"]] + if oldDnssecdata and len(oldDnssecdata.keyData) > 0: + # if existing keyData not in new keyData, mark for removal + remDnssecdata["keyData"] = [keyData for keyData in oldDnssecdata.keyData if keyData not in _dnssecdata["keyData"]] - # if new keyData not in existing keyData, mark for add - addDnssecdata["keyData"] = [keyData for keyData in _dnssecdata["keyData"] if keyData not in oldDnssecdata["keyData"]] + # if new keyData not in existing keyData, mark for add + addDnssecdata["keyData"] = [keyData for keyData in _dnssecdata["keyData"] if keyData not in oldDnssecdata.keyData] else: # there are no new dsData or keyData, remove all - remDnssecdata["dsData"] = oldDnssecdata["dsData"] - remDnssecdata["keyData"] = oldDnssecdata["keyData"] + remDnssecdata["dsData"] = getattr(oldDnssecdata, "dsData", None) + remDnssecdata["keyData"] = getattr(oldDnssecdata, "keyData", None) return addDnssecdata, remDnssecdata @@ -346,14 +350,14 @@ class Domain(TimeStampedModel, DomainHelper): def dnssecdata(self, _dnssecdata: dict): _addDnssecdata, _remDnssecdata = self.getDnssecdataChanges(_dnssecdata) addParams = { - "maxSigLife": _dnssecdata.get("maxSigLife", None), - "dsData": _dnssecdata.get("dsData", None), - "keyData": _dnssecdata.get("keyData", None), + "maxSigLife": _addDnssecdata.get("maxSigLife", None), + "dsData": _addDnssecdata.get("dsData", None), + "keyData": _addDnssecdata.get("keyData", None), } remParams = { - "maxSigLife": _dnssecdata.get("maxSigLife", None), - "dsData": _dnssecdata.get("dsData", None), - "keyData": _dnssecdata.get("keyData", None), + "maxSigLife": _remDnssecdata.get("maxSigLife", None), + "remDsData": _remDnssecdata.get("dsData", None), + "remKeyData": _remDnssecdata.get("keyData", None), } addRequest = commands.UpdateDomain(name=self.name) addExtension = commands.UpdateDomainDNSSECExtension(**addParams) @@ -362,8 +366,10 @@ class Domain(TimeStampedModel, DomainHelper): remExtension = commands.UpdateDomainDNSSECExtension(**remParams) remRequest.add_extension(remExtension) try: - registry.send(addRequest, cleaned=True) - registry.send(remRequest, cleaned=True) + if len(_addDnssecdata.get("dsData", [])) > 0 or len(_addDnssecdata.get("keyData",[])) > 0: + registry.send(addRequest, cleaned=True) + if len(_remDnssecdata.get("dsData", [])) > 0 or len(_remDnssecdata.get("keyData", [])) > 0: + registry.send(remRequest, cleaned=True) except RegistryError as e: logger.error("Error updating DNSSEC, code was %s error was %s" % (e.code, e)) raise e diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index e51395ba3..b4509b162 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -371,8 +371,8 @@ class DomainDsdataView(DomainPermissionView, FormMixin): # or form.cleaned_data['delete'] == False: dsrecord = { "keyTag": form.cleaned_data["key_tag"], - "alg": form.cleaned_data["algorithm"], - "digestType": form.cleaned_data["digest_type"], + "alg": int(form.cleaned_data["algorithm"]), + "digestType": int(form.cleaned_data["digest_type"]), "digest": form.cleaned_data["digest"], } dnssecdata["dsData"].append(common.DSData(**dsrecord)) @@ -490,9 +490,9 @@ class DomainKeydataView(DomainPermissionView, FormMixin): # if 'delete' not in form.cleaned_data # or form.cleaned_data['delete'] == False: keyrecord = { - "flags": form.cleaned_data["flag"], - "protocol": form.cleaned_data["protocol"], - "alg": form.cleaned_data["algorithm"], + "flags": int(form.cleaned_data["flag"]), + "protocol": int(form.cleaned_data["protocol"]), + "alg": int(form.cleaned_data["algorithm"]), "pubKey": form.cleaned_data["pub_key"], } dnssecdata["keyData"].append(common.DNSSECKeyData(**keyrecord)) From 95b1a02789c9fb80222740c2a369c259708db5df Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 10 Oct 2023 05:25:15 -0400 Subject: [PATCH 50/65] modified unit tests --- src/registrar/tests/common.py | 55 ++++++---- src/registrar/tests/test_models_domain.py | 126 +++++++++++++++++----- 2 files changed, 136 insertions(+), 45 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 60a23b3b4..6ede8965b 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -705,17 +705,23 @@ class MockEppLib(TestCase): "pubKey": "AQPJ////4Q==", } dnssecExtensionWithDsData: Mapping[Any, Any] = { - "dsData": [common.DSData(**addDsData1)] # type: ignore + "dsData": [common.DSData(**addDsData1)], # type: ignore + "keyData": [], } dnssecExtensionWithMultDsData: Mapping[str, Any] = { "dsData": [ common.DSData(**addDsData1), # type: ignore common.DSData(**addDsData2), # type: ignore ], + "keyData": [], } dnssecExtensionWithKeyData: Mapping[str, Any] = { - "maxSigLife": 3215, "keyData": [common.DNSSECKeyData(**keyDataDict)], # type: ignore + "dsData": [], + } + dnssecExtensionRemovingDsData: Mapping[Any, Any] = { + "dsData": [], + "keyData": [], } def mockSend(self, _request, cleaned): @@ -756,26 +762,35 @@ class MockEppLib(TestCase): if getattr(_request, "name", None) == "security.gov": return MagicMock(res_data=[self.infoDomainNoContact]) elif getattr(_request, "name", None) == "dnssec-dsdata.gov": - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[ - extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) - ], - ) + if self.mockedSendFunction.call_count == 1: + return MagicMock(res_data=[self.mockDataInfoDomain]) + else: + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[ + extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) + ], + ) elif getattr(_request, "name", None) == "dnssec-multdsdata.gov": - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[ - extensions.DNSSECExtension(**self.dnssecExtensionWithMultDsData) - ], - ) + if self.mockedSendFunction.call_count == 1: + return MagicMock(res_data=[self.mockDataInfoDomain]) + else: + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[ + extensions.DNSSECExtension(**self.dnssecExtensionWithMultDsData) + ], + ) elif getattr(_request, "name", None) == "dnssec-keydata.gov": - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[ - extensions.DNSSECExtension(**self.dnssecExtensionWithKeyData) - ], - ) + if self.mockedSendFunction.call_count == 1: + return MagicMock(res_data=[self.mockDataInfoDomain]) + else: + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[ + extensions.DNSSECExtension(**self.dnssecExtensionWithKeyData) + ], + ) elif getattr(_request, "name", None) == "dnssec-none.gov": # this case is not necessary, but helps improve readability return MagicMock(res_data=[self.mockDataInfoDomain]) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index d0718b4ba..b8d648e45 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -984,15 +984,25 @@ class TestRegistrantDNSSEC(MockEppLib): """Rule: Registrants may modify their secure DNS data""" # helper function to create UpdateDomainDNSSECExtention object for verification - def createUpdateExtension(self, dnssecdata: extensions.DNSSECExtension): - return commands.UpdateDomainDNSSECExtension( - maxSigLife=dnssecdata.maxSigLife, - dsData=dnssecdata.dsData, - keyData=dnssecdata.keyData, - remDsData=None, - remKeyData=None, - remAllDsKeyData=True, - ) + def createUpdateExtension(self, dnssecdata: extensions.DNSSECExtension, remove=False): + if not remove: + return commands.UpdateDomainDNSSECExtension( + maxSigLife=dnssecdata.maxSigLife, + dsData=dnssecdata.dsData, + keyData=dnssecdata.keyData, + remDsData=None, + remKeyData=None, + remAllDsKeyData=False, + ) + else: + return commands.UpdateDomainDNSSECExtension( + maxSigLife=dnssecdata.maxSigLife, + dsData=None, + keyData=None, + remDsData=dnssecdata.dsData, + remKeyData=dnssecdata.keyData, + remAllDsKeyData=False, + ) def setUp(self): """ @@ -1010,25 +1020,25 @@ class TestRegistrantDNSSEC(MockEppLib): def test_user_adds_dnssec_data(self): """ - Scenario: Registrant adds DNSSEC data. + Scenario: Registrant adds DNSSEC ds data. Verify that both the setter and getter are functioning properly This test verifies: - 1 - setter calls UpdateDomain command - 2 - setter adds the UpdateDNSSECExtension extension to the command - 3 - setter causes the getter to call info domain on next get from cache - 4 - getter properly parses dnssecdata from InfoDomain response and sets to cache + 1 - setter initially calls InfoDomain command + 2 - setter then calls UpdateDomain command + 3 - setter adds the UpdateDNSSECExtension extension to the command + 4 - setter causes the getter to call info domain on next get from cache + 5 - getter properly parses dnssecdata from InfoDomain response and sets to cache """ domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") - domain.dnssecdata = self.dnssecExtensionWithDsData # get the DNS SEC extension added to the UpdateDomain command and # verify that it is properly sent # args[0] is the _request sent to registry args, _ = self.mockedSendFunction.call_args - # assert that the extension matches + # assert that the extension on the update matches self.assertEquals( args[0].extensions[0], self.createUpdateExtension( @@ -1039,6 +1049,12 @@ class TestRegistrantDNSSEC(MockEppLib): dnssecdata_get = domain.dnssecdata self.mockedSendFunction.assert_has_calls( [ + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, + ), call( commands.UpdateDomain( name="dnssec-dsdata.gov", @@ -1071,9 +1087,11 @@ class TestRegistrantDNSSEC(MockEppLib): # registry normally sends in this case This test verifies: - 1 - UpdateDomain command called twice - 2 - setter causes the getter to call info domain on next get from cache - 3 - getter properly parses dnssecdata from InfoDomain response and sets to cache + 1 - InfoDomain command is called first + 2 - UpdateDomain command called on the initial setter + 3 - setter causes the getter to call info domain on next get from cache + 4 - UpdateDomain command is not called on second setter (no change) + 5 - getter properly parses dnssecdata from InfoDomain response and sets to cache """ @@ -1088,12 +1106,8 @@ class TestRegistrantDNSSEC(MockEppLib): self.mockedSendFunction.assert_has_calls( [ call( - commands.UpdateDomain( + commands.InfoDomain( name="dnssec-dsdata.gov", - nsset=None, - keyset=None, - registrant=None, - auth_info=None, ), cleaned=True, ), @@ -1113,6 +1127,12 @@ class TestRegistrantDNSSEC(MockEppLib): ), cleaned=True, ), + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, + ), ] ) @@ -1174,9 +1194,65 @@ class TestRegistrantDNSSEC(MockEppLib): dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData["dsData"] ) + def test_user_removes_dnssec_data(self): + """ + Scenario: Registrant removes DNSSEC ds data. + Verify that both the setter and getter are functioning properly + + This test verifies: + 1 - setter initially calls InfoDomain command + 2 - invalidate cache forces second InfoDomain command (to match mocks) + 3 - setter then calls UpdateDomain command + 4 - setter adds the UpdateDNSSECExtension extension to the command with rem + + """ + + domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") + dnssecdata_get_initial = domain.dnssecdata # call to force initial mock + domain._invalidate_cache() + domain.dnssecdata = self.dnssecExtensionRemovingDsData + # get the DNS SEC extension added to the UpdateDomain command and + # verify that it is properly sent + # args[0] is the _request sent to registry + args, _ = self.mockedSendFunction.call_args + # assert that the extension on the update matches + self.assertEquals( + args[0].extensions[0], + self.createUpdateExtension( + extensions.DNSSECExtension(**self.dnssecExtensionWithDsData), + remove=True + ), + ) + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, + ), + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, + ), + call( + commands.UpdateDomain( + name="dnssec-dsdata.gov", + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ), + ] + ) + def test_user_adds_dnssec_keydata(self): """ - Scenario: Registrant adds DNSSEC data. + Scenario: Registrant adds DNSSEC key data. Verify that both the setter and getter are functioning properly This test verifies: From 9e8aa2dee33266cf1291cb830151ac4e3da71970 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 10 Oct 2023 08:08:16 -0400 Subject: [PATCH 51/65] properly typed extensions.DNSSECExtension --- src/registrar/models/domain.py | 104 +++++++++++++++++----- src/registrar/tests/common.py | 7 +- src/registrar/tests/test_models_domain.py | 42 ++++++--- src/registrar/views/domain.py | 16 ++-- 4 files changed, 120 insertions(+), 49 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 2188a1764..c539838e0 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -2,6 +2,7 @@ from itertools import zip_longest import logging from datetime import date from string import digits +from typing import Optional from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore from django.db import models @@ -283,7 +284,7 @@ class Domain(TimeStampedModel, DomainHelper): return e.code @Cache - def dnssecdata(self) -> extensions.DNSSECExtension: + def dnssecdata(self) -> Optional[extensions.DNSSECExtension]: try: return self._get_property("dnssecdata") except Exception as err: @@ -291,17 +292,18 @@ class Domain(TimeStampedModel, DomainHelper): # TODO - 433 error handling ticket should address this logger.info("Domain does not have dnssec data defined %s" % err) return None - + def getDnssecdataChanges( - self, _dnssecdata: dict - ) -> tuple[dict, dict]: + self, + _dnssecdata: Optional[extensions.DNSSECExtension] + ) -> tuple[dict, dict]: """ calls self.dnssecdata, it should pull from cache but may result in an epp call returns tuple of 2 values as follows: addExtension: dict remExtension: dict - + addExtension includes all dsData or keyData to be added remExtension includes all dsData or keyData to be removed @@ -311,34 +313,76 @@ class Domain(TimeStampedModel, DomainHelper): remExtension will be all existing dnssecdata to be deleted """ + if isinstance(_dnssecdata, extensions.DNSSECExtension): + logger.info("extension is properly typed") + else: + logger.info("extension is NOT properly typed") + oldDnssecdata = self.dnssecdata - addDnssecdata = {"dsData": [], "keyData": [],} - remDnssecdata = {"dsData": [], "keyData": [],} - - if _dnssecdata and len(_dnssecdata["dsData"]) > 0: + addDnssecdata: dict = {} + # "dsData": [], + # "keyData": [], + # } + remDnssecdata: dict = {} + # "dsData": [], + # "keyData": [], + # } + if _dnssecdata and _dnssecdata.dsData is not None: + logger.info("there is submitted dsdata for comparison") + logger.info("there is %s submitted records", len(_dnssecdata.dsData)) # initialize addDnssecdata and remDnssecdata for dsData - addDnssecdata["dsData"] = _dnssecdata["dsData"] - remDnssecdata["dsData"] = [] + addDnssecdata["dsData"] = _dnssecdata.dsData + # remDnssecdata["dsData"] = [] if oldDnssecdata and len(oldDnssecdata.dsData) > 0: + logger.info("there is existing ds data for comparison") + logger.info("there is %s existing records for compare", len(oldDnssecdata.dsData)) # if existing dsData not in new dsData, mark for removal - remDnssecdata["dsData"] = [dsData for dsData in oldDnssecdata.dsData if dsData not in _dnssecdata["dsData"]] + dsDataForRemoval = [ + dsData + for dsData in oldDnssecdata.dsData + if dsData not in _dnssecdata.dsData + ] + if len(dsDataForRemoval) > 0: + logger.info("ds data marked for removal") + remDnssecdata["dsData"] = dsDataForRemoval # if new dsData not in existing dsData, mark for add - addDnssecdata["dsData"] = [dsData for dsData in _dnssecdata["dsData"] if dsData not in oldDnssecdata.dsData] - - elif _dnssecdata and len(_dnssecdata["keyData"]) > 0: + dsDataForAdd = [ + dsData + for dsData in _dnssecdata.dsData + if dsData not in oldDnssecdata.dsData + ] + if len(dsDataForAdd) > 0: + logger.info("ds data marked for add") + addDnssecdata["dsData"] = dsDataForAdd + else: + addDnssecdata["dsData"] = None + + elif _dnssecdata and _dnssecdata.keyData is not None: # initialize addDnssecdata and remDnssecdata for keyData - addDnssecdata["keyData"] = _dnssecdata["keyData"] - remDnssecdata["keyData"] = [] + addDnssecdata["keyData"] = _dnssecdata.keyData + # remDnssecdata["keyData"] = [] if oldDnssecdata and len(oldDnssecdata.keyData) > 0: # if existing keyData not in new keyData, mark for removal - remDnssecdata["keyData"] = [keyData for keyData in oldDnssecdata.keyData if keyData not in _dnssecdata["keyData"]] + keyDataForRemoval = [ + keyData + for keyData in oldDnssecdata.keyData + if keyData not in _dnssecdata.keyData + ] + if len(keyDataForRemoval) > 0: + remDnssecdata["keyData"] = keyDataForRemoval # if new keyData not in existing keyData, mark for add - addDnssecdata["keyData"] = [keyData for keyData in _dnssecdata["keyData"] if keyData not in oldDnssecdata.keyData] + keyDataForAdd = [ + keyData + for keyData in _dnssecdata.keyData + if keyData not in oldDnssecdata.keyData + ] + if len(keyDataForAdd) > 0: + addDnssecdata["keyData"] = keyDataForAdd else: # there are no new dsData or keyData, remove all remDnssecdata["dsData"] = getattr(oldDnssecdata, "dsData", None) @@ -347,7 +391,7 @@ class Domain(TimeStampedModel, DomainHelper): return addDnssecdata, remDnssecdata @dnssecdata.setter # type: ignore - def dnssecdata(self, _dnssecdata: dict): + def dnssecdata(self, _dnssecdata: Optional[extensions.DNSSECExtension]): _addDnssecdata, _remDnssecdata = self.getDnssecdataChanges(_dnssecdata) addParams = { "maxSigLife": _addDnssecdata.get("maxSigLife", None), @@ -366,12 +410,26 @@ class Domain(TimeStampedModel, DomainHelper): remExtension = commands.UpdateDomainDNSSECExtension(**remParams) remRequest.add_extension(remExtension) try: - if len(_addDnssecdata.get("dsData", [])) > 0 or len(_addDnssecdata.get("keyData",[])) > 0: + if ( + "dsData" in _addDnssecdata and + _addDnssecdata["dsData"] is not None + or "keyData" in _addDnssecdata and + _addDnssecdata["keyData"] is not None + ): + logger.info("sending addition") registry.send(addRequest, cleaned=True) - if len(_remDnssecdata.get("dsData", [])) > 0 or len(_remDnssecdata.get("keyData", [])) > 0: + if ( + "dsData" in _remDnssecdata and + _remDnssecdata["dsData"] is not None + or "keyData" in _remDnssecdata and + _remDnssecdata["keyData"] is not None + ): + logger.info("sending removal") registry.send(remRequest, cleaned=True) except RegistryError as e: - logger.error("Error updating DNSSEC, code was %s error was %s" % (e.code, e)) + logger.error( + "Error updating DNSSEC, code was %s error was %s" % (e.code, e) + ) raise e @nameservers.setter # type: ignore diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 6ede8965b..4d399fd29 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -706,22 +706,19 @@ class MockEppLib(TestCase): } dnssecExtensionWithDsData: Mapping[Any, Any] = { "dsData": [common.DSData(**addDsData1)], # type: ignore - "keyData": [], } dnssecExtensionWithMultDsData: Mapping[str, Any] = { "dsData": [ common.DSData(**addDsData1), # type: ignore common.DSData(**addDsData2), # type: ignore ], - "keyData": [], } dnssecExtensionWithKeyData: Mapping[str, Any] = { "keyData": [common.DNSSECKeyData(**keyDataDict)], # type: ignore - "dsData": [], } dnssecExtensionRemovingDsData: Mapping[Any, Any] = { - "dsData": [], - "keyData": [], + "dsData": None, + "keyData": None, } def mockSend(self, _request, cleaned): diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index b8d648e45..2a0a820ec 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -984,7 +984,9 @@ class TestRegistrantDNSSEC(MockEppLib): """Rule: Registrants may modify their secure DNS data""" # helper function to create UpdateDomainDNSSECExtention object for verification - def createUpdateExtension(self, dnssecdata: extensions.DNSSECExtension, remove=False): + def createUpdateExtension( + self, dnssecdata: extensions.DNSSECExtension, remove=False + ): if not remove: return commands.UpdateDomainDNSSECExtension( maxSigLife=dnssecdata.maxSigLife, @@ -1033,7 +1035,9 @@ class TestRegistrantDNSSEC(MockEppLib): """ domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") - domain.dnssecdata = self.dnssecExtensionWithDsData + domain.dnssecdata = extensions.DNSSECExtension( + **self.dnssecExtensionWithDsData + ) # get the DNS SEC extension added to the UpdateDomain command and # verify that it is properly sent # args[0] is the _request sent to registry @@ -1098,9 +1102,9 @@ class TestRegistrantDNSSEC(MockEppLib): domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") # set the dnssecdata once - domain.dnssecdata = self.dnssecExtensionWithDsData + domain.dnssecdata = extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) # set the dnssecdata again - domain.dnssecdata = self.dnssecExtensionWithDsData + domain.dnssecdata = extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) # test that the dnssecdata getter is functioning properly dnssecdata_get = domain.dnssecdata self.mockedSendFunction.assert_has_calls( @@ -1155,7 +1159,7 @@ class TestRegistrantDNSSEC(MockEppLib): domain, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") - domain.dnssecdata = self.dnssecExtensionWithMultDsData + domain.dnssecdata = extensions.DNSSECExtension(**self.dnssecExtensionWithMultDsData) # get the DNS SEC extension added to the UpdateDomain command # and verify that it is properly sent # args[0] is the _request sent to registry @@ -1201,16 +1205,18 @@ class TestRegistrantDNSSEC(MockEppLib): This test verifies: 1 - setter initially calls InfoDomain command - 2 - invalidate cache forces second InfoDomain command (to match mocks) + 2 - first setter calls UpdateDomain command + 3 - second setter calls InfoDomain command again 3 - setter then calls UpdateDomain command 4 - setter adds the UpdateDNSSECExtension extension to the command with rem """ domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") - dnssecdata_get_initial = domain.dnssecdata # call to force initial mock - domain._invalidate_cache() - domain.dnssecdata = self.dnssecExtensionRemovingDsData + # dnssecdata_get_initial = domain.dnssecdata # call to force initial mock + # domain._invalidate_cache() + domain.dnssecdata = extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) + domain.dnssecdata = extensions.DNSSECExtension(**self.dnssecExtensionRemovingDsData) # get the DNS SEC extension added to the UpdateDomain command and # verify that it is properly sent # args[0] is the _request sent to registry @@ -1220,7 +1226,7 @@ class TestRegistrantDNSSEC(MockEppLib): args[0].extensions[0], self.createUpdateExtension( extensions.DNSSECExtension(**self.dnssecExtensionWithDsData), - remove=True + remove=True, ), ) self.mockedSendFunction.assert_has_calls( @@ -1231,6 +1237,16 @@ class TestRegistrantDNSSEC(MockEppLib): ), cleaned=True, ), + call( + commands.UpdateDomain( + name="dnssec-dsdata.gov", + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ), call( commands.InfoDomain( name="dnssec-dsdata.gov", @@ -1246,7 +1262,7 @@ class TestRegistrantDNSSEC(MockEppLib): auth_info=None, ), cleaned=True, - ), + ), ] ) @@ -1265,7 +1281,7 @@ class TestRegistrantDNSSEC(MockEppLib): domain, _ = Domain.objects.get_or_create(name="dnssec-keydata.gov") - domain.dnssecdata = self.dnssecExtensionWithKeyData + domain.dnssecdata = extensions.DNSSECExtension(**self.dnssecExtensionWithKeyData) # get the DNS SEC extension added to the UpdateDomain command # and verify that it is properly sent # args[0] is the _request sent to registry @@ -1314,7 +1330,7 @@ class TestRegistrantDNSSEC(MockEppLib): domain, _ = Domain.objects.get_or_create(name="dnssec-invalid.gov") with self.assertRaises(RegistryError) as err: - domain.dnssecdata = self.dnssecExtensionWithDsData + domain.dnssecdata = extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) self.assertTrue( err.is_client_error() or err.is_session_error() or err.is_server_error() ) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index b4509b162..6cc89682e 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -362,8 +362,8 @@ class DomainDsdataView(DomainPermissionView, FormMixin): def form_valid(self, formset): """The formset is valid, perform something with it.""" - # Set the nameservers from the formset - dnssecdata = {"dsData": []} + # Set the dnssecdata from the formset + dnssecdata = extensions.DNSSECExtension() for form in formset: try: @@ -375,13 +375,13 @@ class DomainDsdataView(DomainPermissionView, FormMixin): "digestType": int(form.cleaned_data["digest_type"]), "digest": form.cleaned_data["digest"], } - dnssecdata["dsData"].append(common.DSData(**dsrecord)) + if dnssecdata.dsData is None: + dnssecdata.dsData = [] + dnssecdata.dsData.append(common.DSData(**dsrecord)) except KeyError: # no server information in this field, skip it pass domain = self.get_object() - if len(dnssecdata["dsData"]) == 0: - dnssecdata = {} try: domain.dnssecdata = dnssecdata except RegistryError as err: @@ -483,7 +483,7 @@ class DomainKeydataView(DomainPermissionView, FormMixin): """The formset is valid, perform something with it.""" # Set the nameservers from the formset - dnssecdata = {"keyData": []} + dnssecdata = extensions.DNSSECExtension() for form in formset: try: @@ -495,13 +495,13 @@ class DomainKeydataView(DomainPermissionView, FormMixin): "alg": int(form.cleaned_data["algorithm"]), "pubKey": form.cleaned_data["pub_key"], } + if dnssecdata.keyData is None: + dnssecdata.keyData = [] dnssecdata["keyData"].append(common.DNSSECKeyData(**keyrecord)) except KeyError: # no server information in this field, skip it pass domain = self.get_object() - if len(dnssecdata["keyData"]) == 0: - dnssecdata = {} try: domain.dnssecdata = dnssecdata except RegistryError as err: From cb15b7d0296315190cb29adac71391731f5eba4d Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 10 Oct 2023 08:43:36 -0400 Subject: [PATCH 52/65] cleaned up tests --- src/registrar/tests/common.py | 25 +++++++-------- src/registrar/tests/test_models_domain.py | 37 +++++++++++------------ 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 4d399fd29..d28171155 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -704,22 +704,19 @@ class MockEppLib(TestCase): "alg": 1, "pubKey": "AQPJ////4Q==", } - dnssecExtensionWithDsData: Mapping[Any, Any] = { - "dsData": [common.DSData(**addDsData1)], # type: ignore - } - dnssecExtensionWithMultDsData: Mapping[str, Any] = { + dnssecExtensionWithDsData = extensions.DNSSECExtension(**{ + "dsData": [common.DSData(**addDsData1)], + }) + dnssecExtensionWithMultDsData = extensions.DNSSECExtension(**{ "dsData": [ common.DSData(**addDsData1), # type: ignore common.DSData(**addDsData2), # type: ignore ], - } - dnssecExtensionWithKeyData: Mapping[str, Any] = { + }) + dnssecExtensionWithKeyData = extensions.DNSSECExtension(**{ "keyData": [common.DNSSECKeyData(**keyDataDict)], # type: ignore - } - dnssecExtensionRemovingDsData: Mapping[Any, Any] = { - "dsData": None, - "keyData": None, - } + }) + dnssecExtensionRemovingDsData = extensions.DNSSECExtension() def mockSend(self, _request, cleaned): """Mocks the registry.send function used inside of domain.py @@ -765,7 +762,7 @@ class MockEppLib(TestCase): return MagicMock( res_data=[self.mockDataInfoDomain], extensions=[ - extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) + self.dnssecExtensionWithDsData ], ) elif getattr(_request, "name", None) == "dnssec-multdsdata.gov": @@ -775,7 +772,7 @@ class MockEppLib(TestCase): return MagicMock( res_data=[self.mockDataInfoDomain], extensions=[ - extensions.DNSSECExtension(**self.dnssecExtensionWithMultDsData) + self.dnssecExtensionWithMultDsData ], ) elif getattr(_request, "name", None) == "dnssec-keydata.gov": @@ -785,7 +782,7 @@ class MockEppLib(TestCase): return MagicMock( res_data=[self.mockDataInfoDomain], extensions=[ - extensions.DNSSECExtension(**self.dnssecExtensionWithKeyData) + self.dnssecExtensionWithKeyData ], ) elif getattr(_request, "name", None) == "dnssec-none.gov": diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 2a0a820ec..05f56e0f4 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1035,9 +1035,8 @@ class TestRegistrantDNSSEC(MockEppLib): """ domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") - domain.dnssecdata = extensions.DNSSECExtension( - **self.dnssecExtensionWithDsData - ) + domain.dnssecdata = self.dnssecExtensionWithDsData + # get the DNS SEC extension added to the UpdateDomain command and # verify that it is properly sent # args[0] is the _request sent to registry @@ -1046,7 +1045,7 @@ class TestRegistrantDNSSEC(MockEppLib): self.assertEquals( args[0].extensions[0], self.createUpdateExtension( - extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) + self.dnssecExtensionWithDsData ), ) # test that the dnssecdata getter is functioning properly @@ -1079,7 +1078,7 @@ class TestRegistrantDNSSEC(MockEppLib): ) self.assertEquals( - dnssecdata_get.dsData, self.dnssecExtensionWithDsData["dsData"] + dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData ) def test_dnssec_is_idempotent(self): @@ -1096,15 +1095,15 @@ class TestRegistrantDNSSEC(MockEppLib): 3 - setter causes the getter to call info domain on next get from cache 4 - UpdateDomain command is not called on second setter (no change) 5 - getter properly parses dnssecdata from InfoDomain response and sets to cache - + """ domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") # set the dnssecdata once - domain.dnssecdata = extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) + domain.dnssecdata = self.dnssecExtensionWithDsData # set the dnssecdata again - domain.dnssecdata = extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) + domain.dnssecdata = self.dnssecExtensionWithDsData # test that the dnssecdata getter is functioning properly dnssecdata_get = domain.dnssecdata self.mockedSendFunction.assert_has_calls( @@ -1141,7 +1140,7 @@ class TestRegistrantDNSSEC(MockEppLib): ) self.assertEquals( - dnssecdata_get.dsData, self.dnssecExtensionWithDsData["dsData"] + dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData ) def test_user_adds_dnssec_data_multiple_dsdata(self): @@ -1159,7 +1158,7 @@ class TestRegistrantDNSSEC(MockEppLib): domain, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") - domain.dnssecdata = extensions.DNSSECExtension(**self.dnssecExtensionWithMultDsData) + domain.dnssecdata = self.dnssecExtensionWithMultDsData # get the DNS SEC extension added to the UpdateDomain command # and verify that it is properly sent # args[0] is the _request sent to registry @@ -1168,7 +1167,7 @@ class TestRegistrantDNSSEC(MockEppLib): self.assertEquals( args[0].extensions[0], self.createUpdateExtension( - extensions.DNSSECExtension(**self.dnssecExtensionWithMultDsData) + self.dnssecExtensionWithMultDsData ), ) # test that the dnssecdata getter is functioning properly @@ -1195,7 +1194,7 @@ class TestRegistrantDNSSEC(MockEppLib): ) self.assertEquals( - dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData["dsData"] + dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData.dsData ) def test_user_removes_dnssec_data(self): @@ -1215,8 +1214,8 @@ class TestRegistrantDNSSEC(MockEppLib): domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") # dnssecdata_get_initial = domain.dnssecdata # call to force initial mock # domain._invalidate_cache() - domain.dnssecdata = extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) - domain.dnssecdata = extensions.DNSSECExtension(**self.dnssecExtensionRemovingDsData) + domain.dnssecdata = self.dnssecExtensionWithDsData + domain.dnssecdata = self.dnssecExtensionRemovingDsData # get the DNS SEC extension added to the UpdateDomain command and # verify that it is properly sent # args[0] is the _request sent to registry @@ -1225,7 +1224,7 @@ class TestRegistrantDNSSEC(MockEppLib): self.assertEquals( args[0].extensions[0], self.createUpdateExtension( - extensions.DNSSECExtension(**self.dnssecExtensionWithDsData), + self.dnssecExtensionWithDsData, remove=True, ), ) @@ -1281,7 +1280,7 @@ class TestRegistrantDNSSEC(MockEppLib): domain, _ = Domain.objects.get_or_create(name="dnssec-keydata.gov") - domain.dnssecdata = extensions.DNSSECExtension(**self.dnssecExtensionWithKeyData) + domain.dnssecdata = self.dnssecExtensionWithKeyData # get the DNS SEC extension added to the UpdateDomain command # and verify that it is properly sent # args[0] is the _request sent to registry @@ -1290,7 +1289,7 @@ class TestRegistrantDNSSEC(MockEppLib): self.assertEquals( args[0].extensions[0], self.createUpdateExtension( - extensions.DNSSECExtension(**self.dnssecExtensionWithKeyData) + self.dnssecExtensionWithKeyData ), ) # test that the dnssecdata getter is functioning properly @@ -1317,7 +1316,7 @@ class TestRegistrantDNSSEC(MockEppLib): ) self.assertEquals( - dnssecdata_get.keyData, self.dnssecExtensionWithKeyData["keyData"] + dnssecdata_get.keyData, self.dnssecExtensionWithKeyData.keyData ) def test_update_is_unsuccessful(self): @@ -1330,7 +1329,7 @@ class TestRegistrantDNSSEC(MockEppLib): domain, _ = Domain.objects.get_or_create(name="dnssec-invalid.gov") with self.assertRaises(RegistryError) as err: - domain.dnssecdata = extensions.DNSSECExtension(**self.dnssecExtensionWithDsData) + domain.dnssecdata = self.dnssecExtensionWithDsData self.assertTrue( err.is_client_error() or err.is_session_error() or err.is_server_error() ) From 950014ea4780d67318692877fb95b12c740e1820 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 10 Oct 2023 08:48:14 -0400 Subject: [PATCH 53/65] removed some comments and debug statements --- src/registrar/models/domain.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index c539838e0..4226e6ac9 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -313,31 +313,15 @@ class Domain(TimeStampedModel, DomainHelper): remExtension will be all existing dnssecdata to be deleted """ - if isinstance(_dnssecdata, extensions.DNSSECExtension): - logger.info("extension is properly typed") - else: - logger.info("extension is NOT properly typed") - oldDnssecdata = self.dnssecdata addDnssecdata: dict = {} - # "dsData": [], - # "keyData": [], - # } remDnssecdata: dict = {} - # "dsData": [], - # "keyData": [], - # } if _dnssecdata and _dnssecdata.dsData is not None: - logger.info("there is submitted dsdata for comparison") - logger.info("there is %s submitted records", len(_dnssecdata.dsData)) # initialize addDnssecdata and remDnssecdata for dsData addDnssecdata["dsData"] = _dnssecdata.dsData - # remDnssecdata["dsData"] = [] if oldDnssecdata and len(oldDnssecdata.dsData) > 0: - logger.info("there is existing ds data for comparison") - logger.info("there is %s existing records for compare", len(oldDnssecdata.dsData)) # if existing dsData not in new dsData, mark for removal dsDataForRemoval = [ dsData @@ -345,7 +329,6 @@ class Domain(TimeStampedModel, DomainHelper): if dsData not in _dnssecdata.dsData ] if len(dsDataForRemoval) > 0: - logger.info("ds data marked for removal") remDnssecdata["dsData"] = dsDataForRemoval # if new dsData not in existing dsData, mark for add @@ -355,7 +338,6 @@ class Domain(TimeStampedModel, DomainHelper): if dsData not in oldDnssecdata.dsData ] if len(dsDataForAdd) > 0: - logger.info("ds data marked for add") addDnssecdata["dsData"] = dsDataForAdd else: addDnssecdata["dsData"] = None @@ -363,7 +345,6 @@ class Domain(TimeStampedModel, DomainHelper): elif _dnssecdata and _dnssecdata.keyData is not None: # initialize addDnssecdata and remDnssecdata for keyData addDnssecdata["keyData"] = _dnssecdata.keyData - # remDnssecdata["keyData"] = [] if oldDnssecdata and len(oldDnssecdata.keyData) > 0: # if existing keyData not in new keyData, mark for removal @@ -416,7 +397,6 @@ class Domain(TimeStampedModel, DomainHelper): or "keyData" in _addDnssecdata and _addDnssecdata["keyData"] is not None ): - logger.info("sending addition") registry.send(addRequest, cleaned=True) if ( "dsData" in _remDnssecdata and @@ -424,7 +404,6 @@ class Domain(TimeStampedModel, DomainHelper): or "keyData" in _remDnssecdata and _remDnssecdata["keyData"] is not None ): - logger.info("sending removal") registry.send(remRequest, cleaned=True) except RegistryError as e: logger.error( From f447df6d6468f0d21b250eb22851292c25df0be7 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 10 Oct 2023 10:50:24 -0400 Subject: [PATCH 54/65] tests cleanup and formatting for linter --- src/registrar/models/domain.py | 21 ++-- src/registrar/tests/common.py | 73 +++++------ src/registrar/tests/test_models_domain.py | 147 ++++++++++++++++++---- src/registrar/views/domain.py | 2 +- 4 files changed, 165 insertions(+), 78 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 4226e6ac9..f5e229b0a 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -294,9 +294,8 @@ class Domain(TimeStampedModel, DomainHelper): return None def getDnssecdataChanges( - self, - _dnssecdata: Optional[extensions.DNSSECExtension] - ) -> tuple[dict, dict]: + self, _dnssecdata: Optional[extensions.DNSSECExtension] + ) -> tuple[dict, dict]: """ calls self.dnssecdata, it should pull from cache but may result in an epp call @@ -392,17 +391,17 @@ class Domain(TimeStampedModel, DomainHelper): remRequest.add_extension(remExtension) try: if ( - "dsData" in _addDnssecdata and - _addDnssecdata["dsData"] is not None - or "keyData" in _addDnssecdata and - _addDnssecdata["keyData"] is not None + "dsData" in _addDnssecdata + and _addDnssecdata["dsData"] is not None + or "keyData" in _addDnssecdata + and _addDnssecdata["keyData"] is not None ): registry.send(addRequest, cleaned=True) if ( - "dsData" in _remDnssecdata and - _remDnssecdata["dsData"] is not None - or "keyData" in _remDnssecdata and - _remDnssecdata["keyData"] is not None + "dsData" in _remDnssecdata + and _remDnssecdata["dsData"] is not None + or "keyData" in _remDnssecdata + and _remDnssecdata["keyData"] is not None ): registry.send(remRequest, cleaned=True) except RegistryError as e: diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index d28171155..f27713454 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -7,7 +7,7 @@ import random from string import ascii_uppercase from django.test import TestCase from unittest.mock import MagicMock, Mock, patch -from typing import List, Dict, Mapping, Any +from typing import List, Dict from django.conf import settings from django.contrib.auth import get_user_model, login @@ -704,18 +704,26 @@ class MockEppLib(TestCase): "alg": 1, "pubKey": "AQPJ////4Q==", } - dnssecExtensionWithDsData = extensions.DNSSECExtension(**{ - "dsData": [common.DSData(**addDsData1)], - }) - dnssecExtensionWithMultDsData = extensions.DNSSECExtension(**{ - "dsData": [ - common.DSData(**addDsData1), # type: ignore - common.DSData(**addDsData2), # type: ignore - ], - }) - dnssecExtensionWithKeyData = extensions.DNSSECExtension(**{ - "keyData": [common.DNSSECKeyData(**keyDataDict)], # type: ignore - }) + dnssecExtensionWithDsData = extensions.DNSSECExtension( + **{ + "dsData": [ + common.DSData(**addDsData1) # type: ignore + ], # type: ignore + } + ) + dnssecExtensionWithMultDsData = extensions.DNSSECExtension( + **{ + "dsData": [ + common.DSData(**addDsData1), # type: ignore + common.DSData(**addDsData2), # type: ignore + ], # type: ignore + } + ) + dnssecExtensionWithKeyData = extensions.DNSSECExtension( + **{ + "keyData": [common.DNSSECKeyData(**keyDataDict)], # type: ignore + } + ) dnssecExtensionRemovingDsData = extensions.DNSSECExtension() def mockSend(self, _request, cleaned): @@ -756,35 +764,20 @@ class MockEppLib(TestCase): if getattr(_request, "name", None) == "security.gov": return MagicMock(res_data=[self.infoDomainNoContact]) elif getattr(_request, "name", None) == "dnssec-dsdata.gov": - if self.mockedSendFunction.call_count == 1: - return MagicMock(res_data=[self.mockDataInfoDomain]) - else: - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[ - self.dnssecExtensionWithDsData - ], - ) + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[self.dnssecExtensionWithDsData], + ) elif getattr(_request, "name", None) == "dnssec-multdsdata.gov": - if self.mockedSendFunction.call_count == 1: - return MagicMock(res_data=[self.mockDataInfoDomain]) - else: - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[ - self.dnssecExtensionWithMultDsData - ], - ) + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[self.dnssecExtensionWithMultDsData], + ) elif getattr(_request, "name", None) == "dnssec-keydata.gov": - if self.mockedSendFunction.call_count == 1: - return MagicMock(res_data=[self.mockDataInfoDomain]) - else: - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[ - self.dnssecExtensionWithKeyData - ], - ) + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[self.dnssecExtensionWithKeyData], + ) elif getattr(_request, "name", None) == "dnssec-none.gov": # this case is not necessary, but helps improve readability return MagicMock(res_data=[self.mockDataInfoDomain]) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 05f56e0f4..cf2d002f5 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1034,23 +1034,40 @@ class TestRegistrantDNSSEC(MockEppLib): """ + # need to use a separate patcher and side_effect for this test, as + # response from InfoDomain must be different for different iterations + # of the same command + def side_effect(_request, cleaned): + if isinstance(_request, commands.InfoDomain): + if mocked_send.call_count == 1: + return MagicMock(res_data=[self.mockDataInfoDomain]) + else: + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[self.dnssecExtensionWithDsData], + ) + else: + return MagicMock(res_data=[self.mockDataInfoHosts]) + + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") domain.dnssecdata = self.dnssecExtensionWithDsData - + # get the DNS SEC extension added to the UpdateDomain command and # verify that it is properly sent # args[0] is the _request sent to registry - args, _ = self.mockedSendFunction.call_args + args, _ = mocked_send.call_args # assert that the extension on the update matches self.assertEquals( args[0].extensions[0], - self.createUpdateExtension( - self.dnssecExtensionWithDsData - ), + self.createUpdateExtension(self.dnssecExtensionWithDsData), ) # test that the dnssecdata getter is functioning properly dnssecdata_get = domain.dnssecdata - self.mockedSendFunction.assert_has_calls( + mocked_send.assert_has_calls( [ call( commands.InfoDomain( @@ -1077,9 +1094,9 @@ class TestRegistrantDNSSEC(MockEppLib): ] ) - self.assertEquals( - dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData - ) + self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData) + + patcher.stop() def test_dnssec_is_idempotent(self): """ @@ -1095,9 +1112,28 @@ class TestRegistrantDNSSEC(MockEppLib): 3 - setter causes the getter to call info domain on next get from cache 4 - UpdateDomain command is not called on second setter (no change) 5 - getter properly parses dnssecdata from InfoDomain response and sets to cache - + """ + # need to use a separate patcher and side_effect for this test, as + # response from InfoDomain must be different for different iterations + # of the same command + def side_effect(_request, cleaned): + if isinstance(_request, commands.InfoDomain): + if mocked_send.call_count == 1: + return MagicMock(res_data=[self.mockDataInfoDomain]) + else: + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[self.dnssecExtensionWithDsData], + ) + else: + return MagicMock(res_data=[self.mockDataInfoHosts]) + + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") # set the dnssecdata once @@ -1106,7 +1142,7 @@ class TestRegistrantDNSSEC(MockEppLib): domain.dnssecdata = self.dnssecExtensionWithDsData # test that the dnssecdata getter is functioning properly dnssecdata_get = domain.dnssecdata - self.mockedSendFunction.assert_has_calls( + mocked_send.assert_has_calls( [ call( commands.InfoDomain( @@ -1139,9 +1175,9 @@ class TestRegistrantDNSSEC(MockEppLib): ] ) - self.assertEquals( - dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData - ) + self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData) + + patcher.stop() def test_user_adds_dnssec_data_multiple_dsdata(self): """ @@ -1156,23 +1192,40 @@ class TestRegistrantDNSSEC(MockEppLib): """ + # need to use a separate patcher and side_effect for this test, as + # response from InfoDomain must be different for different iterations + # of the same command + def side_effect(_request, cleaned): + if isinstance(_request, commands.InfoDomain): + if mocked_send.call_count == 1: + return MagicMock(res_data=[self.mockDataInfoDomain]) + else: + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[self.dnssecExtensionWithMultDsData], + ) + else: + return MagicMock(res_data=[self.mockDataInfoHosts]) + + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + domain, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") domain.dnssecdata = self.dnssecExtensionWithMultDsData # get the DNS SEC extension added to the UpdateDomain command # and verify that it is properly sent # args[0] is the _request sent to registry - args, _ = self.mockedSendFunction.call_args + args, _ = mocked_send.call_args # assert that the extension matches self.assertEquals( args[0].extensions[0], - self.createUpdateExtension( - self.dnssecExtensionWithMultDsData - ), + self.createUpdateExtension(self.dnssecExtensionWithMultDsData), ) # test that the dnssecdata getter is functioning properly dnssecdata_get = domain.dnssecdata - self.mockedSendFunction.assert_has_calls( + mocked_send.assert_has_calls( [ call( commands.UpdateDomain( @@ -1197,6 +1250,8 @@ class TestRegistrantDNSSEC(MockEppLib): dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData.dsData ) + patcher.stop() + def test_user_removes_dnssec_data(self): """ Scenario: Registrant removes DNSSEC ds data. @@ -1211,6 +1266,25 @@ class TestRegistrantDNSSEC(MockEppLib): """ + # need to use a separate patcher and side_effect for this test, as + # response from InfoDomain must be different for different iterations + # of the same command + def side_effect(_request, cleaned): + if isinstance(_request, commands.InfoDomain): + if mocked_send.call_count == 1: + return MagicMock(res_data=[self.mockDataInfoDomain]) + else: + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[self.dnssecExtensionWithDsData], + ) + else: + return MagicMock(res_data=[self.mockDataInfoHosts]) + + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") # dnssecdata_get_initial = domain.dnssecdata # call to force initial mock # domain._invalidate_cache() @@ -1219,7 +1293,7 @@ class TestRegistrantDNSSEC(MockEppLib): # get the DNS SEC extension added to the UpdateDomain command and # verify that it is properly sent # args[0] is the _request sent to registry - args, _ = self.mockedSendFunction.call_args + args, _ = mocked_send.call_args # assert that the extension on the update matches self.assertEquals( args[0].extensions[0], @@ -1228,7 +1302,7 @@ class TestRegistrantDNSSEC(MockEppLib): remove=True, ), ) - self.mockedSendFunction.assert_has_calls( + mocked_send.assert_has_calls( [ call( commands.InfoDomain( @@ -1265,6 +1339,8 @@ class TestRegistrantDNSSEC(MockEppLib): ] ) + patcher.stop() + def test_user_adds_dnssec_keydata(self): """ Scenario: Registrant adds DNSSEC key data. @@ -1278,23 +1354,40 @@ class TestRegistrantDNSSEC(MockEppLib): """ + # need to use a separate patcher and side_effect for this test, as + # response from InfoDomain must be different for different iterations + # of the same command + def side_effect(_request, cleaned): + if isinstance(_request, commands.InfoDomain): + if mocked_send.call_count == 1: + return MagicMock(res_data=[self.mockDataInfoDomain]) + else: + return MagicMock( + res_data=[self.mockDataInfoDomain], + extensions=[self.dnssecExtensionWithKeyData], + ) + else: + return MagicMock(res_data=[self.mockDataInfoHosts]) + + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + domain, _ = Domain.objects.get_or_create(name="dnssec-keydata.gov") domain.dnssecdata = self.dnssecExtensionWithKeyData # get the DNS SEC extension added to the UpdateDomain command # and verify that it is properly sent # args[0] is the _request sent to registry - args, _ = self.mockedSendFunction.call_args + args, _ = mocked_send.call_args # assert that the extension matches self.assertEquals( args[0].extensions[0], - self.createUpdateExtension( - self.dnssecExtensionWithKeyData - ), + self.createUpdateExtension(self.dnssecExtensionWithKeyData), ) # test that the dnssecdata getter is functioning properly dnssecdata_get = domain.dnssecdata - self.mockedSendFunction.assert_has_calls( + mocked_send.assert_has_calls( [ call( commands.UpdateDomain( @@ -1319,6 +1412,8 @@ class TestRegistrantDNSSEC(MockEppLib): dnssecdata_get.keyData, self.dnssecExtensionWithKeyData.keyData ) + patcher.stop() + def test_update_is_unsuccessful(self): """ Scenario: An update to the dns data is unsuccessful diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 6cc89682e..2781be038 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -497,7 +497,7 @@ class DomainKeydataView(DomainPermissionView, FormMixin): } if dnssecdata.keyData is None: dnssecdata.keyData = [] - dnssecdata["keyData"].append(common.DNSSECKeyData(**keyrecord)) + dnssecdata.keyData.append(common.DNSSECKeyData(**keyrecord)) except KeyError: # no server information in this field, skip it pass From 4dc94a5410ac4946fce57e3ef6f76644a64aa111 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 10 Oct 2023 11:47:07 -0400 Subject: [PATCH 55/65] copy changes --- src/registrar/templates/domain_dns.html | 2 +- src/registrar/templates/domain_dnssec.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/templates/domain_dns.html b/src/registrar/templates/domain_dns.html index 0133a8b07..b16c1cb8b 100644 --- a/src/registrar/templates/domain_dns.html +++ b/src/registrar/templates/domain_dns.html @@ -9,7 +9,7 @@

                  The Domain Name System (DNS) is the internet service that translates your domain name into an IP address. Before your .gov domain can be used, you'll need to connect it to your DNS hosting service and provide us with your name server information.

                  -

                  You can enter your name services, as well as other DNS-related information, in the following sections:

                  +

                  You can enter your name servers, as well as other DNS-related information, in the following sections:

                  {% url 'domain-dns-nameservers' pk=domain.id as url %}

                  DNS name servers

                  diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index 6a0a15389..91d8bb7b8 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -7,7 +7,7 @@

                  {% if dnssec_enabled %}Set up {% endif %}DNSSEC

                  -

                  DNSSEC, or DNS Security Extensions, is additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your website, they can be certain that it's connecting to the correct server, preventing potential hijacking or tampering with your domain's records.

                  +

                  DNSSEC, or DNS Security Extensions, is additional security layer to protect your domain. Enabling DNSSEC ensures that when someone visits your domain, they can be certain that it's connecting to the correct server, preventing potential hijacking or tampering with your domain's records.

                  {% csrf_token %} @@ -43,7 +43,7 @@
                  - It is strongly recommended that you do not enable this unless you fully understand DNSSEC and know how to set it up properly. If you make a mistake, it could cause your domain name to stop working. + It is strongly recommended that you only enable DNSSEC if you know how to set it up properly at your hosting service. If you make a mistake, it could cause your domain name to stop working.
                  -
                  \ No newline at end of file + diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 9334700ef..c0e121fee 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -241,7 +241,7 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): has_dnssec_records = self.domain.dnssecdata is not None - # Create HTML for the buttons + # Create HTML for the modal button modal_button = '' context['modal_button'] = modal_button From 4c2eaac59f9d8800fe21146b100f8c262211203f Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 10 Oct 2023 20:02:02 -0400 Subject: [PATCH 57/65] comments clean up --- src/registrar/views/domain.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 2cf5ca3c2..24a704d94 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -297,6 +297,8 @@ class DomainDsdataView(DomainPermissionView, FormMixin): if dnssecdata is not None: if dnssecdata.keyData is not None: # TODO: Throw an error + # Note: This is moot if we're + # removing key data pass if dnssecdata.dsData is not None: @@ -311,7 +313,7 @@ class DomainDsdataView(DomainPermissionView, FormMixin): for record in dnssecdata.dsData ) - # Ensure at least 3 fields, filled or empty + # Ensure at least 1 record, filled or empty while len(initial_data) == 0: initial_data.append({}) @@ -385,7 +387,6 @@ class DomainDsdataView(DomainPermissionView, FormMixin): try: domain.dnssecdata = dnssecdata except RegistryError as err: - # Alysia: Check client hold error handling and duplicate this here errmsg = "Error updating DNSSEC data in the registry." logger.error(errmsg) logger.error(err) @@ -415,7 +416,9 @@ class DomainKeydataView(DomainPermissionView, FormMixin): if dnssecdata is not None: if dnssecdata.dsData is not None: - # TODO: Throw an error + # TODO: Throw an error? + # Note: this is moot if we're + # removing Key data pass if dnssecdata.keyData is not None: @@ -430,7 +433,7 @@ class DomainKeydataView(DomainPermissionView, FormMixin): for record in dnssecdata.keyData ) - # Ensure at least 3 fields, filled or empty + # Ensure at least 1 record, filled or empty while len(initial_data) == 0: initial_data.append({}) From bddf791489c1e82f2266b5e66568d12ac4439982 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 11 Oct 2023 18:42:13 -0400 Subject: [PATCH 58/65] some content updates --- src/registrar/templates/domain_dnssec.html | 2 +- src/registrar/templates/domain_dsdata.html | 17 +++++++++++------ src/registrar/templates/domain_sidebar.html | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index 91d8bb7b8..9913b041c 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -5,7 +5,7 @@ {% block domain_content %} -

                  {% if dnssec_enabled %}Set up {% endif %}DNSSEC

                  +

                  DNSSEC

                  DNSSEC, or DNS Security Extensions, is additional security layer to protect your domain. Enabling DNSSEC ensures that when someone visits your domain, they can be certain that it's connecting to the correct server, preventing potential hijacking or tampering with your domain's records.

                  diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 2ad162360..eb0e73e49 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -20,14 +20,19 @@ {% elif not dnssec_ds_confirmed %} -

                  In order to enable DNSSEC and add DS records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.

                  +

                  In order to enable DNSSEC, you must first configure it with your DNS hosting service.

                  +

                  Enter the values given by your DNS provider for DS Data.

                  +

                  Required fields are marked with an asterisk (*).

                  {% csrf_token %} - + {% else %} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index aff9f5dab..1acd87eeb 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -23,7 +23,7 @@ - DNS name servers + Name servers
                • From c7c50fb3d5b19a1d836368e483900e15da225f84 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 11 Oct 2023 18:48:24 -0400 Subject: [PATCH 59/65] minor js update --- src/registrar/assets/js/get-gov.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 4b5ff5e31..f5dffba12 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -276,8 +276,7 @@ function prepareDeleteButtons() { let formToRemove = e.target.closest(".ds-record"); formToRemove.remove(); let forms = document.querySelectorAll(".ds-record"); - let formNum2 = forms.length; - totalForms.setAttribute('value', `${formNum2}`); + totalForms.setAttribute('value', `${forms.length}`); let formNumberRegex = RegExp(`form-(\\d){1}-`, 'g'); let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g'); From bac08277b2fe2e8bd811c0857c3bdca58d052131 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 11 Oct 2023 19:04:10 -0400 Subject: [PATCH 60/65] updated copy and fixed failing test --- src/registrar/templates/domain_dsdata.html | 8 ++++++++ src/registrar/tests/test_views.py | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index eb0e73e49..c068a3752 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -8,6 +8,14 @@ {% include "includes/form_errors.html" with form=form %} {% endfor %} + {% if domain.dnssecdata is None and not dnssec_ds_confirmed %} +
                  +
                  + You have no DS Data added. Enable DNSSEC by adding DS Data or return to the DNSSEC page and click 'enable.' +
                  +
                  + {% endif %} +

                  DS Data

                  {% if domain.dnssecdata is not None and domain.dnssecdata.keyData is not None %} diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index af44cdeaa..06fddfde7 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1618,14 +1618,15 @@ class TestDomainDNSSEC(TestDomainOverview): def test_ds_form_loads_with_no_domain_data(self): """DNSSEC Add DS Data page loads when there is no - domain DNSSEC data and shows a button to Add DS Data record""" + domain DNSSEC data and shows a button to Add new record""" page = self.client.get( reverse( "domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dnssec_none.id} ) ) - self.assertContains(page, "Add DS Data record") + self.assertContains(page, "You have no DS Data added") + self.assertContains(page, "Add new record") def test_ds_form_loads_with_ds_data(self): """DNSSEC Add DS Data page loads when there is From 46ad6002cd0213fd0b46d1efe86ab1f5be2bf5c1 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 13 Oct 2023 12:07:35 -0400 Subject: [PATCH 61/65] CSS tweaks --- src/registrar/assets/sass/_theme/_forms.scss | 4 ++++ src/registrar/forms/domain.py | 2 +- src/registrar/templates/domain_dnssec.html | 8 ++++---- src/registrar/templates/domain_dsdata.html | 2 +- src/registrar/templates/domain_keydata.html | 2 +- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index cdb2889c5..ed118bb94 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -8,6 +8,10 @@ max-width: none; } +.usa-form--text-width { + max-width: measure(5); +} + .usa-textarea { @include at-media('tablet') { height: units('mobile'); diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 73a40bfa1..b28c9f6a4 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -176,7 +176,7 @@ class DomainDsdataForm(forms.Form): digest_type = forms.TypedChoiceField( required=True, - label="Digest Type", + label="Digest type", coerce=int, # need to coerce into int so dsData objects can be compared choices=[(None, "--Select--")] + DIGEST_TYPE_CHOICES, # type: ignore error_messages={"required": ("Digest Type is required.")}, diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index 9913b041c..4fa43bb4c 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -9,10 +9,10 @@

                  DNSSEC, or DNS Security Extensions, is additional security layer to protect your domain. Enabling DNSSEC ensures that when someone visits your domain, they can be certain that it's connecting to the correct server, preventing potential hijacking or tampering with your domain's records.

                  -
                  + {% csrf_token %} {% if has_dnssec_records %} -
                  +
                  In order to fully disable DNSSEC on your domain, you will need to work with your DNS provider to remove your DNSSEC-related records from your zone.
                  @@ -33,7 +33,7 @@ Add Key Data @@ -41,7 +41,7 @@
                  {% else %}
                  -
                  +
                  It is strongly recommended that you only enable DNSSEC if you know how to set it up properly at your hosting service. If you make a mistake, it could cause your domain name to stop working.
                  diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index c068a3752..ca4dce783 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -97,7 +97,7 @@ {% endfor %} - From c6abca99cad675ff8616e18e4f9dfba29cbfca80 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 13 Oct 2023 15:32:30 -0400 Subject: [PATCH 63/65] updates to tests --- src/registrar/tests/common.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index d1021f45c..5df54de76 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -791,6 +791,8 @@ class MockEppLib(TestCase): return self.mockInfoDomainCommands(_request, cleaned) elif isinstance(_request, commands.InfoContact): return self.mockInfoContactCommands(_request, cleaned) + elif isinstance(_request, commands.UpdateDomain): + return self.mockUpdateDomainCommands(_request, cleaned) elif ( isinstance(_request, commands.CreateContact) and getattr(_request, "id", None) == "fail" @@ -809,16 +811,6 @@ class MockEppLib(TestCase): res_data=[self.mockDataHostChange], code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, ) - elif ( - isinstance(_request, commands.UpdateDomain) - and getattr(_request, "name", None) == "dnssec-invalid.gov" - ): - raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR) - elif isinstance(_request, commands.UpdateDomain): - return MagicMock( - res_data=[self.mockDataHostChange], - code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, - ) elif isinstance(_request, commands.DeleteHost): return MagicMock( res_data=[self.mockDataHostChange], @@ -836,6 +828,15 @@ class MockEppLib(TestCase): ) return MagicMock(res_data=[self.mockDataInfoHosts]) + def mockUpdateDomainCommands(self, _request, cleaned): + if getattr(_request, "name", None) == "dnssec-invalid.gov": + raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR) + else: + return MagicMock( + res_data=[self.mockDataHostChange], + code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, + ) + def mockInfoDomainCommands(self, _request, cleaned): if getattr(_request, "name", None) == "security.gov": return MagicMock(res_data=[self.infoDomainNoContact]) From f94363a836a59b34291261f21cdb3296593b9adf Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 13 Oct 2023 15:43:24 -0400 Subject: [PATCH 64/65] refactor epp mockInfoDomainCommands function --- src/registrar/tests/common.py | 74 ++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 5df54de76..7ae107006 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -838,41 +838,45 @@ class MockEppLib(TestCase): ) def mockInfoDomainCommands(self, _request, cleaned): - if getattr(_request, "name", None) == "security.gov": - return MagicMock(res_data=[self.infoDomainNoContact]) - elif getattr(_request, "name", None) == "dnssec-dsdata.gov": - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[self.dnssecExtensionWithDsData], - ) - elif getattr(_request, "name", None) == "dnssec-multdsdata.gov": - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[self.dnssecExtensionWithMultDsData], - ) - elif getattr(_request, "name", None) == "dnssec-keydata.gov": - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[self.dnssecExtensionWithKeyData], - ) - elif getattr(_request, "name", None) == "dnssec-none.gov": - # this case is not necessary, but helps improve readability - return MagicMock(res_data=[self.mockDataInfoDomain]) - elif getattr(_request, "name", None) == "my-nameserver.gov": - if self.mockedSendFunction.call_count == 5: - return MagicMock(res_data=[self.infoDomainTwoHosts]) - else: - return MagicMock(res_data=[self.infoDomainNoHost]) - elif getattr(_request, "name", None) == "nameserverwithip.gov": - return MagicMock(res_data=[self.infoDomainHasIP]) - elif getattr(_request, "name", None) == "namerserversubdomain.gov": - return MagicMock(res_data=[self.infoDomainCheckHostIPCombo]) - elif getattr(_request, "name", None) == "freeman.gov": - return MagicMock(res_data=[self.InfoDomainWithContacts]) - elif getattr(_request, "name", None) == "threenameserversDomain.gov": - return MagicMock(res_data=[self.infoDomainThreeHosts]) - else: - return MagicMock(res_data=[self.mockDataInfoDomain]) + request_name = getattr(_request, "name", None) + + # Define a dictionary to map request names to data and extension values + request_mappings = { + "security.gov": (self.infoDomainNoContact, None), + "dnssec-dsdata.gov": ( + self.mockDataInfoDomain, + self.dnssecExtensionWithDsData, + ), + "dnssec-multdsdata.gov": ( + self.mockDataInfoDomain, + self.dnssecExtensionWithMultDsData, + ), + "dnssec-keydata.gov": ( + self.mockDataInfoDomain, + self.dnssecExtensionWithKeyData, + ), + "dnssec-none.gov": (self.mockDataInfoDomain, None), + "my-nameserver.gov": ( + self.infoDomainTwoHosts + if self.mockedSendFunction.call_count == 5 + else self.infoDomainNoHost, + None, + ), + "nameserverwithip.gov": (self.infoDomainHasIP, None), + "namerserversubdomain.gov": (self.infoDomainCheckHostIPCombo, None), + "freeman.gov": (self.InfoDomainWithContacts, None), + "threenameserversDomain.gov": (self.infoDomainThreeHosts, None), + } + + # Retrieve the corresponding values from the dictionary + res_data, extensions = request_mappings.get( + request_name, (self.mockDataInfoDomain, None) + ) + + return MagicMock( + res_data=[res_data], + extensions=[extensions] if extensions is not None else [], + ) def mockInfoContactCommands(self, _request, cleaned): mocked_result: info.InfoContactResultData From 181cfccfc5463ef1984563b4d4211107d13b3a32 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 17 Oct 2023 13:20:48 -0400 Subject: [PATCH 65/65] added comments, removed empty lines, and renamed methods for code legibility --- src/registrar/assets/js/get-gov.js | 8 ++++++++ src/registrar/config/urls.py | 4 ++-- src/registrar/forms/domain.py | 8 -------- src/registrar/models/domain.py | 12 +++++++++++- src/registrar/views/__init__.py | 4 ++-- src/registrar/views/domain.py | 22 +++++++--------------- 6 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index f5dffba12..c21060382 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -315,9 +315,17 @@ function prepareDeleteButtons() { // Attach click event listener on the delete buttons of the existing forms prepareDeleteButtons(); + // Attack click event listener on the add button if (addButton) addButton.addEventListener('click', addForm); + /* + * Add a formset to the end of the form. + * For each element in the added formset, name the elements with the prefix, + * form-{#}-{element_name} where # is the index of the formset and element_name + * is the element's name. + * Additionally, update the form element's metadata, including totalForms' value. + */ function addForm(e){ let forms = document.querySelectorAll(".ds-record"); let formNum = forms.length; diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 7caa64e5c..bd2215620 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -97,12 +97,12 @@ urlpatterns = [ ), path( "domain//dns/dnssec/dsdata", - views.DomainDsdataView.as_view(), + views.DomainDsDataView.as_view(), name="domain-dns-dnssec-dsdata", ), path( "domain//dns/dnssec/keydata", - views.DomainKeydataView.as_view(), + views.DomainKeyDataView.as_view(), name="domain-dns-dnssec-keydata", ), path( diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 3878c1f43..8abc7e14a 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -16,14 +16,12 @@ from .common import ( class DomainAddUserForm(forms.Form): - """Form for adding a user to a domain.""" email = forms.EmailField(label="Email") class DomainNameserverForm(forms.Form): - """Form for changing nameservers.""" server = forms.CharField(label="Name server", strip=True) @@ -37,7 +35,6 @@ NameserverFormset = formset_factory( class ContactForm(forms.ModelForm): - """Form for updating contacts.""" class Meta: @@ -68,14 +65,12 @@ class ContactForm(forms.ModelForm): class DomainSecurityEmailForm(forms.Form): - """Form for adding or editing a security email to a domain.""" security_email = forms.EmailField(label="Security email", required=False) class DomainOrgNameAddressForm(forms.ModelForm): - """Form for updating the organization name and mailing address.""" zipcode = forms.CharField( @@ -149,12 +144,10 @@ class DomainOrgNameAddressForm(forms.ModelForm): class DomainDnssecForm(forms.Form): - """Form for enabling and disabling dnssec""" class DomainDsdataForm(forms.Form): - """Form for adding or editing DNSSEC DS Data to a domain.""" key_tag = forms.IntegerField( @@ -198,7 +191,6 @@ DomainDsdataFormset = formset_factory( class DomainKeydataForm(forms.Form): - """Form for adding or editing DNSSEC Key Data to a domain.""" flag = forms.TypedChoiceField( diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index c5ba4356b..0b107907e 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -458,11 +458,21 @@ class Domain(TimeStampedModel, DomainHelper): @Cache def dnssecdata(self) -> Optional[extensions.DNSSECExtension]: + """ + Get a complete list of dnssecdata extensions for this domain. + + dnssecdata are provided as a list of DNSSECExtension objects. + + A DNSSECExtension object includes: + maxSigLife: Optional[int] + dsData: Optional[Sequence[DSData]] + keyData: Optional[Sequence[DNSSECKeyData]] + + """ try: return self._get_property("dnssecdata") except Exception as err: # Don't throw error as this is normal for a new domain - # TODO - 433 error handling ticket should address this logger.info("Domain does not have dnssec data defined %s" % err) return None diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index 5bca4e1d5..5fd81df8c 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -6,8 +6,8 @@ from .domain import ( DomainDNSView, DomainNameserversView, DomainDNSSECView, - DomainDsdataView, - DomainKeydataView, + DomainDsDataView, + DomainKeyDataView, DomainYourContactInformationView, DomainSecurityEmailView, DomainUsersView, diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 68d193e86..36b7a9445 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -51,7 +51,6 @@ logger = logging.getLogger(__name__) class DomainView(DomainPermissionView): - """Domain detail overview page.""" template_name = "domain_detail.html" @@ -113,7 +112,6 @@ class DomainOrgNameAddressView(DomainPermissionView, FormMixin): class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin): - """Domain authorizing official editing view.""" model = Domain @@ -156,14 +154,12 @@ class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin): class DomainDNSView(DomainPermissionView): - """DNS Information View.""" template_name = "domain_dns.html" class DomainNameserversView(DomainPermissionView, FormMixin): - """Domain nameserver editing view.""" template_name = "domain_nameservers.html" @@ -242,15 +238,15 @@ class DomainNameserversView(DomainPermissionView, FormMixin): class DomainDNSSECView(DomainPermissionView, FormMixin): - """Domain DNSSEC editing view.""" template_name = "domain_dnssec.html" form_class = DomainDnssecForm def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) """The initial value for the form (which is a formset here).""" + context = super().get_context_data(**kwargs) + self.domain = self.get_object() has_dnssec_records = self.domain.dnssecdata is not None @@ -294,8 +290,7 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): return self.form_valid(form) -class DomainDsdataView(DomainPermissionView, FormMixin): - +class DomainDsDataView(DomainPermissionView, FormMixin): """Domain DNSSEC ds data editing view.""" template_name = "domain_dsdata.html" @@ -395,7 +390,9 @@ class DomainDsdataView(DomainPermissionView, FormMixin): dnssecdata.dsData = [] dnssecdata.dsData.append(common.DSData(**dsrecord)) except KeyError: - # no server information in this field, skip it + # no cleaned_data provided for this form, but passed + # as valid; this can happen if form has been added but + # not been interacted with; in that case, want to ignore pass domain = self.get_object() try: @@ -414,8 +411,7 @@ class DomainDsdataView(DomainPermissionView, FormMixin): return super().form_valid(formset) -class DomainKeydataView(DomainPermissionView, FormMixin): - +class DomainKeyDataView(DomainPermissionView, FormMixin): """Domain DNSSEC key data editing view.""" template_name = "domain_keydata.html" @@ -536,7 +532,6 @@ class DomainKeydataView(DomainPermissionView, FormMixin): class DomainYourContactInformationView(DomainPermissionView, FormMixin): - """Domain your contact information editing view.""" template_name = "domain_your_contact_information.html" @@ -577,7 +572,6 @@ class DomainYourContactInformationView(DomainPermissionView, FormMixin): class DomainSecurityEmailView(DomainPermissionView, FormMixin): - """Domain security email editing view.""" template_name = "domain_security_email.html" @@ -639,14 +633,12 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin): class DomainUsersView(DomainPermissionView): - """User management page in the domain details.""" template_name = "domain_users.html" class DomainAddUserView(DomainPermissionView, FormMixin): - """Inside of a domain's user management, a form for adding users. Multiple inheritance is used here for permissions, form handling, and