digest validation; error messages; test cases

This commit is contained in:
David Kennedy 2023-11-15 06:54:35 -05:00
parent b22615e205
commit 48caa775dd
No known key found for this signature in database
GPG key ID: 6528A5386E66B96B
3 changed files with 155 additions and 65 deletions

View file

@ -8,6 +8,8 @@ from phonenumber_field.widgets import RegionalPhoneNumberWidget
from registrar.utility.errors import ( from registrar.utility.errors import (
NameserverError, NameserverError,
NameserverErrorCodes as nsErrorCodes, NameserverErrorCodes as nsErrorCodes,
DsDataError,
DsDataErrorCodes,
) )
from ..models import Contact, DomainInformation, Domain from ..models import Contact, DomainInformation, Domain
@ -232,14 +234,14 @@ class DomainDsdataForm(forms.Form):
def validate_hexadecimal(value): def validate_hexadecimal(value):
if not re.match(r'^[0-9a-fA-F]+$', value): if not re.match(r'^[0-9a-fA-F]+$', value):
raise forms.ValidationError('Digest must contain only alphanumeric characters [0-9,a-f].') raise forms.ValidationError(str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_CHARS)))
key_tag = forms.IntegerField( key_tag = forms.IntegerField(
required=True, required=True,
label="Key tag", label="Key tag",
validators=[ validators=[
MinValueValidator(0, message="Key tag must be less than 65535"), MinValueValidator(0, message=str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE))),
MaxValueValidator(65535, message="Key tag must be less than 65535"), MaxValueValidator(65535, message=str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE))),
], ],
error_messages={"required": ("Key tag is required.")}, error_messages={"required": ("Key tag is required.")},
) )
@ -257,7 +259,7 @@ class DomainDsdataForm(forms.Form):
label="Digest type", label="Digest type",
coerce=int, # need to coerce into int so dsData objects can be compared coerce=int, # need to coerce into int so dsData objects can be compared
choices=[(None, "--Select--")] + DIGEST_TYPE_CHOICES, # type: ignore 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( digest = forms.CharField(
@ -267,7 +269,7 @@ class DomainDsdataForm(forms.Form):
max_length=64, max_length=64,
error_messages={ error_messages={
"required": "Digest is required.", "required": "Digest is required.",
"max_length": "Digest must be at most 64 characters long.", "max_length": str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_LENGTH)),
}, },
) )
@ -282,64 +284,15 @@ class DomainDsdataForm(forms.Form):
if digest_type == 1 and len(digest) != 40: if digest_type == 1 and len(digest) != 40:
self.add_error( self.add_error(
"digest", "digest",
DsDa) DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA1),
# remove ANY spaces in the server field )
server = server.replace(" ", "") elif digest_type == 2 and len(digest) != 64:
# lowercase the server self.add_error(
server = server.lower() "digest",
cleaned_data["server"] = server DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA256),
ip = cleaned_data.get("ip", None) )
# remove ANY spaces in the ip field
ip = ip.replace(" ", "")
domain = cleaned_data.get("domain", "")
ip_list = self.extract_ip_list(ip)
# validate if the form has a server or an ip
if (ip and ip_list) or server:
self.validate_nameserver_ip_combo(domain, server, ip_list)
return cleaned_data return cleaned_data
def clean_empty_strings(self, cleaned_data):
ip = cleaned_data.get("ip", "")
if ip and len(ip.strip()) == 0:
cleaned_data["ip"] = None
def extract_ip_list(self, ip):
return [ip.strip() for ip in ip.split(",")] if ip else []
def validate_nameserver_ip_combo(self, domain, server, ip_list):
try:
Domain.checkHostIPCombo(domain, server, ip_list)
except NameserverError as e:
if e.code == nsErrorCodes.GLUE_RECORD_NOT_ALLOWED:
self.add_error(
"server",
NameserverError(
code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED,
nameserver=domain,
ip=ip_list,
),
)
elif e.code == nsErrorCodes.MISSING_IP:
self.add_error(
"ip",
NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=domain, ip=ip_list),
)
elif e.code == nsErrorCodes.MISSING_HOST:
self.add_error(
"server",
NameserverError(code=nsErrorCodes.MISSING_HOST, nameserver=domain, ip=ip_list),
)
elif e.code == nsErrorCodes.INVALID_HOST:
self.add_error(
"server",
NameserverError(code=nsErrorCodes.INVALID_HOST, nameserver=server, ip=ip_list),
)
else:
self.add_error("ip", str(e))
DomainDsdataFormset = formset_factory( DomainDsdataFormset = formset_factory(
DomainDsdataForm, DomainDsdataForm,

View file

@ -17,6 +17,8 @@ from registrar.utility.errors import (
SecurityEmailErrorCodes, SecurityEmailErrorCodes,
GenericError, GenericError,
GenericErrorCodes, GenericErrorCodes,
DsDataError,
DsDataErrorCodes,
) )
from registrar.models import ( from registrar.models import (
@ -1878,7 +1880,30 @@ class TestDomainDNSSEC(TestDomainOverview):
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): 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. Uses self.app WebTest because we need to interact with forms.
""" """
@ -1887,13 +1912,100 @@ class TestDomainDNSSEC(TestDomainOverview):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# first two nameservers are required, so if we empty one out we should # first two nameservers are required, so if we empty one out we should
# get a form error # 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 with less_console_noise(): # swallow logged warning message
result = add_data_page.forms[0].submit() result = add_data_page.forms[0].submit()
# form submission was a post with an error, response should be a 200 # 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 # error text appears twice, once at the top of the page, once around
# the field. # 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_length(self):
"""DS Data form errors with invalid data (digest too long)
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"] = "1234567890123456789012345678901234567890123456789012345678901234567890"
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_LENGTH)), 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): class TestApplicationStatus(TestWithUser, WebTest):

View file

@ -125,9 +125,19 @@ class DsDataErrorCodes(IntEnum):
error mapping. error mapping.
Overview of ds data error codes: Overview of ds data error codes:
- 1 BAD_DATA bad data input in ds data - 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_LENGTH invalid digest length exceeds 64
- 5 INVALID_DIGEST_CHARS invalid chars in digest
- 6 INVALID_KEYTAG_SIZE invalid key tag size > 65535
""" """
BAD_DATA = 1 BAD_DATA = 1
INVALID_DIGEST_SHA1 = 2
INVALID_DIGEST_SHA256 = 3
INVALID_DIGEST_LENGTH = 4
INVALID_DIGEST_CHARS = 5
INVALID_KEYTAG_SIZE = 6
class DsDataError(Exception): class DsDataError(Exception):
@ -139,7 +149,22 @@ class DsDataError(Exception):
_error_mapping = { _error_mapping = {
DsDataErrorCodes.BAD_DATA: ( DsDataErrorCodes.BAD_DATA: (
"Theres something wrong with the DS data you provided. If you need help email us at help@get.gov." "Theres 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_LENGTH: (
"Digest must be at most 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): def __init__(self, *args, code=None, **kwargs):