mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-15 17:17:02 +02:00
digest validation; error messages; test cases
This commit is contained in:
parent
b22615e205
commit
48caa775dd
3 changed files with 155 additions and 65 deletions
|
@ -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,
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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: (
|
||||||
"There’s something wrong with the DS data you provided. If you need help email us at help@get.gov."
|
"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_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):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue