mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-21 18:25:58 +02:00
Merge pull request #1337 from cisagov/dk/1208-dnssec-addtl-items
Issue #1208 - DNSSEC additional items
This commit is contained in:
commit
58d97cd899
6 changed files with 175 additions and 24 deletions
|
@ -8,6 +8,10 @@ from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
|||
from registrar.utility.errors import (
|
||||
NameserverError,
|
||||
NameserverErrorCodes as nsErrorCodes,
|
||||
DsDataError,
|
||||
DsDataErrorCodes,
|
||||
SecurityEmailError,
|
||||
SecurityEmailErrorCodes,
|
||||
)
|
||||
|
||||
from ..models import Contact, DomainInformation, Domain
|
||||
|
@ -16,6 +20,8 @@ from .common import (
|
|||
DIGEST_TYPE_CHOICES,
|
||||
)
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class DomainAddUserForm(forms.Form):
|
||||
"""Form for adding a user to a domain."""
|
||||
|
@ -152,7 +158,13 @@ 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)
|
||||
security_email = forms.EmailField(
|
||||
label="Security email",
|
||||
required=False,
|
||||
error_messages={
|
||||
"invalid": str(SecurityEmailError(code=SecurityEmailErrorCodes.BAD_DATA)),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class DomainOrgNameAddressForm(forms.ModelForm):
|
||||
|
@ -228,12 +240,22 @@ class DomainDnssecForm(forms.Form):
|
|||
class DomainDsdataForm(forms.Form):
|
||||
"""Form for adding or editing DNSSEC DS Data to a domain."""
|
||||
|
||||
def validate_hexadecimal(value):
|
||||
"""
|
||||
Tests that string matches all hexadecimal values.
|
||||
|
||||
Raise validation error to display error in form
|
||||
if invalid characters entered
|
||||
"""
|
||||
if not re.match(r"^[0-9a-fA-F]+$", value):
|
||||
raise forms.ValidationError(str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_CHARS)))
|
||||
|
||||
key_tag = forms.IntegerField(
|
||||
required=True,
|
||||
label="Key tag",
|
||||
validators=[
|
||||
MinValueValidator(0, message="Value must be between 0 and 65535"),
|
||||
MaxValueValidator(65535, message="Value must be between 0 and 65535"),
|
||||
MinValueValidator(0, message=str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE))),
|
||||
MaxValueValidator(65535, message=str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE))),
|
||||
],
|
||||
error_messages={"required": ("Key tag is required.")},
|
||||
)
|
||||
|
@ -251,15 +273,38 @@ class DomainDsdataForm(forms.Form):
|
|||
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.")},
|
||||
error_messages={"required": ("Digest type is required.")},
|
||||
)
|
||||
|
||||
digest = forms.CharField(
|
||||
required=True,
|
||||
label="Digest",
|
||||
error_messages={"required": ("Digest is required.")},
|
||||
validators=[validate_hexadecimal],
|
||||
error_messages={
|
||||
"required": "Digest is required.",
|
||||
},
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
# clean is called from clean_forms, which is called from is_valid
|
||||
# after clean_fields. it is used to determine form level errors.
|
||||
# is_valid is typically called from view during a post
|
||||
cleaned_data = super().clean()
|
||||
digest_type = cleaned_data.get("digest_type", 0)
|
||||
digest = cleaned_data.get("digest", "")
|
||||
# validate length of digest depending on digest_type
|
||||
if digest_type == 1 and len(digest) != 40:
|
||||
self.add_error(
|
||||
"digest",
|
||||
DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA1),
|
||||
)
|
||||
elif digest_type == 2 and len(digest) != 64:
|
||||
self.add_error(
|
||||
"digest",
|
||||
DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA256),
|
||||
)
|
||||
return cleaned_data
|
||||
|
||||
|
||||
DomainDsdataFormset = formset_factory(
|
||||
DomainDsdataForm,
|
||||
|
|
|
@ -598,7 +598,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
|
||||
# if unable to update domain raise error and stop
|
||||
if responseCode != ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY:
|
||||
raise NameserverError(code=nsErrorCodes.UNABLE_TO_UPDATE_DOMAIN)
|
||||
raise NameserverError(code=nsErrorCodes.BAD_DATA)
|
||||
|
||||
successTotalNameservers = len(oldNameservers) - deleteCount + addToDomainCount
|
||||
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
aria-describedby="Your DNSSEC records will be deleted from the registry."
|
||||
data-force-action
|
||||
>
|
||||
{% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to delete all DS records on your domain" modal_description="To fully disable DNSSEC: In addition to deleting your DS records here you’ll also need to delete the DS records at your DNS host. To avoid causing your domain to appear offline you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button=modal_button|safe %}
|
||||
{% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to remove all DS records on your domain" modal_description="To fully disable DNSSEC: In addition to removing your DS records here you’ll also need to delete the DS records at your DNS host. To avoid causing your domain to appear offline you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button=modal_button|safe %}
|
||||
</div>
|
||||
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -17,6 +17,8 @@ from registrar.utility.errors import (
|
|||
SecurityEmailErrorCodes,
|
||||
GenericError,
|
||||
GenericErrorCodes,
|
||||
DsDataError,
|
||||
DsDataErrorCodes,
|
||||
)
|
||||
|
||||
from registrar.models import (
|
||||
|
@ -1878,7 +1880,30 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
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
|
||||
"""DS Data form errors with invalid data (missing required fields)
|
||||
|
||||
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)
|
||||
# all four form fields are required, so will test with each blank
|
||||
add_data_page.forms[0]["form-0-key_tag"] = ""
|
||||
add_data_page.forms[0]["form-0-algorithm"] = ""
|
||||
add_data_page.forms[0]["form-0-digest_type"] = ""
|
||||
add_data_page.forms[0]["form-0-digest"] = ""
|
||||
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)
|
||||
self.assertContains(result, "Algorithm is required", count=2, status_code=200)
|
||||
self.assertContains(result, "Digest type is required", count=2, status_code=200)
|
||||
self.assertContains(result, "Digest is required", count=2, status_code=200)
|
||||
|
||||
def test_ds_data_form_invalid_keytag(self):
|
||||
"""DS Data form errors with invalid data (key tag too large)
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
|
@ -1887,13 +1912,87 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
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"] = ""
|
||||
add_data_page.forms[0]["form-0-key_tag"] = "65536" # > 65535
|
||||
add_data_page.forms[0]["form-0-algorithm"] = ""
|
||||
add_data_page.forms[0]["form-0-digest_type"] = ""
|
||||
add_data_page.forms[0]["form-0-digest"] = ""
|
||||
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)
|
||||
self.assertContains(
|
||||
result, str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE)), count=2, status_code=200
|
||||
)
|
||||
|
||||
def test_ds_data_form_invalid_digest_chars(self):
|
||||
"""DS Data form errors with invalid data (digest contains non hexadecimal chars)
|
||||
|
||||
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"] = "1234"
|
||||
add_data_page.forms[0]["form-0-algorithm"] = "3"
|
||||
add_data_page.forms[0]["form-0-digest_type"] = "1"
|
||||
add_data_page.forms[0]["form-0-digest"] = "GG1234"
|
||||
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, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_CHARS)), count=2, status_code=200
|
||||
)
|
||||
|
||||
def test_ds_data_form_invalid_digest_sha1(self):
|
||||
"""DS Data form errors with invalid data (digest is invalid sha-1)
|
||||
|
||||
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"] = "1234"
|
||||
add_data_page.forms[0]["form-0-algorithm"] = "3"
|
||||
add_data_page.forms[0]["form-0-digest_type"] = "1" # SHA-1
|
||||
add_data_page.forms[0]["form-0-digest"] = "A123"
|
||||
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, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA1)), count=2, status_code=200
|
||||
)
|
||||
|
||||
def test_ds_data_form_invalid_digest_sha256(self):
|
||||
"""DS Data form errors with invalid data (digest is invalid sha-256)
|
||||
|
||||
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"] = "1234"
|
||||
add_data_page.forms[0]["form-0-algorithm"] = "3"
|
||||
add_data_page.forms[0]["form-0-digest_type"] = "2" # SHA-256
|
||||
add_data_page.forms[0]["form-0-digest"] = "GG1234"
|
||||
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, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA256)), count=2, status_code=200
|
||||
)
|
||||
|
||||
|
||||
class TestApplicationStatus(TestWithUser, WebTest):
|
||||
|
|
|
@ -66,20 +66,18 @@ class NameserverErrorCodes(IntEnum):
|
|||
value but is not a subdomain
|
||||
- 3 INVALID_IP invalid ip address format or invalid version
|
||||
- 4 TOO_MANY_HOSTS more than the max allowed host values
|
||||
- 5 UNABLE_TO_UPDATE_DOMAIN unable to update the domain
|
||||
- 6 MISSING_HOST host is missing for a nameserver
|
||||
- 7 INVALID_HOST host is invalid for a nameserver
|
||||
- 8 BAD_DATA bad data input for nameserver
|
||||
- 5 MISSING_HOST host is missing for a nameserver
|
||||
- 6 INVALID_HOST host is invalid for a nameserver
|
||||
- 7 BAD_DATA bad data input for nameserver
|
||||
"""
|
||||
|
||||
MISSING_IP = 1
|
||||
GLUE_RECORD_NOT_ALLOWED = 2
|
||||
INVALID_IP = 3
|
||||
TOO_MANY_HOSTS = 4
|
||||
UNABLE_TO_UPDATE_DOMAIN = 5
|
||||
MISSING_HOST = 6
|
||||
INVALID_HOST = 7
|
||||
BAD_DATA = 8
|
||||
MISSING_HOST = 5
|
||||
INVALID_HOST = 6
|
||||
BAD_DATA = 7
|
||||
|
||||
|
||||
class NameserverError(Exception):
|
||||
|
@ -93,9 +91,6 @@ class NameserverError(Exception):
|
|||
NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED: ("Name server address does not match domain name"),
|
||||
NameserverErrorCodes.INVALID_IP: ("{}: Enter an IP address in the required format."),
|
||||
NameserverErrorCodes.TOO_MANY_HOSTS: ("Too many hosts provided, you may not have more than 13 nameservers."),
|
||||
NameserverErrorCodes.UNABLE_TO_UPDATE_DOMAIN: (
|
||||
"Unable to update domain, changes were not applied. Check logs as a Registry Error is the likely cause"
|
||||
),
|
||||
NameserverErrorCodes.MISSING_HOST: ("Name server must be provided to enter IP address."),
|
||||
NameserverErrorCodes.INVALID_HOST: ("Enter a name server in the required format, like ns1.example.com"),
|
||||
NameserverErrorCodes.BAD_DATA: (
|
||||
|
@ -125,9 +120,17 @@ class DsDataErrorCodes(IntEnum):
|
|||
error mapping.
|
||||
Overview of ds data error codes:
|
||||
- 1 BAD_DATA bad data input in ds data
|
||||
- 2 INVALID_DIGEST_SHA1 invalid digest for digest type SHA-1
|
||||
- 3 INVALID_DIGEST_SHA256 invalid digest for digest type SHA-256
|
||||
- 4 INVALID_DIGEST_CHARS invalid chars in digest
|
||||
- 5 INVALID_KEYTAG_SIZE invalid key tag size > 65535
|
||||
"""
|
||||
|
||||
BAD_DATA = 1
|
||||
INVALID_DIGEST_SHA1 = 2
|
||||
INVALID_DIGEST_SHA256 = 3
|
||||
INVALID_DIGEST_CHARS = 4
|
||||
INVALID_KEYTAG_SIZE = 5
|
||||
|
||||
|
||||
class DsDataError(Exception):
|
||||
|
@ -139,7 +142,11 @@ class DsDataError(Exception):
|
|||
_error_mapping = {
|
||||
DsDataErrorCodes.BAD_DATA: (
|
||||
"There’s something wrong with the DS data you provided. If you need help email us at help@get.gov."
|
||||
)
|
||||
),
|
||||
DsDataErrorCodes.INVALID_DIGEST_SHA1: ("SHA-1 digest must be exactly 40 characters."),
|
||||
DsDataErrorCodes.INVALID_DIGEST_SHA256: ("SHA-256 digest must be exactly 64 characters."),
|
||||
DsDataErrorCodes.INVALID_DIGEST_CHARS: ("Digest must contain only alphanumeric characters [0-9,a-f]."),
|
||||
DsDataErrorCodes.INVALID_KEYTAG_SIZE: ("Key tag must be less than 65535"),
|
||||
}
|
||||
|
||||
def __init__(self, *args, code=None, **kwargs):
|
||||
|
|
|
@ -308,7 +308,7 @@ class DomainNameserversView(DomainFormBaseView):
|
|||
except NameserverError as Err:
|
||||
# NamserverErrors *should* be caught in form; if reached here,
|
||||
# there was an uncaught error in submission (through EPP)
|
||||
messages.error(self.request, NameserverError(code=nsErrorCodes.UNABLE_TO_UPDATE_DOMAIN))
|
||||
messages.error(self.request, NameserverError(code=nsErrorCodes.BAD_DATA))
|
||||
logger.error(f"Nameservers error: {Err}")
|
||||
# TODO: registry is not throwing an error when no connection
|
||||
except RegistryError as Err:
|
||||
|
@ -449,7 +449,7 @@ class DomainDsDataView(DomainFormBaseView):
|
|||
modal_button = (
|
||||
'<button type="submit" '
|
||||
'class="usa-button usa-button--secondary" '
|
||||
'name="disable-override-click">Delete all records</button>'
|
||||
'name="disable-override-click">Remove all DS Data</button>'
|
||||
)
|
||||
|
||||
# context to back out of a broken form on all fields delete
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue